diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb6305cf..0d82076b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@
## Unreleased
+### Added
+
+* Add Draft.js copy-paste handling overrides from `draftjs-conductor`. This makes Draftail always preserve the full content as-is when copy-pasting between editors. Fix [#147](https://github.com/springload/draftail/issues/147) ([thibaudcolas/draftjs-conductor#2](https://github.com/thibaudcolas/draftjs-conductor/pull/2)).
+
## [[v0.17.1]](https://github.com/springload/draftail/releases/tag/v0.17.1)
### Changed
diff --git a/lib/components/DraftailEditor.js b/lib/components/DraftailEditor.js
index 8b2b0fc0..2e4bcc83 100644
--- a/lib/components/DraftailEditor.js
+++ b/lib/components/DraftailEditor.js
@@ -1,7 +1,11 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Editor, EditorState, RichUtils } from 'draft-js';
-import { ListNestingStyles } from 'draftjs-conductor';
+import {
+ ListNestingStyles,
+ registerCopySource,
+ handleDraftEditorPastedText,
+} from 'draftjs-conductor';
import {
ENTITY_TYPE,
@@ -48,6 +52,7 @@ class DraftailEditor extends Component {
this.onTab = this.onTab.bind(this);
this.handleKeyCommand = this.handleKeyCommand.bind(this);
this.handleBeforeInput = this.handleBeforeInput.bind(this);
+ this.handlePastedText = this.handlePastedText.bind(this);
this.toggleBlockType = this.toggleBlockType.bind(this);
this.toggleInlineStyle = this.toggleInlineStyle.bind(this);
@@ -312,6 +317,24 @@ class DraftailEditor extends Component {
return NOT_HANDLED;
}
+ handlePastedText(text, html, editorState) {
+ const { stripPastedStyles } = this.props;
+
+ // Leave paste handling to Draft.js when stripping styles is desirable.
+ if (stripPastedStyles) {
+ return false;
+ }
+
+ const pastedState = handleDraftEditorPastedText(html, editorState);
+
+ if (pastedState) {
+ this.onChange(pastedState);
+ return true;
+ }
+
+ return false;
+ }
+
toggleBlockType(blockType) {
const { editorState } = this.state;
this.onChange(RichUtils.toggleBlockType(editorState, blockType));
@@ -528,6 +551,14 @@ class DraftailEditor extends Component {
return null;
}
+ componentDidMount() {
+ this.copySource = registerCopySource(this.editorRef);
+ }
+
+ componentWillUnmount() {
+ this.copySource.unregister();
+ }
+
render() {
const {
placeholder,
@@ -609,6 +640,7 @@ class DraftailEditor extends Component {
)}
handleKeyCommand={this.handleKeyCommand}
handleBeforeInput={this.handleBeforeInput}
+ handlePastedText={this.handlePastedText}
onFocus={this.onFocus}
onBlur={this.onBlur}
onTab={this.onTab}
diff --git a/lib/components/DraftailEditor.test.js b/lib/components/DraftailEditor.test.js
index ba4455cb..38e79716 100644
--- a/lib/components/DraftailEditor.test.js
+++ b/lib/components/DraftailEditor.test.js
@@ -18,9 +18,12 @@ import { ENTITY_TYPE } from '../api/constants';
jest.mock('draft-js/lib/generateRandomKey', () => () => 'a');
+const shallowNoLifecycle = elt =>
+ shallow(elt, { disableLifecycleMethods: true });
+
describe('DraftailEditor', () => {
it('empty', () => {
- expect(shallow()).toMatchSnapshot();
+ expect(shallowNoLifecycle()).toMatchSnapshot();
});
it('editorRef', () => {
@@ -29,7 +32,7 @@ describe('DraftailEditor', () => {
it('#readOnly', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.setState({
readOnly: true,
})
@@ -40,7 +43,7 @@ describe('DraftailEditor', () => {
describe('#placeholder', () => {
it('visible', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.find(Editor)
.prop('placeholder'),
).toEqual('Write here…');
@@ -48,7 +51,7 @@ describe('DraftailEditor', () => {
it('hidden', () => {
expect(
- shallow(
+ shallowNoLifecycle(
{
it('#spellCheck', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.find(Editor)
.prop('spellCheck'),
).toEqual(true);
@@ -83,7 +86,7 @@ describe('DraftailEditor', () => {
it('#textAlignment', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.find(Editor)
.prop('textAlignment'),
).toEqual('left');
@@ -91,7 +94,7 @@ describe('DraftailEditor', () => {
it('#textDirectionality', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.find(Editor)
.prop('textDirectionality'),
).toEqual('RTL');
@@ -99,7 +102,7 @@ describe('DraftailEditor', () => {
it('#autoCapitalize', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.find(Editor)
.prop('autoCapitalize'),
).toEqual('characters');
@@ -107,7 +110,7 @@ describe('DraftailEditor', () => {
it('#autoComplete', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.find(Editor)
.prop('autoComplete'),
).toEqual('given-name');
@@ -115,7 +118,7 @@ describe('DraftailEditor', () => {
it('#autoCorrect', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.find(Editor)
.prop('autoCorrect'),
).toEqual('on');
@@ -123,7 +126,7 @@ describe('DraftailEditor', () => {
it('#ariaDescribedBy', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.find(Editor)
.prop('ariaDescribedBy'),
).toEqual('test');
@@ -131,26 +134,26 @@ describe('DraftailEditor', () => {
it('#maxListNesting', () => {
expect(
- shallow(),
+ shallowNoLifecycle(),
).toMatchSnapshot();
});
it('#onSave', () => {
const onSave = jest.fn();
- const wrapper = shallow();
+ const wrapper = shallowNoLifecycle();
wrapper.instance().saveState();
expect(onSave).toHaveBeenCalled();
- shallow()
+ shallowNoLifecycle()
.instance()
.saveState();
});
it('readOnly', () => {
const onSave = jest.fn();
- const wrapper = shallow();
+ const wrapper = shallowNoLifecycle();
wrapper.instance().toggleEditor(true);
expect(wrapper.state('readOnly')).toBe(true);
@@ -158,6 +161,15 @@ describe('DraftailEditor', () => {
expect(wrapper.state('readOnly')).toBe(false);
});
+ it('componentWillUnmount', () => {
+ const wrapper = mount();
+ const copySource = wrapper.instance().copySource;
+ jest.spyOn(copySource, 'unregister');
+ expect(copySource).not.toBeNull();
+ wrapper.unmount();
+ expect(copySource.unregister).toHaveBeenCalled();
+ });
+
describe('onChange', () => {
beforeEach(() => {
jest
@@ -170,7 +182,7 @@ describe('DraftailEditor', () => {
});
it('no filter when typing', () => {
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
,
);
@@ -201,7 +213,7 @@ describe('DraftailEditor', () => {
});
it('filter on paste', () => {
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
,
);
@@ -233,7 +245,7 @@ describe('DraftailEditor', () => {
});
it('getEditorState', () => {
- const wrapper = shallow();
+ const wrapper = shallowNoLifecycle();
expect(wrapper.instance().getEditorState()).toBeInstanceOf(EditorState);
});
@@ -243,7 +255,7 @@ describe('DraftailEditor', () => {
jest
.spyOn(DraftUtils, 'handleNewLine')
.mockImplementation(editorState => editorState);
- const wrapper = shallow();
+ const wrapper = shallowNoLifecycle();
expect(
wrapper.instance().handleReturn({
@@ -254,7 +266,9 @@ describe('DraftailEditor', () => {
DraftUtils.handleNewLine.mockRestore();
});
it('enabled br', () => {
- const wrapper = shallow();
+ const wrapper = shallowNoLifecycle(
+ ,
+ );
expect(
wrapper.instance().handleReturn({
@@ -263,7 +277,7 @@ describe('DraftailEditor', () => {
).toBe(false);
});
it('alt + enter on text', () => {
- const wrapper = shallow();
+ const wrapper = shallowNoLifecycle();
expect(
wrapper.instance().handleReturn({
@@ -272,7 +286,7 @@ describe('DraftailEditor', () => {
).toBe(true);
});
it('alt + enter on entity without url', () => {
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
{
it('alt + enter on entity', () => {
jest.spyOn(window, 'open');
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
{
});
it('onFocus, onBlur', () => {
- const wrapper = shallow();
+ const wrapper = shallowNoLifecycle();
expect(wrapper.state('hasFocus')).toBe(false);
@@ -381,7 +395,7 @@ describe('DraftailEditor', () => {
jest.spyOn(RichUtils, 'onTab');
expect(
- shallow()
+ shallowNoLifecycle()
.instance()
.onTab(),
).toBe(true);
@@ -396,7 +410,7 @@ describe('DraftailEditor', () => {
RichUtils.handleKeyCommand = jest.fn(editorState => editorState);
expect(
- shallow()
+ shallowNoLifecycle()
.instance()
.handleKeyCommand('backspace'),
).toBe(true);
@@ -408,7 +422,7 @@ describe('DraftailEditor', () => {
RichUtils.handleKeyCommand = jest.fn(() => false);
expect(
- shallow()
+ shallowNoLifecycle()
.instance()
.handleKeyCommand('backspace'),
).toBe(false);
@@ -418,7 +432,7 @@ describe('DraftailEditor', () => {
it('entity type', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.instance()
.handleKeyCommand('LINK'),
).toBe(true);
@@ -426,7 +440,7 @@ describe('DraftailEditor', () => {
it('block type', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.instance()
.handleKeyCommand('header-one'),
).toBe(true);
@@ -434,7 +448,7 @@ describe('DraftailEditor', () => {
it('inline style', () => {
expect(
- shallow()
+ shallowNoLifecycle()
.instance()
.handleKeyCommand('BOLD'),
).toBe(true);
@@ -447,7 +461,7 @@ describe('DraftailEditor', () => {
.mockImplementation(e => e);
expect(
- shallow()
+ shallowNoLifecycle()
.instance()
.handleKeyCommand('delete'),
).toBe(true);
@@ -460,7 +474,7 @@ describe('DraftailEditor', () => {
jest.spyOn(DraftUtils, 'handleDeleteAtomic');
expect(
- shallow()
+ shallowNoLifecycle()
.instance()
.handleKeyCommand('delete'),
).toBe(false);
@@ -475,7 +489,9 @@ describe('DraftailEditor', () => {
let wrapper;
beforeEach(() => {
- wrapper = shallow();
+ wrapper = shallowNoLifecycle(
+ ,
+ );
jest.spyOn(DraftUtils, 'resetBlockWithType');
jest
@@ -542,11 +558,69 @@ describe('DraftailEditor', () => {
});
});
+ describe('handlePastedText', () => {
+ it('default handling', () => {
+ const wrapper = mount();
+
+ expect(
+ wrapper
+ .instance()
+ .handlePastedText(
+ 'this is plain text paste',
+ 'this is plain text paste',
+ wrapper.state('editorState'),
+ ),
+ ).toBe(false);
+ });
+
+ it('stripPastedStyles', () => {
+ const wrapper = mount();
+
+ expect(
+ wrapper
+ .instance()
+ .handlePastedText(
+ 'bold',
+ 'bold
',
+ wrapper.state('editorState'),
+ ),
+ ).toBe(false);
+ });
+
+ it('handled by handleDraftEditorPastedText', () => {
+ const wrapper = mount();
+ const text = 'hello,\nworld!';
+ const content = {
+ blocks: [
+ {
+ data: {},
+ depth: 0,
+ entityRanges: [],
+ inlineStyleRanges: [],
+ key: 'a',
+ text: text,
+ type: 'unstyled',
+ },
+ ],
+ entityMap: {},
+ };
+ const html = ``;
+
+ expect(
+ wrapper
+ .instance()
+ .handlePastedText(text, html, wrapper.state('editorState')),
+ ).toBe(true);
+ });
+ });
+
describe('toggleBlockType', () => {
let wrapper;
beforeEach(() => {
- wrapper = shallow();
+ wrapper = shallowNoLifecycle();
jest.spyOn(RichUtils, 'toggleBlockType');
jest.spyOn(wrapper.instance(), 'onChange');
@@ -568,7 +642,7 @@ describe('DraftailEditor', () => {
let wrapper;
beforeEach(() => {
- wrapper = shallow();
+ wrapper = shallowNoLifecycle();
jest.spyOn(RichUtils, 'toggleInlineStyle');
jest.spyOn(wrapper.instance(), 'onChange');
@@ -627,7 +701,7 @@ describe('DraftailEditor', () => {
});
it('works', () => {
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
{
});
it('block', () => {
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
{
});
it('works', () => {
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
{
});
it('block', () => {
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
{
let wrapper;
beforeEach(() => {
- wrapper = shallow();
+ wrapper = shallowNoLifecycle();
jest.spyOn(DraftUtils, 'addHorizontalRuleRemovingSelection');
jest.spyOn(wrapper.instance(), 'onChange');
@@ -782,7 +856,7 @@ describe('DraftailEditor', () => {
let wrapper;
beforeEach(() => {
- wrapper = shallow();
+ wrapper = shallowNoLifecycle();
jest.spyOn(DraftUtils, 'addLineBreak');
jest.spyOn(wrapper.instance(), 'onChange');
@@ -807,7 +881,7 @@ describe('DraftailEditor', () => {
jest.spyOn(EditorState, 'undo');
jest.spyOn(EditorState, 'redo');
- wrapper = shallow();
+ wrapper = shallowNoLifecycle();
jest.spyOn(wrapper.instance(), 'onChange');
});
@@ -851,7 +925,9 @@ describe('DraftailEditor', () => {
],
};
expect(
- shallow()
+ shallowNoLifecycle(
+ ,
+ )
.instance()
.blockRenderer(
convertFromRaw(rawContentState).getFirstBlock(),
@@ -880,7 +956,7 @@ describe('DraftailEditor', () => {
},
],
};
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
{
},
],
};
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
,
);
@@ -949,7 +1025,7 @@ describe('DraftailEditor', () => {
},
],
};
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
,
);
@@ -966,7 +1042,7 @@ describe('DraftailEditor', () => {
describe('source', () => {
describe('onRequestSource', () => {
it('empty', () => {
- const wrapper = shallow();
+ const wrapper = shallowNoLifecycle();
wrapper.instance().onRequestSource('LINK');
@@ -980,7 +1056,7 @@ describe('DraftailEditor', () => {
it('with sourceOptions', () => {
const source = () => ;
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
,
);
@@ -999,7 +1075,7 @@ describe('DraftailEditor', () => {
it('with entity', () => {
const source = () => ;
- const wrapper = shallow(
+ const wrapper = shallowNoLifecycle(
{
});
it('works', () => {
- const wrapper = shallow();
+ const wrapper = shallowNoLifecycle();
wrapper
.instance()
@@ -1081,7 +1157,7 @@ describe('DraftailEditor', () => {
describe('onCloseSource', () => {
it('works', () => {
- const wrapper = shallow();
+ const wrapper = shallowNoLifecycle();
wrapper.instance().toggleSource();
wrapper.instance().onCloseSource();
diff --git a/lib/components/__snapshots__/DraftailEditor.test.js.snap b/lib/components/__snapshots__/DraftailEditor.test.js.snap
index 08cec47a..872f44db 100644
--- a/lib/components/__snapshots__/DraftailEditor.test.js.snap
+++ b/lib/components/__snapshots__/DraftailEditor.test.js.snap
@@ -172,6 +172,7 @@ exports[`DraftailEditor #maxListNesting 1`] = `
}
handleBeforeInput={[Function]}
handleKeyCommand={[Function]}
+ handlePastedText={[Function]}
handleReturn={[Function]}
keyBindingFn={[Function]}
onBlur={[Function]}
@@ -364,6 +365,7 @@ exports[`DraftailEditor empty 1`] = `
}
handleBeforeInput={[Function]}
handleKeyCommand={[Function]}
+ handlePastedText={[Function]}
handleReturn={[Function]}
keyBindingFn={[Function]}
onBlur={[Function]}