In this article, you will learn how to make WYSIWYG rich text editor using React and Draft.js. The source code of this React WYSIWYG editor is given below.
At the end of this article, I have provided links to view the Live Demo and Download the full source code from GitHub.
Project Screenshot
Here’s a screenshot of how our WYSIWYG Editor Using React and Draft.js will look.

Features
- Configurable toolbar with option to add/remove controls.
- Option to change the order of the controls in the toolbar.
- Option to add custom controls to the toolbar.
- Option to change styles and icons in the toolbar.
- Option to show toolbar only when editor is focused.
- Support for inline styles: Bold, Italic, Underline, StrikeThrough, Code, Subscript, Superscript.
- Support for block types: Paragraph, H1 – H6, Blockquote, Code.
- Support for setting font-size and font-family.
- Support for ordered / unordered lists and indenting.
- Support for text-alignment.
- Support for coloring text or background.
- Support for adding / editing links
- Choice of more than 150 emojis.
- Support for mentions.
- Support for hashtags.
- Support for adding / uploading images.
- Support for aligning images, setting height, width.
- Support for Embedded links, flexibility to set height and width.
- Option provided to remove added styling.
- Option of undo and redo.
- Configurable behavior for RTL and Spellcheck.
- Support for placeholder.
- Support for WAI-ARIA Support attributes
- Using editor as controlled or un-controlled React component.
- Support to convert Editor Content to HTML, JSON, Markdown.
- Support to convert the HTML generated by the editor back to editor content.
- Support for internationalization.
Source Code of React WYSIWYG Editor Using Draft.js
package.json
JSON
x
83
83
1
{
2
"name": "react-draft-wysiwyg",
3
"version": "1.15.0",
4
"description": "A wysiwyg on top of DraftJS.",
5
"main": "dist/react-draft-wysiwyg.js",
6
"repository": {
7
"type": "git",
8
"url": "https://github.com/jpuri/react-draft-wysiwyg.git"
9
},
10
"author": "Jyoti Puri",
11
"devDependencies": {
12
"@babel/core": "^7.7.4",
13
"@babel/preset-env": "^7.7.4",
14
"@babel/preset-react": "^7.7.4",
15
"@babel/register": "^7.7.4",
16
"@storybook/react": "^5.2.8",
17
"autoprefixer": "^9.7.3",
18
"babel-eslint": "^10.0.3",
19
"babel-loader": "^8.0.6",
20
"babel-plugin-transform-flow-strip-types": "^6.22.0",
21
"chai": "^4.2.0",
22
"cross-env": "^6.0.3",
23
"css-loader": "^3.2.1",
24
"draft-js": "^0.11.2",
25
"draftjs-to-html": "^0.9.0",
26
"draftjs-to-markdown": "^0.6.0",
27
"embed-video": "^2.0.4",
28
"enzyme": "^3.10.0",
29
"enzyme-adapter-react-16": "^1.15.1",
30
"eslint": "^6.7.2",
31
"eslint-config-airbnb": "^18.0.1",
32
"eslint-plugin-import": "^2.18.2",
33
"eslint-plugin-jsx-a11y": "^6.2.3",
34
"eslint-plugin-mocha": "^6.2.2",
35
"eslint-plugin-react": "^7.17.0",
36
"file-loader": "^5.0.2",
37
"flow-bin": "^0.113.0",
38
"immutable": "^4.0.0-rc.12",
39
"jsdom": "^15.2.1",
40
"mini-css-extract-plugin": "^0.8.0",
41
"mocha": "^6.2.2",
42
"postcss-loader": "^3.0.0",
43
"precss": "^4.0.0",
44
"react": "^16.12.0",
45
"react-addons-test-utils": "^15.6.2",
46
"react-dom": "^16.12.0",
47
"react-test-renderer": "^16.12.0",
48
"rimraf": "^3.0.0",
49
"sinon": "^7.5.0",
50
"style-loader": "^1.0.1",
51
"uglifyjs-webpack-plugin": "^2.2.0",
52
"url-loader": "^3.0.0",
53
"webpack": "^4.41.2",
54
"webpack-bundle-analyzer": "^3.6.0",
55
"webpack-cli": "^3.3.10"
56
},
57
"dependencies": {
58
"classnames": "^2.2.6",
59
"draftjs-utils": "^0.10.2",
60
"html-to-draftjs": "^1.5.0",
61
"linkify-it": "^2.2.0",
62
"prop-types": "^15.7.2"
63
},
64
"peerDependencies": {
65
"draft-js": "^0.10.x || ^0.11.x",
66
"immutable": "3.x.x || 4.x.x",
67
"react": "0.13.x || 0.14.x || ^15.0.0-0 || 15.x.x || ^16.0.0-0 || ^16.x.x || ^17.x.x || ^18.x.x",
68
"react-dom": "0.13.x || 0.14.x || ^15.0.0-0 || 15.x.x || ^16.0.0-0 || ^16.x.x || ^17.x.x || ^18.x.x"
69
},
70
"scripts": {
71
"clean": "rimraf dist",
72
"build:webpack": "cross-env NODE_ENV=production webpack --mode production --config config/webpack.config.js",
73
"build": "npm run clean && npm run build:webpack",
74
"test": "cross-env BABEL_ENV=test mocha --require config/test-compiler.js config/test-setup.js src/**/*Test.js",
75
"lint": "eslint src",
76
"lintdocs": "eslint docs/src",
77
"flow": "flow; test $? -eq 0 -o $? -eq 2",
78
"check": "npm run lint && npm run flow",
79
"storybook": "start-storybook -p 6006",
80
"build-storybook": "build-storybook"
81
},
82
"license": "MIT"
83
}
index.js
JavaScript
1
564
564
1
import React, { Component } from 'react';
2
import PropTypes from 'prop-types';
3
import {
4
Editor,
5
EditorState,
6
RichUtils,
7
convertToRaw,
8
convertFromRaw,
9
CompositeDecorator,
10
getDefaultKeyBinding,
11
} from 'draft-js';
12
import {
13
changeDepth,
14
handleNewLine,
15
blockRenderMap,
16
getCustomStyleMap,
17
extractInlineStyle,
18
getSelectedBlocksType,
19
} from 'draftjs-utils';
20
import classNames from 'classnames';
21
import ModalHandler from '../event-handler/modals';
22
import FocusHandler from '../event-handler/focus';
23
import KeyDownHandler from '../event-handler/keyDown';
24
import SuggestionHandler from '../event-handler/suggestions';
25
import blockStyleFn from '../utils/BlockStyle';
26
import { mergeRecursive } from '../utils/toolbar';
27
import { hasProperty, filter } from '../utils/common';
28
import { handlePastedText } from '../utils/handlePaste';
29
import Controls from '../controls';
30
import getLinkDecorator from '../decorators/Link';
31
import getMentionDecorators from '../decorators/Mention';
32
import getHashtagDecorator from '../decorators/HashTag';
33
import getBlockRenderFunc from '../renderer';
34
import defaultToolbar from '../config/defaultToolbar';
35
import localeTranslations from '../i18n';
36
import './styles.css';
37
import '../../css/Draft.css';
38
39
class WysiwygEditor extends Component {
40
constructor(props) {
41
super(props);
42
const toolbar = mergeRecursive(defaultToolbar, props.toolbar);
43
const wrapperId = props.wrapperId
44
? props.wrapperId
45
: Math.floor(Math.random() * 10000);
46
this.wrapperId = `rdw-wrapper-${wrapperId}`;
47
this.modalHandler = new ModalHandler();
48
this.focusHandler = new FocusHandler();
49
this.blockRendererFn = getBlockRenderFunc(
50
{
51
isReadOnly: this.isReadOnly,
52
isImageAlignmentEnabled: this.isImageAlignmentEnabled,
53
getEditorState: this.getEditorState,
54
onChange: this.onChange,
55
},
56
props.customBlockRenderFunc
57
);
58
this.editorProps = this.filterEditorProps(props);
59
this.customStyleMap = this.getStyleMap(props);
60
this.compositeDecorator = this.getCompositeDecorator(toolbar);
61
const editorState = this.createEditorState(this.compositeDecorator);
62
extractInlineStyle(editorState);
63
this.state = {
64
editorState,
65
editorFocused: false,
66
toolbar,
67
};
68
}
69
70
componentDidMount() {
71
this.modalHandler.init(this.wrapperId);
72
}
73
// todo: change decorators depending on properties recceived in componentWillReceiveProps.
74
75
componentDidUpdate(prevProps) {
76
if (prevProps === this.props) return;
77
const newState = {};
78
const { editorState, contentState } = this.props;
79
if (!this.state.toolbar) {
80
const toolbar = mergeRecursive(defaultToolbar, toolbar);
81
newState.toolbar = toolbar;
82
}
83
if (
84
hasProperty(this.props, 'editorState') &&
85
editorState !== prevProps.editorState
86
) {
87
if (editorState) {
88
newState.editorState = EditorState.set(editorState, {
89
decorator: this.compositeDecorator,
90
});
91
} else {
92
newState.editorState = EditorState.createEmpty(this.compositeDecorator);
93
}
94
} else if (
95
hasProperty(this.props, 'contentState') &&
96
contentState !== prevProps.contentState
97
) {
98
if (contentState) {
99
const newEditorState = this.changeEditorState(contentState);
100
if (newEditorState) {
101
newState.editorState = newEditorState;
102
}
103
} else {
104
newState.editorState = EditorState.createEmpty(this.compositeDecorator);
105
}
106
}
107
if (
108
prevProps.editorState !== editorState ||
109
prevProps.contentState !== contentState
110
) {
111
extractInlineStyle(newState.editorState);
112
}
113
if (Object.keys(newState).length) this.setState(newState);
114
this.editorProps = this.filterEditorProps(this.props);
115
this.customStyleMap = this.getStyleMap(this.props);
116
}
117
118
onEditorBlur = () => {
119
this.setState({
120
editorFocused: false,
121
});
122
};
123
124
onEditorFocus = event => {
125
const { onFocus } = this.props;
126
this.setState({
127
editorFocused: true,
128
});
129
const editFocused = this.focusHandler.isEditorFocused();
130
if (onFocus && editFocused) {
131
onFocus(event);
132
}
133
};
134
135
onEditorMouseDown = () => {
136
this.focusHandler.onEditorMouseDown();
137
};
138
139
keyBindingFn = event => {
140
if (event.key === 'Tab') {
141
const { onTab } = this.props;
142
if (!onTab || !onTab(event)) {
143
const editorState = changeDepth(
144
this.state.editorState,
145
event.shiftKey ? -1 : 1,
146
4
147
);
148
if (editorState && editorState !== this.state.editorState) {
149
this.onChange(editorState);
150
event.preventDefault();
151
}
152
}
153
return null;
154
}
155
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
156
if (SuggestionHandler.isOpen()) {
157
event.preventDefault();
158
}
159
}
160
return getDefaultKeyBinding(event);
161
};
162
163
onToolbarFocus = event => {
164
const { onFocus } = this.props;
165
if (onFocus && this.focusHandler.isToolbarFocused()) {
166
onFocus(event);
167
}
168
};
169
170
onWrapperBlur = event => {
171
const { onBlur } = this.props;
172
if (onBlur && this.focusHandler.isEditorBlur(event)) {
173
onBlur(event, this.getEditorState());
174
}
175
};
176
177
onChange = editorState => {
178
const { readOnly, onEditorStateChange } = this.props;
179
if (
180
!readOnly &&
181
!(
182
getSelectedBlocksType(editorState) === 'atomic' &&
183
editorState.getSelection().isCollapsed
184
)
185
) {
186
if (onEditorStateChange) {
187
onEditorStateChange(editorState, this.props.wrapperId);
188
}
189
if (!hasProperty(this.props, 'editorState')) {
190
this.setState({ editorState }, this.afterChange(editorState));
191
} else {
192
this.afterChange(editorState);
193
}
194
}
195
};
196
197
setWrapperReference = ref => {
198
this.wrapper = ref;
199
};
200
201
setEditorReference = ref => {
202
if (this.props.editorRef) {
203
this.props.editorRef(ref);
204
}
205
this.editor = ref;
206
};
207
208
getCompositeDecorator = toolbar => {
209
const decorators = [
210
this.props.customDecorators,
211
getLinkDecorator({
212
showOpenOptionOnHover: toolbar.link.showOpenOptionOnHover,
213
}),
214
];
215
if (this.props.mention) {
216
decorators.push(
217
getMentionDecorators({
218
this.props.mention,
219
onChange: this.onChange,
220
getEditorState: this.getEditorState,
221
getSuggestions: this.getSuggestions,
222
getWrapperRef: this.getWrapperRef,
223
modalHandler: this.modalHandler,
224
})
225
);
226
}
227
if (this.props.hashtag) {
228
decorators.push(getHashtagDecorator(this.props.hashtag));
229
}
230
return new CompositeDecorator(decorators);
231
};
232
233
getWrapperRef = () => this.wrapper;
234
235
getEditorState = () => this.state ? this.state.editorState : null;
236
237
getSuggestions = () => this.props.mention && this.props.mention.suggestions;
238
239
afterChange = editorState => {
240
setTimeout(() => {
241
const { onChange, onContentStateChange } = this.props;
242
if (onChange) {
243
onChange(convertToRaw(editorState.getCurrentContent()));
244
}
245
if (onContentStateChange) {
246
onContentStateChange(convertToRaw(editorState.getCurrentContent()));
247
}
248
});
249
};
250
251
isReadOnly = () => this.props.readOnly;
252
253
isImageAlignmentEnabled = () => this.state.toolbar.image.alignmentEnabled;
254
255
createEditorState = compositeDecorator => {
256
let editorState;
257
if (hasProperty(this.props, 'editorState')) {
258
if (this.props.editorState) {
259
editorState = EditorState.set(this.props.editorState, {
260
decorator: compositeDecorator,
261
});
262
}
263
} else if (hasProperty(this.props, 'defaultEditorState')) {
264
if (this.props.defaultEditorState) {
265
editorState = EditorState.set(this.props.defaultEditorState, {
266
decorator: compositeDecorator,
267
});
268
}
269
} else if (hasProperty(this.props, 'contentState')) {
270
if (this.props.contentState) {
271
const contentState = convertFromRaw(this.props.contentState);
272
editorState = EditorState.createWithContent(
273
contentState,
274
compositeDecorator
275
);
276
editorState = EditorState.moveSelectionToEnd(editorState);
277
}
278
} else if (
279
hasProperty(this.props, 'defaultContentState') ||
280
hasProperty(this.props, 'initialContentState')
281
) {
282
let contentState =
283
this.props.defaultContentState || this.props.initialContentState;
284
if (contentState) {
285
contentState = convertFromRaw(contentState);
286
editorState = EditorState.createWithContent(
287
contentState,
288
compositeDecorator
289
);
290
editorState = EditorState.moveSelectionToEnd(editorState);
291
}
292
}
293
if (!editorState) {
294
editorState = EditorState.createEmpty(compositeDecorator);
295
}
296
return editorState;
297
};
298
299
filterEditorProps = props =>
300
filter(props, [
301
'onChange',
302
'onEditorStateChange',
303
'onContentStateChange',
304
'initialContentState',
305
'defaultContentState',
306
'contentState',
307
'editorState',
308
'defaultEditorState',
309
'locale',
310
'localization',
311
'toolbarOnFocus',
312
'toolbar',
313
'toolbarCustomButtons',
314
'toolbarClassName',
315
'editorClassName',
316
'toolbarHidden',
317
'wrapperClassName',
318
'toolbarStyle',
319
'editorStyle',
320
'wrapperStyle',
321
'uploadCallback',
322
'onFocus',
323
'onBlur',
324
'onTab',
325
'mention',
326
'hashtag',
327
'ariaLabel',
328
'customBlockRenderFunc',
329
'customDecorators',
330
'handlePastedText',
331
'customStyleMap',
332
]);
333
334
getStyleMap = props => ({ getCustomStyleMap(), props.customStyleMap });
335
336
changeEditorState = contentState => {
337
const newContentState = convertFromRaw(contentState);
338
let { editorState } = this.state;
339
editorState = EditorState.push(
340
editorState,
341
newContentState,
342
'insert-characters'
343
);
344
editorState = EditorState.moveSelectionToEnd(editorState);
345
return editorState;
346
};
347
348
focusEditor = () => {
349
setTimeout(() => {
350
this.editor.focus();
351
});
352
};
353
354
handleKeyCommand = command => {
355
const {
356
editorState,
357
toolbar: { inline },
358
} = this.state;
359
if (inline && inline.options.indexOf(command) >= 0) {
360
const newState = RichUtils.handleKeyCommand(editorState, command);
361
if (newState) {
362
this.onChange(newState);
363
return true;
364
}
365
}
366
return false;
367
};
368
369
handleReturn = event => {
370
if (SuggestionHandler.isOpen()) {
371
return true;
372
}
373
const { editorState } = this.state;
374
const newEditorState = handleNewLine(editorState, event);
375
if (newEditorState) {
376
this.onChange(newEditorState);
377
return true;
378
}
379
return false;
380
};
381
382
handlePastedTextFn = (text, html) => {
383
const { editorState } = this.state;
384
const {
385
handlePastedText: handlePastedTextProp,
386
stripPastedStyles,
387
} = this.props;
388
389
if (handlePastedTextProp) {
390
return handlePastedTextProp(text, html, editorState, this.onChange);
391
}
392
if (!stripPastedStyles) {
393
return handlePastedText(text, html, editorState, this.onChange);
394
}
395
return false;
396
};
397
398
preventDefault = event => {
399
if (
400
event.target.tagName === 'INPUT' ||
401
event.target.tagName === 'LABEL' ||
402
event.target.tagName === 'TEXTAREA'
403
) {
404
this.focusHandler.onInputMouseDown();
405
} else {
406
event.preventDefault();
407
}
408
};
409
410
render() {
411
const { editorState, editorFocused, toolbar } = this.state;
412
const {
413
locale,
414
localization: { locale: newLocale, translations },
415
toolbarCustomButtons,
416
toolbarOnFocus,
417
toolbarClassName,
418
toolbarHidden,
419
editorClassName,
420
wrapperClassName,
421
toolbarStyle,
422
editorStyle,
423
wrapperStyle,
424
uploadCallback,
425
ariaLabel,
426
} = this.props;
427
428
const controlProps = {
429
modalHandler: this.modalHandler,
430
editorState,
431
onChange: this.onChange,
432
translations: {
433
localeTranslations[locale || newLocale],
434
translations,
435
},
436
};
437
const toolbarShow =
438
editorFocused || this.focusHandler.isInputFocused() || !toolbarOnFocus;
439
return (
440
<div
441
id={this.wrapperId}
442
className={classNames(wrapperClassName, 'rdw-editor-wrapper')}
443
style={wrapperStyle}
444
onClick={this.modalHandler.onEditorClick}
445
onBlur={this.onWrapperBlur}
446
aria-label="rdw-wrapper"
447
>
448
{!toolbarHidden && (
449
<div
450
className={classNames('rdw-editor-toolbar', toolbarClassName)}
451
style={{
452
visibility: toolbarShow ? 'visible' : 'hidden',
453
toolbarStyle,
454
}}
455
onMouseDown={this.preventDefault}
456
aria-label="rdw-toolbar"
457
aria-hidden={(!editorFocused && toolbarOnFocus).toString()}
458
onFocus={this.onToolbarFocus}
459
>
460
{toolbar.options.map((opt, index) => {
461
const Control = Controls[opt];
462
const config = toolbar[opt];
463
if (opt === 'image' && uploadCallback) {
464
config.uploadCallback = uploadCallback;
465
}
466
return <Control key={index} {controlProps} config={config} />;
467
})}
468
{toolbarCustomButtons &&
469
toolbarCustomButtons.map((button, index) =>
470
React.cloneElement(button, { key: index, controlProps })
471
)}
472
</div>
473
)}
474
<div
475
ref={this.setWrapperReference}
476
className={classNames(editorClassName, 'rdw-editor-main')}
477
style={editorStyle}
478
onClick={this.focusEditor}
479
onFocus={this.onEditorFocus}
480
onBlur={this.onEditorBlur}
481
onKeyDown={KeyDownHandler.onKeyDown}
482
onMouseDown={this.onEditorMouseDown}
483
>
484
<Editor
485
ref={this.setEditorReference}
486
keyBindingFn={this.keyBindingFn}
487
editorState={editorState}
488
onChange={this.onChange}
489
blockStyleFn={blockStyleFn}
490
customStyleMap={this.getStyleMap(this.props)}
491
handleReturn={this.handleReturn}
492
handlePastedText={this.handlePastedTextFn}
493
blockRendererFn={this.blockRendererFn}
494
handleKeyCommand={this.handleKeyCommand}
495
ariaLabel={ariaLabel || 'rdw-editor'}
496
blockRenderMap={blockRenderMap}
497
{this.editorProps}
498
/>
499
</div>
500
</div>
501
);
502
}
503
}
504
505
WysiwygEditor.propTypes = {
506
onChange: PropTypes.func,
507
onEditorStateChange: PropTypes.func,
508
onContentStateChange: PropTypes.func,
509
// initialContentState is deprecated
510
initialContentState: PropTypes.object,
511
defaultContentState: PropTypes.object,
512
contentState: PropTypes.object,
513
editorState: PropTypes.object,
514
defaultEditorState: PropTypes.object,
515
toolbarOnFocus: PropTypes.bool,
516
spellCheck: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
517
stripPastedStyles: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
518
toolbar: PropTypes.object,
519
toolbarCustomButtons: PropTypes.array,
520
toolbarClassName: PropTypes.string,
521
toolbarHidden: PropTypes.bool,
522
locale: PropTypes.string,
523
localization: PropTypes.object,
524
editorClassName: PropTypes.string,
525
wrapperClassName: PropTypes.string,
526
toolbarStyle: PropTypes.object,
527
editorStyle: PropTypes.object,
528
wrapperStyle: PropTypes.object,
529
uploadCallback: PropTypes.func,
530
onFocus: PropTypes.func,
531
onBlur: PropTypes.func,
532
onTab: PropTypes.func,
533
mention: PropTypes.object,
534
hashtag: PropTypes.object,
535
textAlignment: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
536
readOnly: PropTypes.bool,
537
tabIndex: PropTypes.number, // eslint-disable-line react/no-unused-prop-types
538
placeholder: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
539
ariaLabel: PropTypes.string,
540
ariaOwneeID: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
541
ariaActiveDescendantID: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
542
ariaAutoComplete: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
543
ariaDescribedBy: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
544
ariaExpanded: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
545
ariaHasPopup: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
546
customBlockRenderFunc: PropTypes.func,
547
wrapperId: PropTypes.number,
548
customDecorators: PropTypes.array,
549
editorRef: PropTypes.func,
550
handlePastedText: PropTypes.func,
551
};
552
553
WysiwygEditor.defaultProps = {
554
toolbarOnFocus: false,
555
toolbarHidden: false,
556
stripPastedStyles: false,
557
localization: { locale: 'en', translations: {} },
558
customDecorators: [],
559
};
560
561
export default WysiwygEditor;
562
563
// todo: evaluate draftjs-utils to move some methods here
564
// todo: move color near font-family
styles.css
CSS
1
35
35
1
.rdw-editor-main {
2
height: 100%;
3
overflow: auto;
4
box-sizing: border-box;
5
}
6
.rdw-editor-toolbar {
7
padding: 6px 5px 0;
8
border-radius: 2px;
9
border: 1px solid #F1F1F1;
10
display: flex;
11
justify-content: flex-start;
12
background: white;
13
flex-wrap: wrap;
14
font-size: 15px;
15
margin-bottom: 5px;
16
user-select: none;
17
}
18
.public-DraftStyleDefault-block {
19
margin: 1em 0;
20
}
21
.rdw-editor-wrapper:focus {
22
outline: none;
23
}
24
.rdw-editor-wrapper {
25
box-sizing: content-box;
26
}
27
.rdw-editor-main blockquote {
28
border-left: 5px solid #f1f1f1;
29
padding-left: 5px;
30
}
31
.rdw-editor-main pre {
32
background: #f1f1f1;
33
border-radius: 3px;
34
padding: 1px 10px;
35
}