From 2d37f01a7cc3fbd0dee51bf4877dd606dffe3d64 Mon Sep 17 00:00:00 2001 From: Thibaud Colas Date: Mon, 4 Jun 2018 02:43:34 +0300 Subject: [PATCH] Add Draft.js copy-paste handling overrides from draftjs-conductor. Fix #147 This makes Draftail always preserve the full content as-is when copy-pasting between editors. See also https://github.com/thibaudcolas/draftjs-conductor/pull/2. --- CHANGELOG.md | 4 + lib/components/DraftailEditor.js | 34 +++- lib/components/DraftailEditor.test.js | 178 +++++++++++++----- .../__snapshots__/DraftailEditor.test.js.snap | 2 + 4 files changed, 166 insertions(+), 52 deletions(-) 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 = `

${text}

`; + + 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]}