Skip to content

Commit cb6faf1

Browse files
authoredApr 24, 2017
Merge pull request #350 from mikefowler/add-save-key-handler
Add support for standard save keyboard shortcut
2 parents 2e6bb0a + 55fa2c1 commit cb6faf1

File tree

10 files changed

+118
-22
lines changed

10 files changed

+118
-22
lines changed
 

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"react-ace": "4.1.5",
3636
"react-dom": "15.4.1",
3737
"react-dropzone": "3.10.0",
38+
"react-hotkeys": "^0.9.0",
3839
"react-modal": "^1.7.3",
3940
"react-notification-system": "0.2.7",
4041
"react-redux": "5.0.1",

‎src/constants/keyboardShortcuts.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
'save': 'mod+s',
3+
};

‎src/containers/App.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import React, { Component, PropTypes } from 'react';
2+
import { HotKeys } from 'react-hotkeys';
3+
4+
import keyboardShortcuts from '../constants/keyboardShortcuts';
25

36
// Components
47
import Sidebar from './Sidebar';
@@ -9,7 +12,9 @@ class App extends Component {
912

1013
render() {
1114
return (
12-
<div className="wrapper">
15+
<HotKeys
16+
keyMap={keyboardShortcuts}
17+
className="wrapper">
1318
<Sidebar />
1419
<div className="container">
1520
<Header />
@@ -18,7 +23,7 @@ class App extends Component {
1823
</div>
1924
</div>
2025
<Notifications />
21-
</div>
26+
</HotKeys>
2227
);
2328
}
2429
}

‎src/containers/views/Configuration.js

+20-5
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@ import React, { Component, PropTypes } from 'react';
22
import { connect } from 'react-redux';
33
import { bindActionCreators } from 'redux';
44
import { withRouter } from 'react-router';
5+
import { HotKeys } from 'react-hotkeys';
56
import Editor from '../../components/Editor';
67
import Button from '../../components/Button';
78
import { putConfig, onEditorChange } from '../../actions/config';
89
import { getLeaveMessage } from '../../constants/lang';
9-
import { toYAML } from '../../utils/helpers';
10+
import { toYAML, preventDefault } from '../../utils/helpers';
1011

1112
export class Configuration extends Component {
1213

14+
constructor(props) {
15+
super(props);
16+
17+
this.handleClickSave = this.handleClickSave.bind(this);
18+
}
19+
1320
componentDidMount() {
1421
const { router, route } = this.props;
1522
router.setRouteLeaveHook(route, this.routerWillLeave.bind(this));
@@ -21,7 +28,10 @@ export class Configuration extends Component {
2128
}
2229
}
2330

24-
handleClickSave() {
31+
handleClickSave(e) {
32+
// Prevent the default event from bubbling
33+
preventDefault(e);
34+
2535
const { editorChanged, putConfig } = this.props;
2636
if (editorChanged) {
2737
const value = this.refs.editor.getValue();
@@ -31,13 +41,18 @@ export class Configuration extends Component {
3141

3242
render() {
3343
const { editorChanged, onEditorChange, config, updated } = this.props;
44+
45+
const keyboardHandlers = {
46+
'save': this.handleClickSave,
47+
};
48+
3449
return (
35-
<div>
50+
<HotKeys handlers={keyboardHandlers}>
3651
<div className="content-header">
3752
<h1>Configuration</h1>
3853
<div className="page-buttons">
3954
<Button
40-
onClick={() => this.handleClickSave()}
55+
onClick={this.handleClickSave}
4156
type="save"
4257
active={editorChanged}
4358
triggered={updated} />
@@ -48,7 +63,7 @@ export class Configuration extends Component {
4863
onEditorChange={onEditorChange}
4964
content={toYAML(config)}
5065
ref="editor" />
51-
</div>
66+
</HotKeys>
5267
);
5368
}
5469
}

‎src/containers/views/DataFileEdit.js

+22-5
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { connect } from 'react-redux';
33
import { bindActionCreators } from 'redux';
44
import { browserHistory, withRouter, Link } from 'react-router';
55
import _ from 'underscore';
6+
import { HotKeys } from 'react-hotkeys';
67
import Breadcrumbs from '../../components/Breadcrumbs';
78
import Splitter from '../../components/Splitter';
89
import Errors from '../../components/Errors';
910
import Editor from '../../components/Editor';
1011
import Button from '../../components/Button';
1112
import { clearErrors } from '../../actions/utils';
12-
import { getFilenameFromPath } from '../../utils/helpers';
13+
import { getFilenameFromPath, preventDefault } from '../../utils/helpers';
1314
import {
1415
fetchDataFile, putDataFile, deleteDataFile, onDataFileChanged
1516
} from '../../actions/datafiles';
@@ -20,6 +21,12 @@ import { ADMIN_PREFIX } from '../../constants';
2021

2122
export class DataFileEdit extends Component {
2223

24+
constructor(props) {
25+
super(props);
26+
27+
this.handleClickSave = this.handleClickSave.bind(this);
28+
}
29+
2330
componentDidMount() {
2431
const { fetchDataFile, params, router, route } = this.props;
2532
router.setRouteLeaveHook(route, this.routerWillLeave.bind(this));
@@ -40,8 +47,12 @@ export class DataFileEdit extends Component {
4047
}
4148
}
4249

43-
handleClickSave() {
50+
handleClickSave(e) {
4451
const { datafileChanged, putDataFile, params } = this.props;
52+
53+
// Prevent the default event from bubbling
54+
preventDefault(e);
55+
4556
if (datafileChanged) {
4657
const value = this.refs.editor.getValue();
4758
putDataFile(params.data_file, value);
@@ -74,8 +85,14 @@ export class DataFileEdit extends Component {
7485
const { path, raw_content } = datafile;
7586
const filename = getFilenameFromPath(path);
7687

88+
const keyboardHandlers = {
89+
'save': this.handleClickSave,
90+
};
91+
7792
return (
78-
<div className="single">
93+
<HotKeys
94+
handlers={keyboardHandlers}
95+
className="single">
7996
{errors.length > 0 && <Errors errors={errors} />}
8097
<div className="content-header">
8198
<Breadcrumbs splat={filename} type="datafiles" />
@@ -92,7 +109,7 @@ export class DataFileEdit extends Component {
92109

93110
<div className="content-side">
94111
<Button
95-
onClick={() => this.handleClickSave()}
112+
onClick={this.handleClickSave}
96113
type="save"
97114
active={datafileChanged}
98115
triggered={updated}
@@ -107,7 +124,7 @@ export class DataFileEdit extends Component {
107124
block />
108125
</div>
109126
</div>
110-
</div>
127+
</HotKeys>
111128
);
112129
}
113130
}

‎src/containers/views/DocumentEdit.js

+23-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
33
import { bindActionCreators } from 'redux';
44
import { browserHistory, withRouter, Link } from 'react-router';
55
import _ from 'underscore';
6+
import { HotKeys } from 'react-hotkeys';
67
import Splitter from '../../components/Splitter';
78
import Errors from '../../components/Errors';
89
import Breadcrumbs from '../../components/Breadcrumbs';
@@ -14,13 +15,20 @@ import Metadata from '../../containers/MetaFields';
1415
import { fetchDocument, deleteDocument, putDocument } from '../../actions/collections';
1516
import { updateTitle, updateBody, updatePath } from '../../actions/metadata';
1617
import { clearErrors } from '../../actions/utils';
18+
import { preventDefault } from '../../utils/helpers';
1719
import {
1820
getLeaveMessage, getDeleteMessage, getNotFoundMessage
1921
} from '../../constants/lang';
2022
import { ADMIN_PREFIX } from '../../constants';
2123

2224
export class DocumentEdit extends Component {
2325

26+
constructor(props) {
27+
super(props);
28+
29+
this.handleClickSave = this.handleClickSave.bind(this);
30+
}
31+
2432
componentDidMount() {
2533
const { fetchDocument, params, router, route } = this.props;
2634
const [directory, ...rest] = params.splat;
@@ -58,8 +66,12 @@ export class DocumentEdit extends Component {
5866
}
5967
}
6068

61-
handleClickSave() {
69+
handleClickSave(e) {
6270
const { putDocument, fieldChanged, params } = this.props;
71+
72+
// Prevent the default event from bubbling
73+
preventDefault(e);
74+
6375
if (fieldChanged) {
6476
const collection = params.collection_name;
6577
const [directory, ...rest] = params.splat;
@@ -101,8 +113,14 @@ export class DocumentEdit extends Component {
101113
} = currentDocument;
102114
const [directory, ...rest] = params.splat;
103115

116+
const keyboardHandlers = {
117+
'save': this.handleClickSave,
118+
};
119+
104120
return (
105-
<div className="single">
121+
<HotKeys
122+
handlers={keyboardHandlers}
123+
className="single">
106124
{errors.length > 0 && <Errors errors={errors} />}
107125
<div className="content-header">
108126
<Breadcrumbs
@@ -116,7 +134,7 @@ export class DocumentEdit extends Component {
116134
<InputTitle onChange={updateTitle} title={title} ref="title" />
117135
<MarkdownEditor
118136
onChange={updateBody}
119-
onSave={() => this.handleClickSave()}
137+
onSave={this.handleClickSave}
120138
placeholder="Body"
121139
initialValue={raw_content}
122140
ref="editor" />
@@ -126,7 +144,7 @@ export class DocumentEdit extends Component {
126144

127145
<div className="content-side">
128146
<Button
129-
onClick={() => this.handleClickSave()}
147+
onClick={this.handleClickSave}
130148
type="save"
131149
active={fieldChanged}
132150
triggered={updated}
@@ -150,7 +168,7 @@ export class DocumentEdit extends Component {
150168
block />
151169
</div>
152170
</div>
153-
</div>
171+
</HotKeys>
154172
);
155173
}
156174
}

‎src/containers/views/PageEdit.js

+23-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
33
import { bindActionCreators } from 'redux';
44
import { browserHistory, withRouter, Link } from 'react-router';
55
import _ from 'underscore';
6+
import { HotKeys } from 'react-hotkeys';
67
import Button from '../../components/Button';
78
import Splitter from '../../components/Splitter';
89
import Errors from '../../components/Errors';
@@ -14,13 +15,20 @@ import Metadata from '../MetaFields';
1415
import { fetchPage, deletePage, putPage } from '../../actions/pages';
1516
import { updateTitle, updateBody, updatePath } from '../../actions/metadata';
1617
import { clearErrors } from '../../actions/utils';
18+
import { preventDefault } from '../../utils/helpers';
1719
import {
1820
getLeaveMessage, getDeleteMessage, getNotFoundMessage
1921
} from '../../constants/lang';
2022
import { ADMIN_PREFIX } from '../../constants';
2123

2224
export class PageEdit extends Component {
2325

26+
constructor(props) {
27+
super(props);
28+
29+
this.handleClickSave = this.handleClickSave.bind(this);
30+
}
31+
2432
componentDidMount() {
2533
const { fetchPage, params, router, route } = this.props;
2634
const [directory, ...rest] = params.splat;
@@ -55,8 +63,12 @@ export class PageEdit extends Component {
5563
}
5664
}
5765

58-
handleClickSave() {
66+
handleClickSave(e) {
5967
const { putPage, fieldChanged, params } = this.props;
68+
69+
// Prevent the default event from bubbling
70+
preventDefault(e);
71+
6072
if (fieldChanged) {
6173
const [directory, ...rest] = params.splat;
6274
const filename = rest.join('.');
@@ -87,11 +99,17 @@ export class PageEdit extends Component {
8799
return <h1>{`Could not find the page.`}</h1>;
88100
}
89101

102+
const keyboardHandlers = {
103+
'save': this.handleClickSave,
104+
};
105+
90106
const { name, raw_content, http_url, path, front_matter } = page;
91107
const [directory, ...rest] = params.splat;
92108
const title = front_matter && front_matter.title ? front_matter.title : '';
93109
return (
94-
<div className="single">
110+
<HotKeys
111+
handlers={keyboardHandlers}
112+
className="single">
95113
{errors.length > 0 && <Errors errors={errors} />}
96114
<div className="content-header">
97115
<Breadcrumbs splat={directory || ''} type="pages" />
@@ -103,7 +121,7 @@ export class PageEdit extends Component {
103121
<InputTitle onChange={updateTitle} title={title} ref="title" />
104122
<MarkdownEditor
105123
onChange={updateBody}
106-
onSave={() => this.handleClickSave()}
124+
onSave={this.handleClickSave}
107125
placeholder="Body"
108126
initialValue={raw_content}
109127
ref="editor" />
@@ -113,7 +131,7 @@ export class PageEdit extends Component {
113131

114132
<div className="content-side">
115133
<Button
116-
onClick={() => this.handleClickSave()}
134+
onClick={this.handleClickSave}
117135
type="save"
118136
active={fieldChanged}
119137
triggered={updated}
@@ -134,7 +152,7 @@ export class PageEdit extends Component {
134152
block />
135153
</div>
136154
</div>
137-
</div>
155+
</HotKeys>
138156
);
139157
}
140158

‎src/styles/content.scss

+4
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@
102102
}
103103

104104
.single {
105+
&:focus {
106+
outline: none;
107+
}
108+
105109
.breadcrumbs {
106110
width: 100%;
107111
input {

‎src/styles/main.scss

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ b {
5353

5454
.wrapper {
5555
height: 100%;
56+
57+
&:focus {
58+
outline: none;
59+
}
5660
}
5761

5862
.container {

‎src/utils/helpers.js

+11
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,14 @@ export const existingUploadedFilenames = (uploadedFiles, currentFiles) => {
7474
.map(file => file.name)
7575
.value();
7676
};
77+
78+
/**
79+
* Given an Event object, prevents the default event
80+
* from bubbling, if possible.
81+
* @param {Event} event
82+
*/
83+
export const preventDefault = (event) => {
84+
if (event && event.preventDefault) {
85+
event.preventDefault();
86+
}
87+
};

0 commit comments

Comments
 (0)
Please sign in to comment.