From 0a6ea32e73d97f66e926ff2b5e6ec812ae97859e Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Sat, 10 Aug 2019 11:15:51 +0100 Subject: [PATCH 01/16] Remove dead code in onSave test case --- lib/components/DraftailEditor.test.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/components/DraftailEditor.test.js b/lib/components/DraftailEditor.test.js index 5f494dce..92070600 100644 --- a/lib/components/DraftailEditor.test.js +++ b/lib/components/DraftailEditor.test.js @@ -284,10 +284,6 @@ describe("DraftailEditor", () => { wrapper.instance().saveState(); expect(onSave).toHaveBeenCalled(); - - shallowNoLifecycle() - .instance() - .saveState(); }); it("#stateSaveInterval", () => { From 5de0849c0329152c87ddc4cd3e090a089cd891db Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Sat, 10 Aug 2019 13:09:40 +0100 Subject: [PATCH 02/16] Add editorState and onChange props to make the editor controlled --- lib/components/DraftailEditor.js | 105 +++++++++++++++---------- lib/components/DraftailEditor.test.js | 106 +++++++++++++++++++------- 2 files changed, 142 insertions(+), 69 deletions(-) diff --git a/lib/components/DraftailEditor.js b/lib/components/DraftailEditor.js index 4acf2c25..7de1c78d 100644 --- a/lib/components/DraftailEditor.js +++ b/lib/components/DraftailEditor.js @@ -45,8 +45,12 @@ type ControlProp = {| |}; type Props = {| + // Uncontrolled component props. rawContentState: ?RawDraftContentState, onSave: ?(content: null | RawDraftContentState) => void, + // Controlled component props. + editorState: ?EditorState, + onChange: ?(editorState: EditorState) => void, onFocus: ?() => void, onBlur: ?() => void, placeholder: ?string, @@ -110,7 +114,7 @@ type Props = {| bottomToolbar: ?ComponentType, // Max level of nesting for list items. 0 = no nesting. Maximum = 10. maxListNesting: number, - // Frequency at which to call the save callback (ms). + // Frequency at which to call the onSave callback (ms). stateSaveInterval: number, |}; @@ -119,6 +123,10 @@ const defaultProps = { rawContentState: null, // Called when changes occured. Use this to persist editor content. onSave: null, + // Content of the editor, when using the editor as a controlled component. Incompatible with `rawContentState`. + editorState: null, + // Called whenever the editor state is updated. Use this to store the content of a controlled editor. + onChange: null, // Called when the editor receives focus. onFocus: null, // Called when the editor loses focus. @@ -181,7 +189,8 @@ const defaultProps = { }; type State = {| - editorState: EditorState, + // editorState is only part of the local state if the editor is uncontrolled. + editorState?: EditorState, hasFocus: boolean, readOnlyState: boolean, sourceOptions: ?{ @@ -207,13 +216,13 @@ class DraftailEditor extends Component { /* :: updateTimeout: ?number; */ /* :: lockEditor: () => void; */ /* :: unlockEditor: () => void; */ + /* :: getEditorState: () => EditorState; */ constructor(props: Props) { super(props); this.onChange = this.onChange.bind(this); this.saveState = this.saveState.bind(this); - this.getEditorState = this.getEditorState.bind(this); this.toggleSource = this.toggleSource.bind(this); this.toggleEditor = this.toggleEditor.bind(this); @@ -247,14 +256,29 @@ class DraftailEditor extends Component { this.renderSource = this.renderSource.bind(this); - const { rawContentState } = props; + const { editorState: editorStateProp, rawContentState } = props; this.state = { - editorState: conversion.createEditorState(rawContentState), readOnlyState: false, hasFocus: false, sourceOptions: null, }; + + // If editorState is not used as a prop, create it in local state from rawContentState. + if (editorStateProp !== null) { + this.getEditorState = (): EditorState => { + const { editorState } = this.props; + // $FlowFixMe If the prop is set at initialisation, assume it is going to be set later too. + return editorState; + }; + } else { + this.state.editorState = conversion.createEditorState(rawContentState); + this.getEditorState = (): EditorState => { + const { editorState } = this.state; + // $FlowFixMe If editorState is stored locally just above, we can assume it is accessible there. + return editorState; + }; + } } componentDidMount() { @@ -294,7 +318,7 @@ class DraftailEditor extends Component { /* :: onTab: (event: SyntheticKeyboardEvent<>) => true; */ onTab(event: SyntheticKeyboardEvent<>) { const { maxListNesting } = this.props; - const { editorState } = this.state; + const editorState = this.getEditorState(); const newState = RichUtils.onTab(event, editorState, maxListNesting); this.onChange(newState); @@ -311,8 +335,9 @@ class DraftailEditor extends Component { blockTypes, inlineStyles, entityTypes, + onChange, } = this.props; - const { editorState } = this.state; + const editorState = this.getEditorState(); const shouldFilterPaste = nextState.getCurrentContent() !== editorState.getCurrentContent() && nextState.getLastChangeType() === "insert-fragment"; @@ -332,24 +357,28 @@ class DraftailEditor extends Component { ); } - this.setState( - { - editorState: filteredState, - }, - () => { - window.clearTimeout(this.updateTimeout); - this.updateTimeout = window.setTimeout( - this.saveState, - stateSaveInterval, - ); - }, - ); + if (onChange) { + onChange(filteredState); + } else { + this.setState( + { + editorState: filteredState, + }, + () => { + window.clearTimeout(this.updateTimeout); + this.updateTimeout = window.setTimeout( + this.saveState, + stateSaveInterval, + ); + }, + ); + } } /* :: onEditEntity: (entityKey: string) => void; */ onEditEntity(entityKey: string) { const { entityTypes } = this.props; - const { editorState } = this.state; + const editorState = this.getEditorState(); const content = editorState.getCurrentContent(); const entity = content.getEntity(entityKey); const entityType = entityTypes.find((t) => t.type === entity.type); @@ -374,7 +403,7 @@ class DraftailEditor extends Component { /* :: onRemoveEntity: (entityKey: string, blockKey: string) => void; */ onRemoveEntity(entityKey: string, blockKey: string) { const { entityTypes } = this.props; - const { editorState } = this.state; + const editorState = this.getEditorState(); const content = editorState.getCurrentContent(); const entity = content.getEntity(entityKey); const entityType = entityTypes.find((t) => t.type === entity.type); @@ -397,7 +426,7 @@ class DraftailEditor extends Component { /* :: onUndoRedo: (type: string) => void; */ onUndoRedo(type: string) { - const { editorState } = this.state; + const editorState = this.getEditorState(); let newEditorState = editorState; if (type === UNDO_TYPE) { @@ -411,7 +440,7 @@ class DraftailEditor extends Component { /* :: onRequestSource: (entityType: string) => void; */ onRequestSource(entityType: string) { - const { editorState } = this.state; + const editorState = this.getEditorState(); const contentState = editorState.getCurrentContent(); const entityKey = DraftUtils.getSelectionEntity(editorState); @@ -452,16 +481,10 @@ class DraftailEditor extends Component { }); } - /* :: getEditorState: () => EditorState; */ - getEditorState() { - const { editorState } = this.state; - return editorState; - } - /* :: saveState: () => void; */ saveState() { const { onSave } = this.props; - const { editorState } = this.state; + const editorState = this.getEditorState(); if (onSave) { onSave(conversion.serialiseEditorState(editorState)); @@ -493,7 +516,7 @@ class DraftailEditor extends Component { /* :: handleReturn: (e: SyntheticKeyboardEvent<>) => 'not-handled' | 'handled'; */ handleReturn(e: SyntheticKeyboardEvent<>) { const { enableLineBreak, inlineStyles } = this.props; - const { editorState } = this.state; + const editorState = this.getEditorState(); let ret = NOT_HANDLED; // alt + enter opens links and other entities with a `url` property. @@ -560,7 +583,7 @@ class DraftailEditor extends Component { /* :: handleKeyCommand: (command: DraftEditorCommand) => 'handled' | 'not-handled'; */ handleKeyCommand(command: DraftEditorCommand) { const { entityTypes, blockTypes, inlineStyles } = this.props; - const { editorState } = this.state; + const editorState = this.getEditorState(); if (entityTypes.some((t) => t.type === command)) { this.onRequestSource(command); @@ -604,7 +627,7 @@ class DraftailEditor extends Component { /* :: handleBeforeInput: (char: string) => 'handled' | 'not-handled'; */ handleBeforeInput(char: string) { const { blockTypes, inlineStyles, enableHorizontalRule } = this.props; - const { editorState } = this.state; + const editorState = this.getEditorState(); const selection = editorState.getSelection(); if (selection.isCollapsed()) { @@ -678,32 +701,32 @@ class DraftailEditor extends Component { /* :: toggleBlockType: (blockType: string) => void; */ toggleBlockType(blockType: string) { - const { editorState } = this.state; + const editorState = this.getEditorState(); this.onChange(RichUtils.toggleBlockType(editorState, blockType)); } /* :: toggleInlineStyle: (inlineStyle: string) => void; */ toggleInlineStyle(inlineStyle: string) { - const { editorState } = this.state; + const editorState = this.getEditorState(); this.onChange(RichUtils.toggleInlineStyle(editorState, inlineStyle)); } /* :: addHR: () => void; */ addHR() { - const { editorState } = this.state; + const editorState = this.getEditorState(); this.onChange(DraftUtils.addHorizontalRuleRemovingSelection(editorState)); } /* :: addBR: () => void; */ addBR() { - const { editorState } = this.state; + const editorState = this.getEditorState(); this.onChange(DraftUtils.addLineBreak(editorState)); } /* :: blockRenderer: (block: ContentBlock) => {}; */ blockRenderer(block: ContentBlock) { const { entityTypes } = this.props; - const { editorState } = this.state; + const editorState = this.getEditorState(); const contentState = editorState.getCurrentContent(); if (block.getType() !== BLOCK_TYPE.ATOMIC) { @@ -770,7 +793,8 @@ class DraftailEditor extends Component { /* :: renderSource: () => ?Node; */ renderSource() { - const { editorState, sourceOptions } = this.state; + const { sourceOptions } = this.state; + const editorState = this.getEditorState(); if (sourceOptions && sourceOptions.entityType) { const Source = sourceOptions.entityType.source; @@ -822,7 +846,8 @@ class DraftailEditor extends Component { topToolbar, bottomToolbar, } = this.props; - const { editorState, hasFocus, readOnlyState } = this.state; + const { hasFocus, readOnlyState } = this.state; + const editorState = this.getEditorState(); const isReadOnly = readOnlyState || readOnly; const hidePlaceholder = DraftUtils.shouldHidePlaceholder(editorState); const entityDecorators = entityTypes diff --git a/lib/components/DraftailEditor.test.js b/lib/components/DraftailEditor.test.js index 92070600..cb3aa2d3 100644 --- a/lib/components/DraftailEditor.test.js +++ b/lib/components/DraftailEditor.test.js @@ -27,6 +27,26 @@ describe("DraftailEditor", () => { expect(shallowNoLifecycle()).toMatchSnapshot(); }); + it("#rawContentState sets local state", () => { + const wrapper = shallowNoLifecycle( + , + ); + expect(wrapper.state("editorState")).toBeInstanceOf(EditorState); + }); + + it("#editorState is passed through", () => { + const editorState = EditorState.createEmpty(); + const wrapper = shallowNoLifecycle( + , + ); + expect(wrapper.find("PluginEditor").prop("editorState")).toBe(editorState); + }); + it("editorRef", () => { expect(mount().instance().editorRef).toBeDefined(); }); @@ -77,17 +97,7 @@ describe("DraftailEditor", () => { placeholder="Write here…" rawContentState={{ entityMap: {}, - blocks: [ - { - key: "b3kdk", - text: "test", - type: "header-two", - depth: 0, - inlineStyleRanges: [], - entityRanges: [], - data: {}, - }, - ], + blocks: [{ text: "test" }], }} />, ) @@ -277,13 +287,36 @@ describe("DraftailEditor", () => { ).toMatchSnapshot(); }); - it("#onSave", () => { - const onSave = jest.fn(); - const wrapper = shallowNoLifecycle(); + describe("#onSave", () => { + it("works", () => { + jest.useFakeTimers(); + + const onSave = jest.fn(); + const wrapper = shallowNoLifecycle( + , + ); + + wrapper.instance().onChange(EditorState.createEmpty()); + + jest.advanceTimersByTime(150); + + expect(onSave).toHaveBeenCalled(); + }); + + it("does not get called when onChange is used", () => { + jest.useFakeTimers(); - wrapper.instance().saveState(); + const onSave = jest.fn(); + const wrapper = shallowNoLifecycle( + {}} />, + ); + + wrapper.instance().onChange(EditorState.createEmpty()); - expect(onSave).toHaveBeenCalled(); + jest.advanceTimersByTime(1000); + + expect(onSave).not.toHaveBeenCalled(); + }); }); it("#stateSaveInterval", () => { @@ -390,27 +423,42 @@ describe("DraftailEditor", () => { const onSave = jest.fn(); const wrapper = shallowNoLifecycle(); - const contentState = convertFromRaw({ - entityMap: {}, - blocks: [{ text: "test" }], - }); + wrapper.instance().onChange(EditorState.createEmpty()); + jest.advanceTimersByTime(1000); + expect(onSave).toHaveBeenCalled(); + }); - const editorState = EditorState.push( - EditorState.createEmpty(), - contentState, - "insert-fragment", + it("calls onChange if used instead", () => { + jest.useFakeTimers(); + + const onSave = jest.fn(); + const onChange = jest.fn(); + const wrapper = shallowNoLifecycle( + , ); - wrapper.instance().onChange(editorState); + wrapper.instance().onChange(EditorState.createEmpty()); jest.advanceTimersByTime(1000); - expect(onSave).toHaveBeenCalled(); + expect(onSave).not.toHaveBeenCalled(); + expect(onChange).toHaveBeenCalled(); }); }); - it("getEditorState", () => { - const wrapper = shallowNoLifecycle(); + describe("getEditorState", () => { + it("works with uncontrolled editor", () => { + const wrapper = shallowNoLifecycle(); + + expect(wrapper.instance().getEditorState()).toBeInstanceOf(EditorState); + }); - expect(wrapper.instance().getEditorState()).toBeInstanceOf(EditorState); + it("works with controlled editor", () => { + const editorState = EditorState.createEmpty(); + const wrapper = shallowNoLifecycle( + , + ); + + expect(wrapper.instance().getEditorState()).toBe(editorState); + }); }); describe("handleReturn", () => { From f6ca6cd5a16f50dceb5e2c143abd70d14ff58562 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Sun, 11 Aug 2019 11:16:48 +0100 Subject: [PATCH 03/16] Add demo of controlled editor --- examples/docs.story.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/examples/docs.story.js b/examples/docs.story.js index c467ce7d..4050db2b 100644 --- a/examples/docs.story.js +++ b/examples/docs.story.js @@ -1,8 +1,8 @@ import { storiesOf } from "@storybook/react"; -import React from "react"; +import React, { useState } from "react"; import { injectIntl } from "react-intl"; import { convertFromHTML, convertToHTML } from "draft-convert"; -import { convertToRaw, convertFromRaw } from "draft-js"; +import { EditorState, convertToRaw, convertFromRaw } from "draft-js"; import { Formik } from "formik"; import { DraftailEditor, INLINE_STYLE, ENTITY_TYPE, BLOCK_TYPE } from "../lib"; @@ -22,6 +22,8 @@ import PrismDecorator from "./components/PrismDecorator"; import ReadingTime from "./components/ReadingTime"; storiesOf("Docs", module) + // Add a decorator rendering story as a component for hooks support. + .addDecorator((Story) => ) .add("Built-in formats", () => ( )} - )); + )) + .add("Controlled component", () => { + const [editorState, setEditorState] = useState(EditorState.createEmpty()); + return ( + + ); + }); From d272345f8e2850e77e89f30647ced2cc5257d072 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Mon, 12 Aug 2019 00:05:20 +0100 Subject: [PATCH 04/16] Make EditorWrapper compatible with a controlled editor --- examples/components/EditorWrapper.js | 61 ++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/examples/components/EditorWrapper.js b/examples/components/EditorWrapper.js index aa5dfb2f..0ca4ca56 100644 --- a/examples/components/EditorWrapper.js +++ b/examples/components/EditorWrapper.js @@ -1,5 +1,6 @@ // @flow import React, { Component } from "react"; +import { EditorState, convertToRaw } from "draft-js"; import type { RawDraftContentState } from "draft-js/lib/RawDraftContentState"; import { DraftailEditor } from "../../lib"; @@ -14,7 +15,10 @@ const DRAFTAIL_VERSION = type Props = {| id: string, - onSave: ?(?RawDraftContentState) => void, + rawContentState: ?RawDraftContentState, + editorState: ?EditorState, + onSave: ?(content: null | RawDraftContentState) => void, + onChange: ?(editorState: EditorState) => void, |}; type State = {| @@ -32,10 +36,11 @@ class EditorWrapper extends Component { }; this.onSave = this.onSave.bind(this); + this.onChange = this.onChange.bind(this); } - /* :: onSave: (content: ?RawDraftContentState) => void; */ - onSave(content: ?RawDraftContentState) { + /* :: onSave: (content: null | RawDraftContentState) => void; */ + onSave(content: null | RawDraftContentState) { const { id, onSave } = this.props; this.setState(({ saveCount }) => ({ content, saveCount: saveCount + 1 })); @@ -47,19 +52,48 @@ class EditorWrapper extends Component { } } + /* :: onChange: (nextState: EditorState) => void; */ + onChange(nextState: EditorState) { + const { id, onChange } = this.props; + const content = convertToRaw(nextState.getCurrentContent()); + + this.setState(({ saveCount }) => ({ content, saveCount: saveCount + 1 })); + + sessionStorage.setItem(`${id}:content`, JSON.stringify(content)); + + if (onChange) { + onChange(nextState); + } + } + render() { - const { id, onSave, ...editorProps } = this.props; + const { + id, + editorState, + rawContentState, + onSave, + onChange, + ...editorProps + } = this.props; const { content, saveCount } = this.state; - const storedContent = sessionStorage.getItem(`${id}:content`) || null; - const initialContent = storedContent ? JSON.parse(storedContent) : null; + const dataProps = {}; + let initialContent; + + if (editorState) { + dataProps.editorState = editorState; + dataProps.onChange = this.onChange; + } else { + const storedContent = sessionStorage.getItem(`${id}:content`) || "null"; + initialContent = + rawContentState || storedContent ? JSON.parse(storedContent) : null; + dataProps.rawContentState = initialContent; + dataProps.onSave = this.onSave; + } + return (
- +
@@ -73,7 +107,10 @@ class EditorWrapper extends Component { {`Saves: ${saveCount}`} - + {/* Running multiple editors with the same base state is a source of issues. */} + {editorState ? null : ( + + )} From ea4b51cdcabbed094a3bc8c10c6f72b8353f0a02 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Mon, 12 Aug 2019 00:10:20 +0100 Subject: [PATCH 05/16] Split editorState getters into separate functions to avoid typing issues --- lib/components/DraftailEditor.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/components/DraftailEditor.js b/lib/components/DraftailEditor.js index 7de1c78d..92870c57 100644 --- a/lib/components/DraftailEditor.js +++ b/lib/components/DraftailEditor.js @@ -266,18 +266,10 @@ class DraftailEditor extends Component { // If editorState is not used as a prop, create it in local state from rawContentState. if (editorStateProp !== null) { - this.getEditorState = (): EditorState => { - const { editorState } = this.props; - // $FlowFixMe If the prop is set at initialisation, assume it is going to be set later too. - return editorState; - }; + this.getEditorState = this.getEditorStateProp.bind(this); } else { this.state.editorState = conversion.createEditorState(rawContentState); - this.getEditorState = (): EditorState => { - const { editorState } = this.state; - // $FlowFixMe If editorState is stored locally just above, we can assume it is accessible there. - return editorState; - }; + this.getEditorState = this.getEditorStateState.bind(this); } } @@ -481,6 +473,18 @@ class DraftailEditor extends Component { }); } + /* :: getEditorStateProp: () => EditorState; */ + getEditorStateProp() { + const { editorState } = this.props; + return editorState; + } + + /* :: getEditorStateState: () => EditorState; */ + getEditorStateState() { + const { editorState } = this.state; + return editorState; + } + /* :: saveState: () => void; */ saveState() { const { onSave } = this.props; From 6e99f70167f9590caed1d79e078886dc3062f33a Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Tue, 13 Aug 2019 13:43:50 +0100 Subject: [PATCH 06/16] Update to latest version of draftjs-conductor --- package-lock.json | 88 +++++++++++++++++++++++++++++++++-------------- package.json | 2 +- 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 26f5d847..3445b56f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10123,9 +10123,9 @@ } }, "draftjs-conductor": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/draftjs-conductor/-/draftjs-conductor-0.4.1.tgz", - "integrity": "sha512-5BcJLdYLNIA/TNp/9xwIeD1quWsWEoi0ZI81TiW3vLecBEQgWKVxuKZaLdaXHYpW4/kdQvAJz2KcOBTpJkYBxg==" + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/draftjs-conductor/-/draftjs-conductor-0.5.2.tgz", + "integrity": "sha512-g7j7Kjxr5wIAXnS6bD/yDf3gq7FpfiHmnp5RFgjw+54GGN6EIO1ABh/OCw9LAU/xTCJ9dHIGYLHStR0KWUic+Q==" }, "draftjs-filters": { "version": "2.2.3", @@ -12206,7 +12206,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -12227,12 +12228,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -12247,17 +12250,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -12374,7 +12380,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -12386,6 +12393,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -12400,6 +12408,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -12407,12 +12416,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -12431,6 +12442,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -12511,7 +12523,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -12523,6 +12536,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -12608,7 +12622,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -12644,6 +12659,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -12663,6 +12679,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -12706,12 +12723,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -23684,7 +23703,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -23705,12 +23725,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -23725,17 +23747,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -23852,7 +23877,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -23864,6 +23890,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -23878,6 +23905,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -23885,12 +23913,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -23909,6 +23939,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -23989,7 +24020,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -24001,6 +24033,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -24086,7 +24119,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -24122,6 +24156,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -24141,6 +24176,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -24184,12 +24220,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/package.json b/package.json index 20169750..036a0a05 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "dependencies": { "decorate-component-with-props": "^1.0.2", "draft-js-plugins-editor": "^2.1.1", - "draftjs-conductor": "^0.4.1", + "draftjs-conductor": "^0.5.2", "draftjs-filters": "^2.2.3" }, "devDependencies": { From d7ad3f96cb16a08a635cfed0263d459f9eaabdf1 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Tue, 13 Aug 2019 13:48:35 +0100 Subject: [PATCH 07/16] Remove unneeded link suffix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1329fc4c..318603cb 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Here are important features worth highlighting: ## Documentation -- [Getting started](https://www.draftail.org/docs/getting-started.html) +- [Getting started](https://www.draftail.org/docs/getting-started) - [API reference](https://www.draftail.org/docs/api) - [User guide](https://www.draftail.org/docs/user-guide) - [Getting started with extensions](https://www.draftail.org/docs/getting-started-with-extensions) From ed9e25b354cc46eeef0e24567832746a9a02ba28 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Tue, 13 Aug 2019 21:27:37 +0100 Subject: [PATCH 08/16] Replace internal conversion API with new methods from draftjs-conductor --- lib/api/conversion.js | 35 ----------------- lib/api/conversion.test.js | 64 -------------------------------- lib/components/DraftailEditor.js | 7 ++-- 3 files changed, 4 insertions(+), 102 deletions(-) delete mode 100644 lib/api/conversion.js delete mode 100644 lib/api/conversion.test.js diff --git a/lib/api/conversion.js b/lib/api/conversion.js deleted file mode 100644 index cbed74d0..00000000 --- a/lib/api/conversion.js +++ /dev/null @@ -1,35 +0,0 @@ -// @flow -import { EditorState, convertFromRaw, convertToRaw } from "draft-js"; -import type { RawDraftContentState } from "draft-js/lib/RawDraftContentState"; - -const EMPTY_CONTENT_STATE = null; - -export default { - createEditorState(rawContentState: ?RawDraftContentState) { - let editorState; - - if (rawContentState) { - const contentState = convertFromRaw(rawContentState); - editorState = EditorState.createWithContent(contentState); - } else { - editorState = EditorState.createEmpty(); - } - - return editorState; - }, - - serialiseEditorState(editorState: EditorState) { - const contentState = editorState.getCurrentContent(); - const rawContentState = convertToRaw(contentState); - - const isEmpty = rawContentState.blocks.every((block) => { - const isEmptyBlock = - block.text.trim().length === 0 && - (!block.entityRanges || block.entityRanges.length === 0) && - (!block.inlineStyleRanges || block.inlineStyleRanges.length === 0); - return isEmptyBlock; - }); - - return isEmpty ? EMPTY_CONTENT_STATE : rawContentState; - }, -}; diff --git a/lib/api/conversion.test.js b/lib/api/conversion.test.js deleted file mode 100644 index 7b93c347..00000000 --- a/lib/api/conversion.test.js +++ /dev/null @@ -1,64 +0,0 @@ -import { EditorState, convertFromRaw, convertToRaw } from "draft-js"; -import conversion from "./conversion"; - -const stubContent = { - entityMap: {}, - blocks: [ - { - key: "1dcqo", - text: "Hello, World!", - type: "unstyled", - depth: 0, - inlineStyleRanges: [], - entityRanges: [], - data: {}, - }, - { - key: "dmtba", - text: "This is a title", - type: "header-two", - depth: 0, - inlineStyleRanges: [], - entityRanges: [], - data: {}, - }, - ], -}; - -describe("conversion", () => { - describe("#createEditorState", () => { - it("creates state from real content", () => { - const state = conversion.createEditorState(stubContent); - const result = convertToRaw(state.getCurrentContent()); - expect(result.blocks.length).toEqual(2); - expect(result.blocks[0].text).toEqual("Hello, World!"); - }); - }); - - describe("#serialiseEditorState", () => { - it("keeps real content", () => { - const state = conversion.createEditorState(stubContent); - expect(conversion.serialiseEditorState(state)).toEqual(stubContent); - }); - - it("discards empty content", () => { - const state = conversion.createEditorState(null); - expect(conversion.serialiseEditorState(state)).toBeNull(); - }); - - it("discards content with only empty text", () => { - const editorState = EditorState.createWithContent( - convertFromRaw({ - entityMap: {}, - blocks: [ - { - key: "a", - text: "", - }, - ], - }), - ); - expect(conversion.serialiseEditorState(editorState)).toBeNull(); - }); - }); -}); diff --git a/lib/components/DraftailEditor.js b/lib/components/DraftailEditor.js index 92870c57..792243df 100644 --- a/lib/components/DraftailEditor.js +++ b/lib/components/DraftailEditor.js @@ -12,6 +12,8 @@ import { ListNestingStyles, registerCopySource, handleDraftEditorPastedText, + createEditorStateFromRaw, + serialiseEditorStateToRaw, } from "draftjs-conductor"; import decorateComponentWithProps from "decorate-component-with-props"; @@ -27,7 +29,6 @@ import { import DraftUtils from "../api/DraftUtils"; import behavior from "../api/behavior"; -import conversion from "../api/conversion"; import Toolbar from "./Toolbar"; import type { ToolbarProps } from "./Toolbar"; @@ -268,7 +269,7 @@ class DraftailEditor extends Component { if (editorStateProp !== null) { this.getEditorState = this.getEditorStateProp.bind(this); } else { - this.state.editorState = conversion.createEditorState(rawContentState); + this.state.editorState = createEditorStateFromRaw(rawContentState); this.getEditorState = this.getEditorStateState.bind(this); } } @@ -491,7 +492,7 @@ class DraftailEditor extends Component { const editorState = this.getEditorState(); if (onSave) { - onSave(conversion.serialiseEditorState(editorState)); + onSave(serialiseEditorStateToRaw(editorState)); } } From d8a91b662487e7fbde8a0dcb548824d9ed8b9490 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 14 Aug 2019 16:31:06 +0100 Subject: [PATCH 09/16] =?UTF-8?q?Expose=20draftjs-conductor=20data=20conve?= =?UTF-8?q?rsion=20APIs=20through=20Draftail=E2=80=99s=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/index.js b/lib/index.js index 14844b92..2a9670c7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,3 +8,8 @@ export { default as Icon } from "./components/Icon"; export { default as ToolbarButton } from "./components/ToolbarButton"; export { default as DraftUtils } from "./api/DraftUtils"; export { BLOCK_TYPE, ENTITY_TYPE, INLINE_STYLE } from "./api/constants"; +// Expose methods from draftjs-conductor directly for users of Draftail. +export { + createEditorStateFromRaw, + serialiseEditorStateToRaw, +} from "draftjs-conductor"; From c2c072e5308e8fb1c6b3926a8b872a371060510c Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 14 Aug 2019 16:32:16 +0100 Subject: [PATCH 10/16] Use data conversion APIs in EditorWrapper --- examples/components/EditorWrapper.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/components/EditorWrapper.js b/examples/components/EditorWrapper.js index 0ca4ca56..26a0be76 100644 --- a/examples/components/EditorWrapper.js +++ b/examples/components/EditorWrapper.js @@ -1,9 +1,9 @@ // @flow import React, { Component } from "react"; -import { EditorState, convertToRaw } from "draft-js"; +import { EditorState } from "draft-js"; import type { RawDraftContentState } from "draft-js/lib/RawDraftContentState"; -import { DraftailEditor } from "../../lib"; +import { DraftailEditor, serialiseEditorStateToRaw } from "../../lib"; import SentryBoundary from "./SentryBoundary"; import Highlight from "./Highlight"; @@ -55,7 +55,7 @@ class EditorWrapper extends Component { /* :: onChange: (nextState: EditorState) => void; */ onChange(nextState: EditorState) { const { id, onChange } = this.props; - const content = convertToRaw(nextState.getCurrentContent()); + const content = serialiseEditorStateToRaw(nextState); this.setState(({ saveCount }) => ({ content, saveCount: saveCount + 1 })); From b84375a3e7b62f563d8be5d4b44178d2f140d84f Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 14 Aug 2019 16:32:32 +0100 Subject: [PATCH 11/16] Remove dead code from getComponentWrapper --- .../getComponentWrapper.test.js.snap | 7 ------- lib/utils/getComponentWrapper.js | 17 ----------------- lib/utils/getComponentWrapper.test.js | 15 --------------- 3 files changed, 39 deletions(-) delete mode 100644 lib/utils/__snapshots__/getComponentWrapper.test.js.snap delete mode 100644 lib/utils/getComponentWrapper.js delete mode 100644 lib/utils/getComponentWrapper.test.js diff --git a/lib/utils/__snapshots__/getComponentWrapper.test.js.snap b/lib/utils/__snapshots__/getComponentWrapper.test.js.snap deleted file mode 100644 index 2c805447..00000000 --- a/lib/utils/__snapshots__/getComponentWrapper.test.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`getComponentWrapper works 1`] = ` - -`; diff --git a/lib/utils/getComponentWrapper.js b/lib/utils/getComponentWrapper.js deleted file mode 100644 index 6651981a..00000000 --- a/lib/utils/getComponentWrapper.js +++ /dev/null @@ -1,17 +0,0 @@ -// @flow -import React from "react"; -import type { ComponentType } from "react"; - -/** - * Wraps a component to provide it with additional props based on context. - */ -const getComponentWrapper = (Wrapped: ComponentType<{}>, wrapperProps: {}) => { - const Wrapper = (props: {}) => ( - // flowlint inexact-spread:off - - ); - - return Wrapper; -}; - -export default getComponentWrapper; diff --git a/lib/utils/getComponentWrapper.test.js b/lib/utils/getComponentWrapper.test.js deleted file mode 100644 index 83cabb88..00000000 --- a/lib/utils/getComponentWrapper.test.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; -import { shallow } from "enzyme"; - -import getComponentWrapper from "./getComponentWrapper"; - -describe("getComponentWrapper", () => { - it("works", () => { - const Wrapped = () =>
; - const Wrapper = getComponentWrapper(Wrapped, { - test: true, - }); - - expect(shallow()).toMatchSnapshot(); - }); -}); From a031487ef04e87208bb39188248af8c7119e0525 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 14 Aug 2019 18:44:35 +0100 Subject: [PATCH 12/16] Convert otherwise-unused import into Flow-only import --- examples/components/EditorWrapper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/components/EditorWrapper.js b/examples/components/EditorWrapper.js index 26a0be76..dedf0bf7 100644 --- a/examples/components/EditorWrapper.js +++ b/examples/components/EditorWrapper.js @@ -1,6 +1,6 @@ // @flow import React, { Component } from "react"; -import { EditorState } from "draft-js"; +/* :: import { EditorState } from "draft-js"; */ import type { RawDraftContentState } from "draft-js/lib/RawDraftContentState"; import { DraftailEditor, serialiseEditorStateToRaw } from "../../lib"; From 222cb055b77551460d38a089c675a5a70ad15037 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 14 Aug 2019 22:55:35 +0100 Subject: [PATCH 13/16] Fix EditorWrapper not loading preset content --- examples/components/EditorWrapper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/components/EditorWrapper.js b/examples/components/EditorWrapper.js index dedf0bf7..11ae2266 100644 --- a/examples/components/EditorWrapper.js +++ b/examples/components/EditorWrapper.js @@ -85,7 +85,7 @@ class EditorWrapper extends Component { } else { const storedContent = sessionStorage.getItem(`${id}:content`) || "null"; initialContent = - rawContentState || storedContent ? JSON.parse(storedContent) : null; + rawContentState || (storedContent ? JSON.parse(storedContent) : null); dataProps.rawContentState = initialContent; dataProps.onSave = this.onSave; } From 562ea9251b75c14c83e00d198a67f6b6de4f4880 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 14 Aug 2019 22:56:58 +0100 Subject: [PATCH 14/16] Add CHANGELOG updates for controlled component feature --- CHANGELOG.md | 1 + lib/components/DraftailEditor.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8bc9fef..6bfdba39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Added - Add ability to disable the editor on demand with the [`readOnly`](https://www.draftail.org/docs/next/api#draftaileditor) prop, matching behavior of Draft.js. [#201](https://github.com/springload/draftail/issues/201), [#206](https://github.com/springload/draftail/pull/206), thanks to [@SpearThruster](https://github.com/SpearThruster). +- Add ability to use the editor as a controlled component, like vanilla Draft.js editors, with [`editorState` and `onChange`](https://www.draftail.org/docs/next/api#editorstate-and-onchange) props. Have a look at the [controlled component documentation](https://www.draftail.org/docs/next/controlled-component) for further details. [#180](https://github.com/springload/draftail/issues/180), [#207](https://github.com/springload/draftail/pull/207). ### Fixed diff --git a/lib/components/DraftailEditor.js b/lib/components/DraftailEditor.js index 792243df..78443a3f 100644 --- a/lib/components/DraftailEditor.js +++ b/lib/components/DraftailEditor.js @@ -124,9 +124,9 @@ const defaultProps = { rawContentState: null, // Called when changes occured. Use this to persist editor content. onSave: null, - // Content of the editor, when using the editor as a controlled component. Incompatible with `rawContentState`. + // Content of the editor, when using the editor as a controlled component. Incompatible with `rawContentState` and `onSave`. editorState: null, - // Called whenever the editor state is updated. Use this to store the content of a controlled editor. + // Called whenever the editor state is updated. Use this to manage the content of a controlled editor. Incompatible with `rawContentState` and `onSave`. onChange: null, // Called when the editor receives focus. onFocus: null, From ca292f792ec29781caadd52a5c5ae464d5629962 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 14 Aug 2019 23:07:11 +0100 Subject: [PATCH 15/16] Update draftjs-conductor to latest --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3445b56f..c1c93ba0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10123,9 +10123,9 @@ } }, "draftjs-conductor": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/draftjs-conductor/-/draftjs-conductor-0.5.2.tgz", - "integrity": "sha512-g7j7Kjxr5wIAXnS6bD/yDf3gq7FpfiHmnp5RFgjw+54GGN6EIO1ABh/OCw9LAU/xTCJ9dHIGYLHStR0KWUic+Q==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/draftjs-conductor/-/draftjs-conductor-1.0.0.tgz", + "integrity": "sha512-pz5MIpS2aH6fgo2jrHo0Rt+rxqRgqnbrxEaaTiFLW4Dg/0U0eZhyxZPt6pByoS20Q7jCYOpolGpszKodNVi+TQ==" }, "draftjs-filters": { "version": "2.2.3", diff --git a/package.json b/package.json index 036a0a05..58a13be2 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "dependencies": { "decorate-component-with-props": "^1.0.2", "draft-js-plugins-editor": "^2.1.1", - "draftjs-conductor": "^0.5.2", + "draftjs-conductor": "^1.0.0", "draftjs-filters": "^2.2.3" }, "devDependencies": { From 5d66d08ab12d0be3a64ed826546e64d6a55cc532 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Wed, 14 Aug 2019 23:27:29 +0100 Subject: [PATCH 16/16] Cleanup unnecessary prop rename --- lib/components/DraftailEditor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/components/DraftailEditor.js b/lib/components/DraftailEditor.js index 78443a3f..63bea050 100644 --- a/lib/components/DraftailEditor.js +++ b/lib/components/DraftailEditor.js @@ -257,7 +257,7 @@ class DraftailEditor extends Component { this.renderSource = this.renderSource.bind(this); - const { editorState: editorStateProp, rawContentState } = props; + const { editorState, rawContentState } = props; this.state = { readOnlyState: false, @@ -265,10 +265,10 @@ class DraftailEditor extends Component { sourceOptions: null, }; - // If editorState is not used as a prop, create it in local state from rawContentState. - if (editorStateProp !== null) { + if (editorState !== null) { this.getEditorState = this.getEditorStateProp.bind(this); } else { + // If editorState is not used as a prop, create it in local state from rawContentState. this.state.editorState = createEditorStateFromRaw(rawContentState); this.getEditorState = this.getEditorStateState.bind(this); }