diff --git a/scripts/generate-types/typesPostFixes.js b/scripts/generate-types/typesPostFixes.js index f3dbafa0d..29b485ae0 100644 --- a/scripts/generate-types/typesPostFixes.js +++ b/scripts/generate-types/typesPostFixes.js @@ -178,6 +178,7 @@ exports.replacements = (componentName) => ({ stateFromHTML: (input: string) => EditorState; stateToPlainText: (input: ContentState) => string; stateToEntityList: (input: ContentState) => RawDraftContentState['entityMap']; + plainTextFromHTML: (input: string) => string; };`, ], ], diff --git a/src/components/RichTextEditor/index.d.ts b/src/components/RichTextEditor/index.d.ts index 0ed5315e8..0bc465491 100644 --- a/src/components/RichTextEditor/index.d.ts +++ b/src/components/RichTextEditor/index.d.ts @@ -11,14 +11,8 @@ export interface RichTextEditorMentions { export interface RichTextEditorProps { className?: string; placeholder?: string; - /** - * Editor State - */ - initialValue?: EditorState; - /** - * Editor State: Instance of draft-js editor state - */ - value?: EditorState; + initialValue?: string; + value?: string; onChange?: (...args: any[]) => any; mentions?: RichTextEditorMentions[]; onFileSelect?: (...args: any[]) => any; @@ -33,6 +27,7 @@ declare const RichTextEditor: React.FC & { stateFromHTML: (input: string) => EditorState; stateToPlainText: (input: ContentState) => string; stateToEntityList: (input: ContentState) => RawDraftContentState['entityMap']; + plainTextFromHTML: (input: string) => string; }; export default RichTextEditor; diff --git a/src/components/RichTextEditor/index.jsx b/src/components/RichTextEditor/index.jsx index 03f9e3ae1..cf1106189 100644 --- a/src/components/RichTextEditor/index.jsx +++ b/src/components/RichTextEditor/index.jsx @@ -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 === '


'; + +const getInitialEditorState = (value, initialValue) => { + if (value) { + return editorStateFromHTML(value); + } + if (initialValue) { + return editorStateFromHTML(initialValue); + } + return EditorState.createEmpty(); +}; + const RichTextEditor = ({ className, value, @@ -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({}); @@ -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(); @@ -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; @@ -139,7 +153,7 @@ const RichTextEditor = ({
- - + +
{!_.isEmpty(mentions) && ( @@ -196,10 +210,8 @@ const RichTextEditor = ({ RichTextEditor.propTypes = { className: PropTypes.string, placeholder: PropTypes.string, - /** Editor State */ - initialValue: PropTypes.instanceOf(EditorState), - /** Editor State: Instance of draft-js editor state */ - value: PropTypes.instanceOf(EditorState), + initialValue: PropTypes.string, + value: PropTypes.string, onChange: PropTypes.func, mentions: PropTypes.arrayOf( PropTypes.shape({ @@ -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; diff --git a/src/components/RichTextEditor/index.spec.jsx b/src/components/RichTextEditor/index.spec.jsx index d99772e08..1904dd02d 100644 --- a/src/components/RichTextEditor/index.spec.jsx +++ b/src/components/RichTextEditor/index.spec.jsx @@ -53,14 +53,15 @@ describe('', () => { it('should warn when value is passed and onChange is not', () => { console.warn = jest.fn(); - render(); + const newState = '123'; + render(); 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(test')} />); + const { container } = render(); expect(getByClass(container, 'DraftEditor-root')).toHaveTextContent('test'); }); @@ -76,34 +77,35 @@ describe('', () => { it('should pass state if onChange is supplied', () => { const onChange = jest.fn(); - const { container } = render(); - expect(onChange).toHaveBeenCalledTimes(1); + const { container } = render(); + expect(onChange).toHaveBeenCalledTimes(0); const editorNode = container.querySelector('.public-DraftEditor-content'); const eventProperties = createPasteEvent('123'); const pasteEvent = createEvent.paste(editorNode, eventProperties); fireEvent(editorNode, pasteEvent); - expect(onChange).toHaveBeenCalledTimes(2); - expect(RichTextEditor.stateToHTML(onChange.mock.calls[1][0])).toEqual('

123

'); - 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('

123

'); + 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(); - 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('

123

'); + 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('

12

'); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0]).toEqual('

12

'); expect(RichUtils.handleKeyCommand).toHaveBeenCalledTimes(1); expect(RichUtils.handleKeyCommand.mock.calls[0][1]).toEqual('backspace'); }); @@ -111,35 +113,41 @@ describe('', () => { 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(); 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(); + const newState = '123'; + const { queryAllByTestId, getByTestId, container } = render( + + ); 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('

123

'); + 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(); expect(queryAllByTestId('button-wrapper')).toHaveLength(5); @@ -147,7 +155,12 @@ describe('', () => { fireEvent.mouseDown(queryAllByTestId('button-wrapper')[4]); - expect(onChange).toHaveBeenCalledTimes(2); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange.mock.calls[0][0]).toEqual( + `
    +
  1. 123
  2. +
` + ); }); describe('mention feature', () => { @@ -187,7 +200,7 @@ describe('', () => { }, ]; const onChange = jest.fn(); - const newState = RichTextEditor.stateFromHTML('@'); + const newState = '@'; const { queryByTestId, queryAllByTestId, container } = render( ); diff --git a/www/examples/RichTextEditor.mdx b/www/examples/RichTextEditor.mdx index 65d92c280..0a563bd05 100644 --- a/www/examples/RichTextEditor.mdx +++ b/www/examples/RichTextEditor.mdx @@ -36,11 +36,15 @@ You have flexibility to create an controlled or uncontrolled editor based on you ```jsx live=true const Example = () => { const [state, setState] = React.useState(' '); + const [changeCount, setChangeCount] = React.useState(0); return ( <>
setState(RichTextEditor.stateToHTML(output))} + onChange={(output) => { + setState(output); + setChangeCount(changeCount + 1); + }} placeholder="Custom placeholder" />
@@ -49,6 +53,12 @@ const Example = () => {

HTML Output:

{state}
+ +
+

+ Change Time Count: {changeCount} +

+
); }; @@ -61,19 +71,31 @@ render(Example); ```jsx live=true const Example = () => { const [state, setState] = React.useState( - RichTextEditor.stateFromHTML( - `

test

asdasdasdasd

  • test
  1. test
  2. 123
` - ) + `

test

asdasdasdasd

  • test
  1. test
  2. 123
` ); + const [changeCount, setChangeCount] = React.useState(0); + return ( <>
- + { + setState(output); + setChangeCount(changeCount + 1); + }} + />

HTML Output:

- {RichTextEditor.stateToHTML(state)} + {state} +
+ +
+

+ Change Time Count: {changeCount} +

); @@ -107,35 +129,31 @@ const Example = () => { }, ]; - const [state, setState] = React.useState(RichTextEditor.stateFromHTML(' ')); + const [state, setState] = React.useState(''); + const [changeCount, setChangeCount] = React.useState(0); return ( <>
- + { + setState(output); + setChangeCount(changeCount + 1); + }} + mentions={contacts} + />

Contacts List:

- <> - {_(RichTextEditor.stateToEntityList(state)) - .filter({ type: 'mention' }) - .map( - ( - { - data: { - mention: { name, title }, - }, - }, - index - ) => ( -
- {index + 1}: {name} {title ? `, ${title}` : ''} -
- ) - ) - .value()} - + {state} +
+ +
+

+ Change Time Count: {changeCount} +

); @@ -148,7 +166,8 @@ render(Example); ```jsx live=true const Example = () => { - const [state, setState] = React.useState(RichTextEditor.stateFromHTML(' ')); + const [state, setState] = React.useState(''); + const [changeCount, setChangeCount] = React.useState(0); const onFileSelect = () => '../assets/tile/adslot-logo-1.png'; @@ -157,12 +176,21 @@ const Example = () => { return ( <>
- + { + setState(output); + setChangeCount(changeCount + 1); + }} + onFileSelect={onFileSelect} + onFileRemove={onFileRemove} + onHTMLChange={() => setChangeCount(changeCount + 1)} + />

PlainText Output:

- {RichTextEditor.stateToPlainText(state)} + {state}
);