Skip to content

Commit

Permalink
refactor: update rich text editor
Browse files Browse the repository at this point in the history
  • Loading branch information
shn2016 committed Feb 26, 2023
1 parent aa998fe commit 2411c4d
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 80 deletions.
1 change: 1 addition & 0 deletions scripts/generate-types/typesPostFixes.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ exports.replacements = (componentName) => ({
stateFromHTML: (input: string) => EditorState;
stateToPlainText: (input: ContentState) => string;
stateToEntityList: (input: ContentState) => RawDraftContentState['entityMap'];
plainTextFromHTML: (input: string) => string;
};`,
],
],
Expand Down
11 changes: 3 additions & 8 deletions src/components/RichTextEditor/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,8 @@ export interface RichTextEditorMentions {
export interface RichTextEditorProps {
className?: string;
placeholder?: string;
/**
* Editor State
*/
initialValue?: EditorState;
/**
* Editor State: Instance of <a href="https://draftjs.org/docs/api-reference-editor-state">draft-js editor state</a>
*/
value?: EditorState;
initialValue?: string;
value?: string;
onChange?: (...args: any[]) => any;
mentions?: RichTextEditorMentions[];
onFileSelect?: (...args: any[]) => any;
Expand All @@ -33,6 +27,7 @@ declare const RichTextEditor: React.FC<RichTextEditorProps> & {
stateFromHTML: (input: string) => EditorState;
stateToPlainText: (input: ContentState) => string;
stateToEntityList: (input: ContentState) => RawDraftContentState['entityMap'];
plainTextFromHTML: (input: string) => string;
};

export default RichTextEditor;
47 changes: 30 additions & 17 deletions src/components/RichTextEditor/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ import './styles.css';

const { EditorState, Modifier, convertToRaw, RichUtils } = draftJs;

const editorStateToHTML = (input) => stateToHTML(input.getCurrentContent());
const editorStateFromHTML = (input) => EditorState.createWithContent(stateFromHTML(input));
const isEditorStateEmpty = (html) => html === '<p><br></p>';

const getInitialEditorState = (value, initialValue) => {
if (value) {
return editorStateFromHTML(value);
}
if (initialValue) {
return editorStateFromHTML(initialValue);
}
return EditorState.createEmpty();
};

const RichTextEditor = ({
className,
value,
Expand All @@ -38,7 +52,7 @@ const RichTextEditor = ({

const classNames = cc('aui--editor-root', className);

const [editorState, setEditorState] = React.useState(initialValue || EditorState.createEmpty());
const [editorState, setEditorState] = React.useState(getInitialEditorState(value, initialValue));
const [isMentionListOpen, setIsMentionListOpen] = React.useState(false);
const [suggestions, setSuggestions] = React.useState(mentions);
const [files, setFiles] = React.useState({});
Expand Down Expand Up @@ -75,16 +89,16 @@ const RichTextEditor = ({
}, []);

const handleOnChange = (newState) => {
if (!value) {
setEditorState(newState);
}
if (onChange) {
onChange(newState);
const oldHTML = editorStateToHTML(editorState);
const newHTML = editorStateToHTML(newState);
if (oldHTML !== newHTML && onChange) {
onChange(isEditorStateEmpty(newHTML) ? '' : newHTML);
}
setEditorState(newState);
};

const insertText = (text) => {
const state = value || editorState;
const state = editorState;
const currentContent = state.getCurrentContent();
const currentSelection = state.getSelection();

Expand All @@ -95,7 +109,7 @@ const RichTextEditor = ({
};

const handleKeyCommand = (command) => {
const newState = RichUtils.handleKeyCommand(value || editorState, command);
const newState = RichUtils.handleKeyCommand(editorState, command);
if (newState) {
handleOnChange(newState);
return true;
Expand Down Expand Up @@ -139,7 +153,7 @@ const RichTextEditor = ({
<div className="aui--editor-container" onClick={focusEditor}>
<Editor
editorKey="editor"
editorState={value || editorState}
editorState={editorState}
handleKeyCommand={handleKeyCommand}
onChange={handleOnChange}
placeholder={placeholder}
Expand Down Expand Up @@ -172,8 +186,8 @@ const RichTextEditor = ({
<FilePreviewList files={files} onFileRemove={handleFileRemove} />
<div className="aui--editor-toolbar">
<div className="aui--editor-style-buttons">
<InlineStyleButtons editorState={value || editorState} onToggle={handleOnChange} />
<BlockStyleButtons editorState={value || editorState} onToggle={handleOnChange} />
<InlineStyleButtons editorState={editorState} onToggle={handleOnChange} />
<BlockStyleButtons editorState={editorState} onToggle={handleOnChange} />
</div>
<div data-testid="rich-text-editor-advanced-buttons" className="aui--editor-advanced-buttons">
{!_.isEmpty(mentions) && (
Expand All @@ -196,10 +210,8 @@ const RichTextEditor = ({
RichTextEditor.propTypes = {
className: PropTypes.string,
placeholder: PropTypes.string,
/** Editor State */
initialValue: PropTypes.instanceOf(EditorState),
/** Editor State: Instance of <a href="https://draftjs.org/docs/api-reference-editor-state">draft-js editor state</a> */
value: PropTypes.instanceOf(EditorState),
initialValue: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
mentions: PropTypes.arrayOf(
PropTypes.shape({
Expand All @@ -220,9 +232,10 @@ RichTextEditor.defaultProps = {

RichTextEditor.createEmpty = EditorState.createEmpty;
RichTextEditor.createWithText = createEditorStateWithText;
RichTextEditor.stateToHTML = (input) => stateToHTML(input.getCurrentContent());
RichTextEditor.stateFromHTML = (input) => EditorState.createWithContent(stateFromHTML(input));
RichTextEditor.stateToHTML = editorStateToHTML;
RichTextEditor.stateFromHTML = editorStateFromHTML;
RichTextEditor.stateToPlainText = (input) => input.getCurrentContent().getPlainText();
RichTextEditor.stateToEntityList = (input) => convertToRaw(input.getCurrentContent()).entityMap;
RichTextEditor.plainTextFromHTML = (input) => RichTextEditor.stateToPlainText(editorStateFromHTML(input));

export default RichTextEditor;
63 changes: 38 additions & 25 deletions src/components/RichTextEditor/index.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,15 @@ describe('<RichTextEditor />', () => {

it('should warn when value is passed and onChange is not', () => {
console.warn = jest.fn();
render(<RichTextEditor value={RichTextEditor.createEmpty()} />);
const newState = '123';
render(<RichTextEditor value={newState} />);
expect(console.warn).toHaveBeenCalledWith(
'Failed prop type: You have provided a `value` prop to RichTextEditor component without an `onChange` handler. This will render a read-only field.'
);
});

it('should set initial state correctly', () => {
const { container } = render(<RichTextEditor initialValue={RichTextEditor.stateFromHTML('<b>test</b>')} />);
const { container } = render(<RichTextEditor initialValue="<b>test</b>" />);
expect(getByClass(container, 'DraftEditor-root')).toHaveTextContent('test');
});

Expand All @@ -76,78 +77,90 @@ describe('<RichTextEditor />', () => {

it('should pass state if onChange is supplied', () => {
const onChange = jest.fn();
const { container } = render(<RichTextEditor value={RichTextEditor.createEmpty()} onChange={onChange} />);
expect(onChange).toHaveBeenCalledTimes(1);
const { container } = render(<RichTextEditor value={''} onChange={onChange} />);
expect(onChange).toHaveBeenCalledTimes(0);
const editorNode = container.querySelector('.public-DraftEditor-content');
const eventProperties = createPasteEvent('<strong>123</strong>');
const pasteEvent = createEvent.paste(editorNode, eventProperties);

fireEvent(editorNode, pasteEvent);
expect(onChange).toHaveBeenCalledTimes(2);
expect(RichTextEditor.stateToHTML(onChange.mock.calls[1][0])).toEqual('<p><strong>123</strong></p>');
expect(RichTextEditor.stateToPlainText(onChange.mock.calls[1][0])).toEqual('123');
expect(RichTextEditor.stateToEntityList(onChange.mock.calls[1][0])).toEqual({});
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange.mock.calls[0][0]).toEqual('<p><strong>123</strong></p>');
expect(RichTextEditor.stateToPlainText(RichTextEditor.stateFromHTML(onChange.mock.calls[0][0]))).toEqual('123');
expect(RichTextEditor.plainTextFromHTML(onChange.mock.calls[0][0])).toEqual('123');

expect(RichTextEditor.stateToEntityList(RichTextEditor.stateFromHTML(onChange.mock.calls[0][0]))).toEqual({});
});

it('should correctly handle key commands', () => {
jest.spyOn(RichUtils, 'handleKeyCommand');
const onChange = jest.fn();
const newState = RichTextEditor.stateFromHTML('123');
const newState = '123';
const { container } = render(<RichTextEditor initialValue={newState} onChange={onChange} />);
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledTimes(0);

const editorNode = container.querySelector('.public-DraftEditor-content');
fireEvent.focus(editorNode);
expect(onChange).toHaveBeenCalledTimes(2);
expect(RichTextEditor.stateToHTML(onChange.mock.calls[1][0])).toEqual('<p>123</p>');
expect(onChange).toHaveBeenCalledTimes(0);

fireEvent.keyDown(editorNode, { key: 'Backspace', keyCode: 8, which: 8 });
expect(onChange).toHaveBeenCalledTimes(3);
expect(RichTextEditor.stateToHTML(onChange.mock.calls[2][0])).toEqual('<p>12</p>');
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange.mock.calls[0][0]).toEqual('<p>12</p>');
expect(RichUtils.handleKeyCommand).toHaveBeenCalledTimes(1);
expect(RichUtils.handleKeyCommand.mock.calls[0][1]).toEqual('backspace');
});

it('should correctly handle the new state of key commands', () => {
jest.spyOn(RichUtils, 'handleKeyCommand');
const onChange = jest.fn();
const newState = RichTextEditor.stateFromHTML('123');
const newState = '123';
const { container } = render(<RichTextEditor initialValue={newState} onChange={onChange} />);

const editorNode = container.querySelector('.public-DraftEditor-content');
fireEvent.focus(editorNode);

fireEvent.keyDown(editorNode, { key: 'b', keyCode: 66, which: 66, ctrlKey: true });
expect(onChange).toHaveBeenCalledTimes(3);

expect(onChange).toHaveBeenCalledTimes(0);
expect(RichUtils.handleKeyCommand).toHaveBeenCalledTimes(1);
expect(RichUtils.handleKeyCommand.mock.calls[0][1]).toEqual('bold');
});

it('should toggle italics', () => {
it('should toggle italics', async () => {
const onChange = jest.fn();
const newState = RichTextEditor.stateFromHTML('123');
const { queryAllByTestId, getByTestId } = render(<RichTextEditor initialValue={newState} onChange={onChange} />);
const newState = '123';
const { queryAllByTestId, getByTestId, container } = render(
<RichTextEditor initialValue={newState} onChange={onChange} />
);

expect(queryAllByTestId('button-wrapper')).toHaveLength(5);
expect(queryAllByTestId('button-wrapper')[1]).toContainElement(getByTestId('italics'));

const editorNode = container.querySelector('.public-DraftEditor-content');
fireEvent.focus(editorNode);
fireEvent.mouseDown(queryAllByTestId('button-wrapper')[1]);

expect(onChange).toHaveBeenCalledTimes(2);
expect(RichTextEditor.stateToHTML(onChange.mock.calls[1][0])).toEqual('<p>123</p>');
expect(onChange).toHaveBeenCalledTimes(0);
fireEvent.keyDown(editorNode, { key: 'i', keyCode: 73, which: 73, ctrlKey: true });
expect(RichUtils.handleKeyCommand).toHaveBeenCalledTimes(1);
expect(RichUtils.handleKeyCommand.mock.calls[0][1]).toEqual('italic');
});

it('should correctly generate unordered list', () => {
const onChange = jest.fn();
const newState = RichTextEditor.stateFromHTML('123');
const newState = '123';
const { queryAllByTestId, getByTestId } = render(<RichTextEditor initialValue={newState} onChange={onChange} />);

expect(queryAllByTestId('button-wrapper')).toHaveLength(5);
expect(queryAllByTestId('button-wrapper')[4]).toContainElement(getByTestId('number'));

fireEvent.mouseDown(queryAllByTestId('button-wrapper')[4]);

expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange.mock.calls[0][0]).toEqual(
`<ol>
<li>123</li>
</ol>`
);
});

describe('mention feature', () => {
Expand Down Expand Up @@ -187,7 +200,7 @@ describe('<RichTextEditor />', () => {
},
];
const onChange = jest.fn();
const newState = RichTextEditor.stateFromHTML('@');
const newState = '@';
const { queryByTestId, queryAllByTestId, container } = render(
<RichTextEditor initialValue={newState} mentions={contacts} onChange={onChange} />
);
Expand Down
Loading

0 comments on commit 2411c4d

Please sign in to comment.