Skip to content

Commit

Permalink
feat(api): onDraftEditorCopy, onDraftEditorCut for [email protected] (#268)
Browse files Browse the repository at this point in the history
Those new methods can be used with the new `onCopy` and `onCut` props introduced in Draft.js 0.11.0,
to replace `registerCopySource` and remove the need for an editor ref. `registerCopySource` is
still available and works the same as before, but will eventually be removed once this package
drops support for Draft.js 0.10.
  • Loading branch information
thibaudcolas authored Nov 19, 2020
1 parent 62a519c commit 05b31cb
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 55 deletions.
3 changes: 2 additions & 1 deletion .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
.*/node_modules/draft-js/lib/DraftEditorLeaf.react.js.flow
.*/node_modules/draft-js/lib/DraftEditorDragHandler.js.flow
.*/node_modules/draft-js/lib/DraftEditor.react.js.flow
.*/node_modules/draft-js/lib/DraftEditor.react.js.flow
.*/node_modules/config-chain
.*/node_modules/fbjs/lib/emptyFunction.js.flow
.*/node_modules/fbjs/lib/editOnPaste.js.flow
suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe

[include]
Expand Down
67 changes: 62 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,15 @@ Relevant Draft.js issues:
- [Nested list styles above 4 levels are not retained when copy-pasting between Draft instances. – facebook/draft-js#1605 (comment)](https://github.com/facebook/draft-js/pull/1605#pullrequestreview-87340460)
- [Merged `<p>` tags on paste – facebook/draft-js#523 (comment)](https://github.com/facebook/draft-js/issues/523#issuecomment-371098488)

All of those problems can be fixed with this library, which overrides the `copy` event to transfer more of the editor’s content, and introduces a function to use with the Draft.js [`handlePastedText`](https://draftjs.org/docs/api-reference-editor#handlepastedtext) to retrieve the pasted content.
All of those problems can be fixed with this library, which overrides the `copy` and `cut` events to transfer more of the editor’s content, and introduces a function to use with the Draft.js [`handlePastedText`](https://draftjs.org/docs/api-reference-editor#handlepastedtext) to retrieve the pasted content.

**This will paste all copied content, even if the target editor might not support it.** To ensure only supported content is retained, use filters like [draftjs-filters](https://github.com/thibaudcolas/draftjs-filters).

Here’s how to use the copy override, and the paste handler:
Note: IE11 isn’t supported, as it doesn't support storing HTML in the clipboard, and we also use the [`Element.closest`](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest) API.

#### With draft.js 0.11 and above

Here’s how to use the copy/cut override, and the paste handler:

```js
import {
Expand Down Expand Up @@ -106,6 +110,41 @@ class MyEditor extends Component {
return false;
}

render() {
const { editorState } = this.state;

return (
<Editor
editorState={editorState}
onChange={this.onChange}
onCopy={onDraftEditorCopy}
onCut={onDraftEditorCut}
handlePastedText={this.handlePastedText}
/>
);
}
}
```

The copy/cut event handlers will ensure the clipboard contains a full representation of the Draft.js content state on copy/cut, while `handleDraftEditorPastedText` retrieves Draft.js content state from the clipboard. Voilà! This also changes the HTML clipboard content to be more semantic, with less styles copied to other word processors/editors.

You can also use `getDraftEditorPastedContent` method and set new EditorState by yourself. It is useful when you need to do some transformation with content (for example filtering unsupported styles), before past it in the state.

#### With draft.js 0.10

The above code relies on the `onCopy` and `onCut` event handlers, only available from Draft.js v0.11.0 onwards. For Draft.js v0.10.5, use `registerCopySource` instead, providing a `ref` to the editor:

```js
import {
registerCopySource,
handleDraftEditorPastedText,
} from "draftjs-conductor";

class MyEditor extends Component {
componentDidMount() {
this.copySource = registerCopySource(this.editorRef);
}

componentWillUnmount() {
if (this.copySource) {
this.copySource.unregister();
Expand All @@ -129,11 +168,29 @@ class MyEditor extends Component {
}
```

`registerCopySource` will ensure the clipboard contains a full representation of the Draft.js content state on copy, while `handleDraftEditorPastedText` retrieves Draft.js content state from the clipboard. Voilà! This also changes the HTML clipboard content to be more semantic, with less styles copied to other word processors/editors.
#### With draft-js-plugins

You can also use `getDraftEditorPastedContent` method and set new EditorState by yourself. It is useful when you need to do some transformation with content (for example filtering unsupported styles), before past it in the state.
The setup is slightly different with `draft-js-plugins` (and React hooks) – we need to use the provided `getEditorRef` method:

Note: IE11 isn’t supported, as it doesn't support storing HTML in the clipboard, and we also use the [`Element.closest`](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest) API.
```tsx
// reference to the editor
const editor = useRef<Editor>(null);

// register code for copying
useEffect(() => {
let unregisterCopySource: undefined | unregisterObject = undefined;
if (editor.current !== null) {
unregisterCopySource = registerCopySource(
editor.current.getEditorRef() as any,
);
}
return () => {
unregisterCopySource?.unregister();
};
});
```

See [#115](https://github.com/thibaudcolas/draftjs-conductor/issues/115) for further details.

### Editor state data conversion helpers

Expand Down
6 changes: 5 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import pkg from "./package.json";

const BANNER = `// @flow`;

export default [
const config = [
{
input: "src/lib/index.js",
external: [
"draft-js/lib/getDraftEditorSelection",
"draft-js/lib/getContentStateFragment",
"draft-js/lib/editOnCopy",
"draft-js/lib/editOnCut",
].concat(Object.keys(pkg.peerDependencies)),
output: [
{ file: pkg.main, format: "cjs" },
Expand All @@ -34,3 +36,5 @@ export default [
],
},
];

export default config;
21 changes: 4 additions & 17 deletions src/demo/components/DemoEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import type { DraftEntityType } from "draft-js/lib/DraftEntityType";
import {
ListNestingStyles,
blockDepthStyleFn,
registerCopySource,
onDraftEditorCopy,
onDraftEditorCut,
handleDraftEditorPastedText,
createEditorStateFromRaw,
serialiseEditorStateToRaw,
Expand Down Expand Up @@ -113,15 +114,10 @@ type State = {
readOnly: boolean,
};

/* :: import type { ElementRef } from "react"; */

/**
* Demo editor.
*/
class DemoEditor extends Component<Props, State> {
/* :: editorRef: ?ElementRef<Editor>; */
/* :: copySource: { unregister: () => void }; */

static defaultProps = {
rawContentState: null,
};
Expand Down Expand Up @@ -154,14 +150,6 @@ class DemoEditor extends Component<Props, State> {
this.handlePastedText = this.handlePastedText.bind(this);
}

componentDidMount() {
this.copySource = registerCopySource(this.editorRef);
}

componentWillUnmount() {
this.copySource.unregister();
}

/* :: onChange: (nextState: EditorState) => void; */
onChange(nextState: EditorState) {
this.setState({ editorState: nextState });
Expand Down Expand Up @@ -345,16 +333,15 @@ class DemoEditor extends Component<Props, State> {
</button>
</div>
<Editor
ref={(ref) => {
this.editorRef = ref;
}}
editorState={editorState}
readOnly={readOnly}
onChange={this.onChange}
stripPastedStyles={false}
blockRendererFn={this.blockRenderer}
blockStyleFn={blockDepthStyleFn}
keyBindingFn={this.keyBindingFn}
onCopy={onDraftEditorCopy}
onCut={onDraftEditorCut}
handlePastedText={this.handlePastedText}
/>
</SentryBoundary>
Expand Down
37 changes: 15 additions & 22 deletions src/demo/components/DemoEditor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@ describe("DemoEditor", () => {
).toMatchSnapshot();
});

it("componentWillUnmount", () => {
const wrapper = mount(<DemoEditor extended={false} />);
const copySource = wrapper.instance().copySource;
jest.spyOn(copySource, "unregister");
expect(copySource).not.toBeNull();
wrapper.unmount();
expect(copySource.unregister).toHaveBeenCalled();
});

describe("#extended", () => {
it("works", () => {
expect(
Expand Down Expand Up @@ -127,19 +118,21 @@ describe("DemoEditor", () => {
});

it("no entity", () => {
window.sessionStorage.getItem = jest.fn(() =>
JSON.stringify({
entityMap: {},
blocks: [
{
type: "atomic",
text: " ",
entityRanges: [],
},
],
}),
);
const editable = mount(<DemoEditor extended={true} />)
const editable = mount(
<DemoEditor
rawContentState={{
entityMap: {},
blocks: [
{
type: "atomic",
text: " ",
entityRanges: [],
},
],
}}
extended={true}
/>,
)
.instance()
.blockRenderer({
getType: () => "atomic",
Expand Down
35 changes: 26 additions & 9 deletions src/lib/api/copypaste.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// @flow
import getContentStateFragment from "draft-js/lib/getContentStateFragment";
import getDraftEditorSelection from "draft-js/lib/getDraftEditorSelection";
import editOnCopy from "draft-js/lib/editOnCopy";
import editOnCut from "draft-js/lib/editOnCut";

import {
EditorState,
Modifier,
Expand Down Expand Up @@ -67,11 +70,9 @@ const getSelectedContent = (
// See also https://github.com/basecamp/trix/blob/62145978f352b8d971cf009882ba06ca91a16292/src/trix/controllers/input_controller.coffee#L415-L422
// We serialise the editor content within HTML, not as a separate mime type, because Draft.js only allows access
// to HTML in its paste event handler.
const draftEditorCopyListener = (
const draftEditorCopyCutListener = (
ref: ElementRef<Editor>,
e: Event & {
clipboardData: DataTransfer,
},
e: SyntheticClipboardEvent<>,
) => {
const selection = window.getSelection();

Expand Down Expand Up @@ -111,20 +112,36 @@ const draftEditorCopyListener = (
}
};

export const onDraftEditorCopy = (
editor: Editor,
e: SyntheticClipboardEvent<>,
) => {
draftEditorCopyCutListener(editor, e);
editOnCopy(editor, e);
};

export const onDraftEditorCut = (
editor: Editor,
e: SyntheticClipboardEvent<>,
) => {
draftEditorCopyCutListener(editor, e);
editOnCut(editor, e);
};

/**
* Registers custom copy/cut event listeners on an editor.
*/
export const registerCopySource = (ref: ElementRef<Editor>) => {
const editorElt = ref.editor;
const onCopy = draftEditorCopyListener.bind(null, ref);
const onCopyCut = draftEditorCopyCutListener.bind(null, ref);

editorElt.addEventListener("copy", onCopy);
editorElt.addEventListener("cut", onCopy);
editorElt.addEventListener("copy", onCopyCut);
editorElt.addEventListener("cut", onCopyCut);

return {
unregister() {
editorElt.removeEventListener("copy", onCopy);
editorElt.removeEventListener("cut", onCopy);
editorElt.removeEventListener("copy", onCopyCut);
editorElt.removeEventListener("cut", onCopyCut);
},
};
};
Expand Down
23 changes: 23 additions & 0 deletions src/lib/api/copypaste.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { EditorState, convertFromRaw, convertToRaw } from "draft-js";
import {
registerCopySource,
onDraftEditorCopy,
onDraftEditorCut,
handleDraftEditorPastedText,
getDraftEditorPastedContent,
} from "./copypaste";
Expand All @@ -11,11 +13,16 @@ jest.mock("draft-js/lib/getContentStateFragment", () => (content) =>
content.getBlockMap(),
);

jest.mock("draft-js/lib/editOnCopy", () => jest.fn(() => {}));
jest.mock("draft-js/lib/editOnCut", () => jest.fn(() => {}));

jest.mock("draft-js-10/lib/generateRandomKey", () => () => "a");
jest.mock("draft-js-10/lib/getDraftEditorSelection", () => () => ({}));
jest.mock("draft-js-10/lib/getContentStateFragment", () => (content) =>
content.getBlockMap(),
);
jest.mock("draft-js-10/lib/editOnCopy", () => jest.fn(() => {}));
jest.mock("draft-js-10/lib/editOnCut", () => jest.fn(() => {}));

const dispatchEvent = (editor, type, setData) => {
const event = Object.assign(new Event(type), {
Expand Down Expand Up @@ -88,6 +95,22 @@ describe("copypaste", () => {
});
});

describe("onDraftEditorCopy", () => {
it("calls editOnCopy", () => {
const editor = document.createElement("div");
window.getSelection = getSelection();
onDraftEditorCopy(editor, dispatchEvent(editor, "copy"));
});
});

describe("onDraftEditorCut", () => {
it("does not break", () => {
const editor = document.createElement("div");
window.getSelection = getSelection();
onDraftEditorCut(editor, dispatchEvent(editor, "cut"));
});
});

/**
* jsdom does not implement the DOM selection API, we have to do a lot of overriding.
*/
Expand Down
2 changes: 2 additions & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export {

export {
registerCopySource,
onDraftEditorCopy,
onDraftEditorCut,
handleDraftEditorPastedText,
getDraftEditorPastedContent,
} from "./api/copypaste";
Expand Down

0 comments on commit 05b31cb

Please sign in to comment.