What is WYSIWYG?
In computing, WYSIWYG (/ˈwɪziwɪɡ/ WIZ-ee-wig), an acronym for What You See Is What You Get, is a system in which editing software allows content to be edited in a form that resembles its appearance when printed or displayed as a finished product, such as a printed document, web page, or slide presentation.
The Result#
draft-js-plugins-editor#
Draft.js
is a highly customizable but complex library to use.
draft-js-plugins-editor
makes it easier to working with Draft.js
, but it's not actively maintained right now.
Install#
1yarn add draft-js2yarn add -D @types/draft-js
Draft.js
is written in Flow, not TypeScript, so I installed @types/draft-js
as well.
1/** TextEditor.tsx */23import React from "react";4import { Editor, EditorState } from "draft-js";5import "draft-js/dist/Draft.css";67const TextEditor = () => {8 const [editorState, setEditorState] = React.useState<EditorState>(9 EditorState.createEmpty()10 );11 return <Editor editorState={editorState} onChange={setEditorState} />;12};1314export default TextEditor;
I called EditorState.createEmpty()
when initializing useState
hook, but you can also use EditorState.createWithContent
.
This will render a default contenteditable
component.
You can pass readOnly
prop to the Editor
to make it uneditable.
Bold, Italic, H1/H2/H3#
RichUtils
, the build in utilities of Draft.js
provide basic commands
.
Using Key Command#
1/** TextEditor.tsx */23...45import { Editor, EditorState, RichUtils, DraftEditorCommand } from "draft-js";67...89const TextEditor = () => {1011 ...1213 const handleKeyCommand = (command: DraftEditorCommand) => {14 const newState = RichUtils.handleKeyCommand(editorState, command);15 if (newState) {16 setEditorState(newState);17 return "handled";18 }19 return "not-handled";20 };2122 return (23 <Editor24 editorState={editorState}25 onChange={setEditorState}26 handleKeyCommand={handleKeyCommand}27 />28 );29};
You can also make your custom commands using handleKeyCommand
and keyBindingFn
(For example, saving contents with Command + S
).
Toolbar#
1/** TextEditor.tsx */23...45const TextEditor = () => {67 ...89 const handleTogggleClick = (e: React.MouseEvent, inlineStyle: string) => {10 e.preventDefault();11 setEditorState(RichUtils.toggleInlineStyle(editorState, inlineStyle));12 };1314 const handleBlockClick = (e: React.MouseEvent, blockType: string) => {15 e.preventDefault();16 setEditorState(RichUtils.toggleBlockType(editorState, blockType));17 };1819 return (20 <div>21 <button onMouseDown={(e) => handleBlockClick(e, "header-one")}>H1</button>22 <button onMouseDown={(e) => handleBlockClick(e, "header-two")}>H2</button>23 <button onMouseDown={(e) => handleBlockClick(e, "header-three")}>H3</button>24 <button onMouseDown={(e) => handleBlockClick(e, "unstyled")}>Normal</button>25 <button onMouseDown={(e) => handleTogggleClick(e, "BOLD")}>bold</button>26 <button onMouseDown={(e) => handleTogggleClick(e, "ITALIC")}>italic</button>27 <button onMouseDown={(e) => handleTogggleClick(e, "STRIKETHROUGH")}>strikthrough</button>28 <button onMouseDown={(e) => handleBlockClick(e, "ordered-list-item")}>Ordered List</button>29 <button onMouseDown={(e) => handleBlockClick(e, "unordered-list-item")}>Unordered List</button>30 <Editor editorState={editorState} onChange={setEditorState} handleKeyCommand={handleKeyCommand} />31 </div>32 );33};3435export default TextEditor;
Redo / Undo#
1/** TextEditor.tsx */23...4 <button5 disabled={editorState.getUndoStack().size <= 0}6 onMouseDown={() => setEditorState(EditorState.undo(editorState))}>7 undo8 </button>9 <button10 disabled={editorState.getRedoStack().size <= 0}11 onMouseDown={() => setEditorState(EditorState.redo(editorState))}>12 redo13 </button>14...
Add Link#
To render links, we need to provide decorator
.
1/** Link.tsx */23import { CompositeDecorator, DraftDecoratorComponentProps } from "draft-js";45export const Link = (props: DraftDecoratorComponentProps) => {6 const { url } = props.contentState.getEntity(props.entityKey).getData();7 return (8 <a rel="noopener noreferrer" target="_blank" href={url}>9 {props.children}10 </a>11 );12};1314export const linkDecorator = new CompositeDecorator([15 {16 strategy: (contentBlock, callback, contentState) => {17 contentBlock.findEntityRanges((character) => {18 const entityKey = character.getEntity();19 return entityKey !== null && contentState.getEntity(entityKey).getType() === "LINK";20 }, callback);21 },22 component: Link23 }24]);
We should filter links with strategy
method and render them with component
prop by passing them into CompositeDecorator
.
DraftDecoratorComponentProps
is not included to @types/draft-js
currently, so I extended it based on Flow
type defined in Draft.js
library. (See here)
1/** module.d.ts */23import 'draft-js';45declare module 'draft-js' {6 export interface DraftDecoratorComponentProps {7 blockKey: any;8 children?: ReactNode;9 contentState: ContentState;10 decoratedText: string;11 dir?: any;12 end: number;13 entityKey?: string;14 offsetKey: string;15 start: number;16 }17}
Then, we need to build a method to add a link entity to the editor.
1/** TextEditor.tsx */23...4const handleAddLink = () => {5 const selection = editorState.getSelection();6 const link = prompt("Please enter the URL of your link");7 if (!link) {8 setEditorState(RichUtils.toggleLink(editorState, selection, null));9 return;10 }11 const content = editorState.getCurrentContent();12 const contentWithEntity = content.createEntity("LINK", "MUTABLE", {13 url: link14 });15 const newEditorState = EditorState.push(editorState, contentWithEntity, "apply-entity");16 const entityKey = contentWithEntity.getLastCreatedEntityKey();17 setEditorState(RichUtils.toggleLink(newEditorState, selection, entityKey));18 };19...
1/** TextEditor.tsx */23import React from "react";4import { Editor, EditorState, RichUtils, DraftEditorCommand } from "draft-js";5import "draft-js/dist/Draft.css";6import { linkDecorator } from "./Link";78const TextEditor = () => {9 const [editorState, setEditorState] = React.useState<EditorState>(EditorState.createEmpty(linkDecorator));1011 ...1213 return (14 <div>15 ...16 <button17 {/* If nothing is selected, disable link button */}18 disabled={editorState.getSelection().isCollapsed()}19 onMouseDown={(e) => {20 e.preventDefault();21 handleAddLink();22 }}>23 link24 </button>25 ...26 );27};
Insert Images#
We can think of several ways to insert images, like copy and paste / drag and drop / type URL.
We will type URL to insert images here, but if you want to implement copy and paste / drag and drop as well, you can usehandlePastedFiles
or handleDroppedFiles
props of the Editor
.
1/** TextEditor.tsx */23...4 const handleInsertImage = () => {5 const src = prompt("Please enter the URL of your picture");6 if (!src) {7 return;8 }9 const contentState = editorState.getCurrentContent();10 const contentStateWithEntity = contentState.createEntity("image", "IMMUTABLE", { src });11 const entityKey = contentStateWithEntity.getLastCreatedEntityKey();12 const newEditorState = EditorState.set(editorState, {13 currentContent: contentStateWithEntity14 });15 return setEditorState(AtomicBlockUtils.insertAtomicBlock(newEditorState, entityKey, " "));16 };17...
Since images are custom Block
, we need to use blockRendererFn
prop of the Editor
.
1/** Media.tsx */23import React from 'react';4import { ContentBlock, ContentState } from 'draft-js';56interface BlockComponentProps {7 contentState: ContentState;8 block: ContentBlock;9}1011export const Image = (props: BlockComponentProps) => {12 const { block, contentState } = props;13 const { src } = contentState.getEntity(block.getEntityAt(0)).getData();14 return <img src={src} alt={src} role="presentation" />;15};1617const Media = (props: BlockComponentProps) => {18 const entity = props.contentState.getEntity(props.block.getEntityAt(0));19 const type = entity.getType();2021 let media = null;22 if (type === 'image') {23 media = <Image {...props} />;24 }2526 return media;27};2829export const mediaBlockRenderer = (block: ContentBlock) => {30 if (block.getType() === 'atomic') {31 return {32 component: Media,33 editable: false,34 };35 }36 return null;37};
1/** TextEditor.tsx */23...4 <Editor5 editorState={editorState}6 onChange={setEditorState}7 handleKeyCommand={handleKeyCommand}8 blockRendererFn={mediaBlockRenderer}9 />10...
Save & Reload#
To save the contents, we need to convert them into JSON (We will use localStorage
here).
You can also convert them into HTML by using libraries like draft-js-export-html
.
1/** TextEditor.tsx */23import React from "react";4import {5 ...6 convertToRaw,7 convertFromRaw8} from "draft-js";910...1112const TEXT_EDITOR_ITEM = "draft-js-example-item";1314const TextEditor = () => {15 const data = localStorage.getItem(TEXT_EDITOR_ITEM);1617 const initialState = data18 ? EditorState.createWithContent(convertFromRaw(JSON.parse(data)), linkDecorator)19 : EditorState.createEmpty(linkDecorator);2021 const [editorState, setEditorState] = React.useState<EditorState>(initialState);2223 const handleSave = () => {24 const data = JSON.stringify(convertToRaw(editorState.getCurrentContent()));25 localStorage.setItem(TEXT_EDITOR_ITEM, data);26 };2728 ...2930 return (31 <div>32 ...33 <Editor34 editorState={editorState}35 onChange={setEditorState}36 handleKeyCommand={handleKeyCommand}37 blockRendererFn={mediaBlockRenderer}38 />39 <button40 onClick={(e) => {41 e.preventDefault();42 handleSave();43 }}>44 save45 </button>46 </div>47 );48};