Skip to content

Commit 5c68da3

Browse files
authored
Merge pull request #18 from HubSpot/separate-key-command-controller
Separate key command controller and add Toolbar component
2 parents e94df53 + 6205531 commit 5c68da3

File tree

7 files changed

+326
-74
lines changed

7 files changed

+326
-74
lines changed

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,48 @@ const EditorWithPlugins = plugins(Editor);
110110
const toHTML = plugins(convertToHTML);
111111
const fromHTML = plugins(convertFromHTML);
112112
```
113+
114+
***
115+
116+
## KeyCommandController
117+
**Higher-order component to consolidate key command listeners across the component tree**
118+
119+
An increasingly common pattern for rich text editors is a toolbar detached from the main `Editor` component. This toolbar will be outside of the `Editor` component subtree, but will often need to respond to key commands that would otherwise be encapsulated by the `Editor`. `KeyCommandController` is a higher-order component that allows the subscription to key commands to move up the React tree so that components outside that subtree may listen and emit changes to editor state. `KeyCommandController`. It may be used with any component, but a good example is the `Toolbar` component:
120+
121+
```javascript
122+
import {Editor, Toolbar, KeyCommandController, compose} from 'draft-extend';
123+
124+
const plugins = compose(
125+
FirstPlugin,
126+
SecondPlugin
127+
);
128+
129+
const WrappedEditor = plugins(Editor);
130+
const WrappedToolbar = plugins(Toolbar);
131+
132+
const Parent = ({editorState, onChange, handleKeyCommand, addKeyCommandListener, removeKeyCommandListener}) => {
133+
return (
134+
<div>
135+
<WrappedEditor
136+
editorState={editorState}
137+
onChange={onChange}
138+
handleKeyCommand={handleKeyCommand}
139+
addKeyCommandListener={addKeyCommandListener}
140+
removeKeyCommandListener={removeKeyCommandListener}
141+
/>
142+
<WrappedToolbar
143+
editorState={editorState}
144+
onChange={onChange}
145+
addKeyCommandListener={addKeyCommandListener}
146+
removeKeyCommandListener={removeKeyCommandListener}
147+
/>
148+
</div>
149+
);
150+
};
151+
152+
export default KeyCommandController(Parent);
153+
```
154+
155+
`KeyCommandController` provides the final `handleKeyCommand` to use in the `Editor` component as well as subscribe/unsubscribe functions. As long as these props are passed from some common parent wrapped with `KeyCommandController` that also receives `editorState` and `onChange` props, other components may subscribe and emit chagnes to the editor state.
156+
157+
Additionally, `KeyCommandController`s are composable and will defer to the highest parent instance. That is, if a `KeyCommandController` receives `handleKeyCommand`, `addKeyCommandListener`, and `removeKeyCommandListener` props (presumably from another controller) it will delegate to that controller's record of subscribed functions, keeping all listeners in one place.

example/blockStyles.html

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
const {
3535
Editor,
36+
Toolbar,
37+
KeyCommandController,
3638
createPlugin
3739
} = window.DraftExtend;
3840

@@ -88,6 +90,7 @@
8890
});
8991

9092
const WithPlugin = BlockPlugin(Editor);
93+
const WithPluginToolbar = BlockPlugin(Toolbar);
9194
const toHTML = BlockPlugin(convertToHTML);
9295
const fromHTML = BlockPlugin(convertFromHTML);
9396

@@ -107,16 +110,26 @@
107110

108111
render() {
109112
return (
110-
<WithPlugin
111-
editorState={this.state.editorState}
112-
onChange={this.onChange}
113-
/>
113+
<div>
114+
<WithPlugin
115+
{...this.props}
116+
editorState={this.state.editorState}
117+
onChange={this.onChange}
118+
/>
119+
<WithPluginToolbar
120+
{...this.props}
121+
editorState={this.state.editorState}
122+
onChange={this.onChange}
123+
/>
124+
</div>
114125
);
115126
}
116127
});
117128

129+
const WrappedComponent = KeyCommandController(BlockStylesExample);
130+
118131
ReactDOM.render(
119-
<BlockStylesExample />,
132+
<WrappedComponent />,
120133
document.getElementById('target')
121134
);
122135
</script>

example/inlineStyles.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
<WithPlugin
112112
editorState={this.state.editorState}
113113
onChange={this.onChange}
114+
keyCommandListeners={[Draft.RichUtils.handleKeyCommand]}
114115
/>
115116
);
116117
}

src/components/Editor.js

Lines changed: 42 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
CompositeDecorator,
77
getDefaultKeyBinding
88
} from 'draft-js';
9+
import KeyCommandController from './KeyCommandController';
910
import OverlayWrapper from './OverlayWrapper';
1011

1112
const propTypes = {
@@ -19,16 +20,18 @@ const propTypes = {
1920
blockRendererFn: PropTypes.func,
2021
blockStyleFn: PropTypes.func,
2122
keyBindingFn: PropTypes.func,
22-
keyCommandListeners: PropTypes.arrayOf(PropTypes.func),
23+
addKeyCommandListener: PropTypes.func.isRequired,
24+
removeKeyCommandListener: PropTypes.func.isRequired,
2325
handleReturn: PropTypes.func,
2426
onEscape: PropTypes.func,
2527
onTab: PropTypes.func,
2628
onUpArrow: PropTypes.func,
2729
onDownArrow: PropTypes.func,
28-
readOnly: PropTypes.bool
30+
readOnly: PropTypes.bool,
31+
showButtons: PropTypes.bool
2932
};
3033

31-
export default React.createClass({
34+
const EditorWrapper = React.createClass({
3235
propTypes,
3336

3437
childContextTypes: {
@@ -52,8 +55,8 @@ export default React.createClass({
5255
blockRendererFn: () => {},
5356
blockStyleFn: () => {},
5457
keyBindingFn: () => {},
55-
keyCommandListeners: [],
56-
readOnly: false
58+
readOnly: false,
59+
showButtons: true
5760
};
5861
},
5962

@@ -76,10 +79,6 @@ export default React.createClass({
7679
};
7780
},
7881

79-
componentWillMount() {
80-
this.keyCommandListeners = List(this.props.keyCommandListeners);
81-
},
82-
8382
componentWillReceiveProps(nextProps) {
8483
if (nextProps.decorators.length === this.state.decorator._decorators.length) {
8584
const allDecoratorsMatch = this.state.decorator._decorators.every((decorator, i) => {
@@ -93,16 +92,6 @@ export default React.createClass({
9392
this.setState({decorator: new CompositeDecorator(nextProps.decorators)});
9493
},
9594

96-
addKeyCommandListener(listener) {
97-
this.keyCommandListeners = this.keyCommandListeners.unshift(listener);
98-
},
99-
100-
removeKeyCommandListener(listener) {
101-
this.keyCommandListeners = this.keyCommandListeners.filterNot((l) => {
102-
return l === listener;
103-
});
104-
},
105-
10695
keyBindingFn(e) {
10796
const pluginsCommand = this.props.keyBindingFn(e);
10897
if (pluginsCommand) {
@@ -112,61 +101,24 @@ export default React.createClass({
112101
return getDefaultKeyBinding(e);
113102
},
114103

115-
handleKeyCommand(command, keyboardEvent = null) {
116-
const decoratedState = this.getDecoratedState();
117-
118-
const result = this.keyCommandListeners.reduce(({state, hasChanged}, listener) => {
119-
if (hasChanged === true) {
120-
return {
121-
state,
122-
hasChanged
123-
};
124-
}
125-
126-
const listenerResult = listener(state, command, keyboardEvent);
127-
const isEditorState = listenerResult instanceof EditorState;
128-
129-
if (listenerResult === true || (isEditorState && listenerResult !== state)) {
130-
if (isEditorState) {
131-
this.props.onChange(listenerResult);
132-
return {
133-
state: listenerResult,
134-
hasChanged: true
135-
};
136-
}
137-
return {
138-
state,
139-
hasChanged: true
140-
};
141-
}
142-
143-
return {
144-
state,
145-
hasChanged
146-
};
147-
}, {state: decoratedState, hasChanged: false});
148-
149-
return result.hasChanged;
150-
},
151-
152104
handleReturn(e) {
153-
return (this.props.handleReturn && this.props.handleReturn(e)) || this.handleKeyCommand('return', e);
105+
return (this.props.handleReturn && this.props.handleReturn(e)) || this.props.handleKeyCommand('return', e);
154106
},
155107

156108
onEscape(e) {
157-
return (this.props.onEscape && this.props.onEscape(e)) || this.handleKeyCommand('escape', e);
109+
return (this.props.onEscape && this.props.onEscape(e)) || this.props.handleKeyCommand('escape', e);
158110
},
159111

160112
onTab(e) {
161-
return (this.props.onTab && this.props.onTab(e)) || this.handleKeyCommand('tab', e);
113+
return (this.props.onTab && this.props.onTab(e)) || this.props.handleKeyCommand('tab', e);
162114
},
163115

164116
onUpArrow(e) {
165-
return (this.props.onUpArrow && this.props.onUpArrow(e)) || this.handleKeyCommand('up-arrow', e);
117+
return (this.props.onUpArrow && this.props.onUpArrow(e)) || this.props.handleKeyCommand('up-arrow', e);
166118
},
167119

168120
onDownArrow(e) {
169-
return (this.props.onDownArrow && this.props.onDownArrow(e)) || this.handleKeyCommand('down-arrow', e);
121+
return (this.props.onDownArrow && this.props.onDownArrow(e)) || this.props.handleKeyCommand('down-arrow', e);
170122
},
171123

172124
focus() {
@@ -213,23 +165,41 @@ export default React.createClass({
213165
},
214166

215167
renderPluginButtons() {
168+
const {
169+
onChange,
170+
addKeyCommandListener,
171+
removeKeyCommandListener,
172+
showButtons
173+
} = this.props;
174+
175+
if (showButtons === false) {
176+
return null;
177+
}
178+
216179
const decoratedState = this.getDecoratedState();
217180

218181
return this.props.buttons.map((Button, index) => {
219182
return (
220183
<Button
221184
{...this.getOtherProps()}
222-
addKeyCommandListener={this.addKeyCommandListener}
185+
key={`button-${index}`}
186+
attachedToEditor={true}
223187
editorState={decoratedState}
224-
key={`button${index}`}
225-
onChange={this.props.onChange}
226-
removeKeyCommandListener={this.removeKeyCommandListener}
188+
onChange={onChange}
189+
addKeyCommandListener={addKeyCommandListener}
190+
removeKeyCommandListener={removeKeyCommandListener}
227191
/>
228192
);
229193
});
230194
},
231195

232196
renderOverlays() {
197+
const {
198+
onChange,
199+
addKeyCommandListener,
200+
removeKeyCommandListener
201+
} = this.props;
202+
233203
const decoratedState = this.getDecoratedState();
234204

235205
return this.props.overlays.map((Overlay, index) => {
@@ -238,9 +208,9 @@ export default React.createClass({
238208
<Overlay
239209
{...this.getOtherProps()}
240210
editorState={decoratedState}
241-
onChange={this.props.onChange}
242-
addKeyCommandListener={this.addKeyCommandListener}
243-
removeKeyCommandListener={this.removeKeyCommandListener}
211+
onChange={onChange}
212+
addKeyCommandListener={addKeyCommandListener}
213+
removeKeyCommandListener={removeKeyCommandListener}
244214
/>
245215
</OverlayWrapper>
246216
);
@@ -253,6 +223,7 @@ export default React.createClass({
253223
blockRendererFn,
254224
blockStyleFn,
255225
onChange,
226+
handleKeyCommand,
256227
...otherProps
257228
} = this.props;
258229

@@ -273,8 +244,8 @@ export default React.createClass({
273244
blockStyleFn={blockStyleFn}
274245
blockRendererFn={blockRendererFn}
275246
customStyleMap={styleMap}
247+
handleKeyCommand={handleKeyCommand}
276248
keyBindingFn={this.keyBindingFn}
277-
handleKeyCommand={this.handleKeyCommand}
278249
handleReturn={this.handleReturn}
279250
onEscape={this.onEscape}
280251
onTab={this.onTab}
@@ -292,3 +263,5 @@ export default React.createClass({
292263
);
293264
}
294265
});
266+
267+
export default KeyCommandController(EditorWrapper);

0 commit comments

Comments
 (0)