Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Override Draft.js copy-paste to preserve full editor content #2

Merged
merged 21 commits into from
Jun 3, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2f899bc
chore(demo): add hr to demo site to test copy-paste idempotence
thibaudcolas May 20, 2018
4366190
feat(copy-paste): add wip internal clipboard reuse for copy-paste
thibaudcolas May 20, 2018
1869b10
feat(copy-paste): wip HTML injection implementation of idempotent paste
thibaudcolas May 20, 2018
7ce0af4
feat(copy-paste): finish copy source registration implementation
thibaudcolas May 21, 2018
0976c7e
feat(copy-paste): use DOMParser instead of regexes for paste handling
thibaudcolas May 23, 2018
2c37f74
feat(copy-paste): expose copy-paste functions in package
thibaudcolas May 23, 2018
adadf15
feat(copy-paste): simplify copy-paste API by storing scope at callsite
thibaudcolas May 26, 2018
4d1bc53
feat(copy-paste): add support for invalid JSON in copy-paste
thibaudcolas May 27, 2018
138ec99
feat(copy-paste): improve dom attr cleanup after copy event is over
thibaudcolas May 28, 2018
c68b8b6
docs(copy-paste): document new copy-paste related helper
thibaudcolas May 28, 2018
84d9ed8
chore(build): update react-scripts to latest
thibaudcolas Jun 2, 2018
c6d572f
test(copy-paste): use mount instead of shallow so enzyme tests use refs
thibaudcolas Jun 2, 2018
bd918be
chore(demo): add BR button to the demo to facilitate copy-paste testing
thibaudcolas Jun 2, 2018
52a33b3
chore(demo): fix highlight component taking too long to render
thibaudcolas Jun 3, 2018
49d5403
feat(copy-paste): update copy handler to use clipboardData
thibaudcolas Jun 3, 2018
8d0126d
chore(demo): preventDefault on line break button
thibaudcolas Jun 3, 2018
b207637
feat(copy-paste): add support for cut event
thibaudcolas Jun 3, 2018
a9da3dd
feat(copy-paste): opt-out copy-paste handling in IE11
thibaudcolas Jun 3, 2018
028425f
chore(demo): remove unused Prism dependency
thibaudcolas Jun 3, 2018
7593e8d
test(copy-paste): add unit tests for all copy-paste handling
thibaudcolas Jun 3, 2018
dbc5a3b
chore(demo): add missing tests for demo editor copy-paste
thibaudcolas Jun 3, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/demo/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class App extends Component<{}> {
return (
<div className="App">
<DemoEditor extended={false} />
<DemoEditor extended={true} />
</div>
);
}
Expand Down
74 changes: 72 additions & 2 deletions src/demo/components/DemoEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import {
import type { DraftBlockType } from "draft-js/lib/DraftBlockType.js.flow";
import type { DraftEntityType } from "draft-js/lib/DraftEntityType.js.flow";

import { ListNestingStyles, blockDepthStyleFn } from "../../lib/index";
import {
ListNestingStyles,
blockDepthStyleFn,
registerCopySource,
unregisterCopySource,
handleDraftEditorPastedText,
} from "../../lib/index";

import SentryBoundary from "./SentryBoundary";
import Highlight from "./Highlight";
Expand Down Expand Up @@ -76,6 +82,12 @@ const ENTITIES = [
src: "^http",
},
},
{
type: "HORIZONTAL_RULE",
label: "HR",
attributes: [],
whitelist: {},
},
];

const MAX_LIST_NESTING = 15;
Expand All @@ -92,6 +104,8 @@ type State = {
* Demo editor.
*/
class DemoEditor extends Component<Props, State> {
editorRef: ?Object;

constructor(props: Props) {
super(props);
const { extended } = props;
Expand Down Expand Up @@ -124,6 +138,15 @@ class DemoEditor extends Component<Props, State> {
(this: any).toggleBlock = this.toggleBlock.bind(this);
(this: any).toggleEntity = this.toggleEntity.bind(this);
(this: any).blockRenderer = this.blockRenderer.bind(this);
(this: any).handlePastedText = this.handlePastedText.bind(this);
}

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

componentWillUnmount() {
unregisterCopySource(this.editorRef);
}

onChange(nextState: EditorState) {
Expand All @@ -149,7 +172,7 @@ class DemoEditor extends Component<Props, State> {
e.preventDefault();
}

toggleEntity(type: DraftEntityType) {
toggleEntity(type: DraftEntityType | "HORIZONTAL_RULE") {
const { editorState } = this.state;
let content = editorState.getCurrentContent();

Expand All @@ -162,6 +185,13 @@ class DemoEditor extends Component<Props, State> {
this.onChange(
AtomicBlockUtils.insertAtomicBlock(editorState, entityKey, " "),
);
} else if (type === "HORIZONTAL_RULE") {
// $FlowFixMe
content = content.createEntity(type, "IMMUTABLE", {});
const entityKey = content.getLastCreatedEntityKey();
this.onChange(
AtomicBlockUtils.insertAtomicBlock(editorState, entityKey, " "),
);
} else {
content = content.createEntity(type, "MUTABLE", {
url: "http://www.example.com/",
Expand All @@ -173,16 +203,52 @@ class DemoEditor extends Component<Props, State> {
}

blockRenderer(block: ContentBlock) {
const { editorState } = this.state;
const content = editorState.getCurrentContent();

if (block.getType() !== "atomic") {
return null;
}

const entityKey = block.getEntityAt(0);

if (!entityKey) {
return {
editable: false,
};
}

const entity = content.getEntity(entityKey);

if (entity.getType() === "HORIZONTAL_RULE") {
return {
component: () => <hr />,
editable: false,
};
}

return {
component: Image,
editable: false,
};
}

handlePastedText(text: string, html: ?string, editorState: EditorState) {
let newState = handleDraftEditorPastedText(
this.editorRef,
text,
html,
editorState,
);

if (newState) {
this.onChange(newState);
return true;
}

return false;
}

onTab(event: SyntheticKeyboardEvent<>) {
const { editorState } = this.state;
const newState = RichUtils.onTab(event, editorState, MAX_LIST_NESTING);
Expand Down Expand Up @@ -226,12 +292,16 @@ class DemoEditor extends Component<Props, State> {
))}
</div>
<Editor
ref={(ref) => {
this.editorRef = ref;
}}
editorState={editorState}
onChange={this.onChange}
stripPastedStyles={false}
blockRendererFn={this.blockRenderer}
blockStyleFn={blockDepthStyleFn}
onTab={this.onTab}
handlePastedText={this.handlePastedText}
/>
</SentryBoundary>
<ListNestingStyles max={MAX_LIST_NESTING} />
Expand Down
111 changes: 95 additions & 16 deletions src/demo/components/DemoEditor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,31 +90,110 @@ describe("DemoEditor", () => {
expect(RichUtils.toggleBlockType).toHaveBeenCalled();
});

it("toggleEntity - LINK", () => {
shallow(<DemoEditor extended={false} />)
.instance()
.toggleEntity("LINK");
describe("toggleEntity", () => {
it("LINK", () => {
shallow(<DemoEditor extended={false} />)
.instance()
.toggleEntity("LINK");

expect(RichUtils.toggleLink).toHaveBeenCalled();
});
expect(RichUtils.toggleLink).toHaveBeenCalled();
});

it("toggleEntity - IMAGE", () => {
shallow(<DemoEditor extended={false} />)
.instance()
.toggleEntity("IMAGE");
it("IMAGE", () => {
shallow(<DemoEditor extended={false} />)
.instance()
.toggleEntity("IMAGE");

expect(AtomicBlockUtils.insertAtomicBlock).toHaveBeenCalled();
});
expect(AtomicBlockUtils.insertAtomicBlock).toHaveBeenCalled();
});

it("blockRenderer", () => {
expect(
it("HORIZONTAL_RULE", () => {
shallow(<DemoEditor extended={false} />)
.instance()
.toggleEntity("HORIZONTAL_RULE");

expect(AtomicBlockUtils.insertAtomicBlock).toHaveBeenCalled();
});
});

describe("blockRenderer", () => {
it("unstyled", () => {
expect(
shallow(<DemoEditor extended={false} />)
.instance()
.blockRenderer({
getType: () => "unstyled",
}),
).toBe(null);
});

it("HORIZONTAL_RULE", () => {
window.sessionStorage.getItem = jest.fn(() =>
JSON.stringify({
entityMap: {
"3": {
type: "HORIZONTAL_RULE",
data: {},
},
},
blocks: [
{
type: "atomic",
text: " ",
entityRanges: [
{
key: 3,
offset: 0,
length: 1,
},
],
},
],
}),
);
const Component = shallow(<DemoEditor extended={true} />)
.instance()
.blockRenderer({
getType: () => "atomic",
getEntityAt: () => "3",
}).component;
expect(Component()).toEqual(<hr />);
});

it("IMAGE", () => {
window.sessionStorage.getItem = jest.fn(() =>
JSON.stringify({
entityMap: {
"1": {
type: "IMAGE",
data: {
src: "example.png",
},
},
},
blocks: [
{
type: "atomic",
text: " ",
entityRanges: [
{
key: 1,
offset: 0,
length: 1,
},
],
},
],
}),
).toMatchObject({
editable: false,
);

const Component = shallow(<DemoEditor extended={true} />)
.instance()
.blockRenderer({
getType: () => "atomic",
getEntityAt: () => "1",
}).component;
expect(<Component />).toMatchSnapshot();
});
});

Expand Down
3 changes: 3 additions & 0 deletions src/demo/components/__snapshots__/App.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ exports[`App renders 1`] = `
<DemoEditor
extended={false}
/>
<DemoEditor
extended={true}
/>
</div>
`;
14 changes: 14 additions & 0 deletions src/demo/components/__snapshots__/DemoEditor.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,17 @@ exports[`DemoEditor #extended works 1`] = `
>
📷
</button>
<button
key="HORIZONTAL_RULE"
onMouseDown={[Function]}
>
HR
</button>
</div>
`;

exports[`DemoEditor blockRenderer IMAGE 1`] = `<Image />`;

exports[`DemoEditor renders 1`] = `
<div
className="EditorToolbar"
Expand Down Expand Up @@ -185,5 +193,11 @@ exports[`DemoEditor renders 1`] = `
>
📷
</button>
<button
key="HORIZONTAL_RULE"
onMouseDown={[Function]}
>
HR
</button>
</div>
`;
Loading