Skip to content
This repository has been archived by the owner on Feb 6, 2023. It is now read-only.

Transfer selected content via HTML clipboard on copy-paste between editors #1784

Conversation

thibaudcolas
Copy link
Contributor

@thibaudcolas thibaudcolas commented Jun 10, 2018

Summary

This PR changes how Draft.js handles copy-cut-paste between editors so all content is kept as-is. I’ve been working on this as an external package at /~https://github.com/thibaudcolas/draftjs-conductor, but there is no reason for this not to just be fixed in Draft.js directly.

On copy, we store the selected content within the clipboard’s HTML, overriding the browser’s default copy/cut handling, so that the Draft.js paste processor can then retrieve content from the serialised "Raw" content instead of relying on HTML parsing.

This fixes the following issues: #380, #787, #1389, #1163, #1154, #1605 (review), and #523 but only for the copy-paste case, #523 (comment). Here's what it looks like in practice, copy-pasting between two tabs in the media example:

draftjs-copy-paste-787

As-is, the new copy/cut behavior and HTML clipboard generation doesn't replicate browsers’ behavior of inlining styles into the clipboard content (e.g. if you copy content in a particular font into Google Docs, browsers will try to preserve that). This seems like an advantage to me, but might be worth assessing further. For example, it means that copy-pasting nested lists into Google Docs will no longer add extra indentation to the (otherwise not nested anymore) list items there.

Edit: one last thing – I put the serialised content inside text/html instead of using a separate mime type for two reasons. First one is that without this PR, Draft.js only offered access to HTML via handlePastedText for me to hook into. Second reason is that I've heard some browsers didn't support custom mime types. Fixing this issue in Draft.js itself, we could revisit this.

Test Plan

  • Add unit tests for editOnCopy – I don't think I've seen any yet, would welcome some direction on how to test this.
  • Add unit tests for DraftPasteProcessor
  • Run the manual copy-paste test plan in browsers supported by Draft.js (particularly IE11)
  • Go through copy-pasting into multiple word processors, with the following support matrix.

Before making this PR, I first fixed this issue for my own editor. If you want to learn more, have a look at wagtail/wagtail#4582.

If you want to use this in your own editor without waiting for the PR to be merged, I made my fix available as a separate package: /~https://github.com/thibaudcolas/draftjs-conductor#idempotent-copy-paste-between-draftjs-editors. Here's what it looks like to use it:

npm install --save draftjs-conductor

Then:

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

class MyEditor extends Component {
  constructor(props: Props) {
    super(props);

    this.state = {
      editorState: EditorState.createEmpty(),
    };

    this.onChange = this.onChange.bind(this);
    this.handlePastedText = this.handlePastedText.bind(this);
  }

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

  onChange(nextState: EditorState) {
    this.setState({ editorState: nextState });
  }

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

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

    return false;
  }

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

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

    return (
      <Editor
        ref={(ref) => {
          this.editorRef = ref;
        }}
        editorState={editorState}
        onChange={this.onChange}
        handlePastedText={this.handlePastedText}
      />
    );
  }
}

editor.setClipboard(fragment);

// IE11 does not support ClipboardEvent.clipboardData.
if (e.clipboardData && fragment) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think skipping IE11 here is fair, since it doesn't handle HTML in the clipboard to start with.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We handle IE11 in our fork in a rather different way. We have two hidden divs, one for copy and one for paste, that exist to accept or receive incoming content. In the copy case we copy the HTML of the component into this div and temporarily move selection so that the browser is able to copy that content in the way it wants to. For paste we temporarily move focus to the other div and then grab the content from there and convert it into a fragment.

The trick is ensuring those hidden divs are styled the same as the editor and that the focus games play out correctly but it works extremely well.

Copy link
Contributor Author

@thibaudcolas thibaudcolas Jun 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@colinjeanne could you share your code's fork, at least the part that's relevant to this? I think I'd be keen to add this to my editor as well, depending on how much code it takes.

Edit: I see textioHQ@86b8a41 from #986.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there are a few different changes, the first was textioHQ#21 followed later by textioHQ#26. We originally wrote this solution in order to get HTML into the clipboard even for browsers that didn't support adding it explicitly.


const fragmentElt = document.createElement('div');
const domSelection = window.getSelection();
fragmentElt.appendChild(domSelection.getRangeAt(0).cloneContents());
Copy link
Contributor Author

@thibaudcolas thibaudcolas Jun 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is taken from Rangy: /~https://github.com/timdown/rangy/blob/1e55169d2e4d1d9458c2a87119addf47a8265276/src/core/domrange.js#L515-L520.

Also from what I understand modern browsers only support one range in the selection.


try {
// If JSON parsing fails, handle the paste as normal HTML.
rawContent = JSON.parse(fragmentAttr);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure in what scenario that would occur – perhaps if the clipboard gets truncated somehow.

ericls added a commit to aminland/draft-js that referenced this pull request Jul 6, 2018
@pablopunk
Copy link

Any update on this? I think I'm having an issue that could be solved with this PR

@thibaudcolas
Copy link
Contributor Author

@pablopunk this is working really well in my editor via the separate package I made available (/~https://github.com/thibaudcolas/draftjs-conductor#idempotent-copy-paste-between-draftjs-editors). If you have time to try it out, I'm sure more feedback would help the PR.

@rchasman
Copy link

Thanks for this @thibaudcolas, and packaging it so nicely! We are having the same issues. Facebook, any plans to merge this?

@niveditc
Copy link
Contributor

Thanks @thibaudcolas for submitting this & other folks for confirming that this is working for you :)

For this change, I want to make sure that it doesn't break any existing copy-paste behavior on facebook.com or our internal editors. Will import into our stack and test it soon - thanks for your patience!

@thibaudcolas
Copy link
Contributor Author

thibaudcolas commented Jan 24, 2019

That's great to hear @niveditc! I haven't received any bug reports from the users of my editor since putting this in production in august 2018. That said, there are some sources of bugs I can easily think of:

For the last issue, this is the most problematic IMO but is actually already happening when pasting rich text with links or images in an editor that doesn't support them. So in a sense it’s nothing new, it will just become more likely if the copy/paste retains more content. I haven't had this issue with my editor because I also made a filtering library (/~https://github.com/thibaudcolas/draftjs-filters) so copy/pasting only keeps what the editor actually supports.

facebook-github-bot pushed a commit that referenced this pull request Apr 22, 2019
…ustom entities.

Summary:
details are here: #1784
in the comment, nivedita mentioned we need to test this out for intern editor
hence not merging the PR.

since that comment was made back in Jan, so it's probably gonna take a while
for this to be merged into FB codebase. In the mean time this is blocking
CMS Editor's embeded CMS use case. So I figured we should do a branching
to merge the fix just for CMS Editor.

Unfortunately, the logic is pretty deep in the DraftJS stack, so I had to make
a couple changes to pass down the copy paste behavior.

Differential Revision: D14626028

fbshipit-source-id: 4826ea33abf2618835841200af81e10d707aa23b
jdecked pushed a commit to twitter-forks/draft-js that referenced this pull request Oct 9, 2019
…ustom entities.

Summary:
details are here: facebookarchive#1784
in the comment, nivedita mentioned we need to test this out for intern editor
hence not merging the PR.

since that comment was made back in Jan, so it's probably gonna take a while
for this to be merged into FB codebase. In the mean time this is blocking
CMS Editor's embeded CMS use case. So I figured we should do a branching
to merge the fix just for CMS Editor.

Unfortunately, the logic is pretty deep in the DraftJS stack, so I had to make
a couple changes to pass down the copy paste behavior.

Differential Revision: D14626028

fbshipit-source-id: 4826ea33abf2618835841200af81e10d707aa23b
jdecked pushed a commit to twitter-forks/draft-js that referenced this pull request Oct 9, 2019
…ustom entities.

Summary:
details are here: facebookarchive#1784
in the comment, nivedita mentioned we need to test this out for intern editor
hence not merging the PR.

since that comment was made back in Jan, so it's probably gonna take a while
for this to be merged into FB codebase. In the mean time this is blocking
CMS Editor's embeded CMS use case. So I figured we should do a branching
to merge the fix just for CMS Editor.

Unfortunately, the logic is pretty deep in the DraftJS stack, so I had to make
a couple changes to pass down the copy paste behavior.

Differential Revision: D14626028

fbshipit-source-id: 4826ea33abf2618835841200af81e10d707aa23b
mmissey pushed a commit to mmissey/draft-js that referenced this pull request Mar 24, 2020
…ustom entities.

Summary:
details are here: facebookarchive#1784
in the comment, nivedita mentioned we need to test this out for intern editor
hence not merging the PR.

since that comment was made back in Jan, so it's probably gonna take a while
for this to be merged into FB codebase. In the mean time this is blocking
CMS Editor's embeded CMS use case. So I figured we should do a branching
to merge the fix just for CMS Editor.

Unfortunately, the logic is pretty deep in the DraftJS stack, so I had to make
a couple changes to pass down the copy paste behavior.

Differential Revision: D14626028

fbshipit-source-id: 4826ea33abf2618835841200af81e10d707aa23b
@yasuf
Copy link

yasuf commented Apr 24, 2020

wow @thibaudcolas thanks a lot for the detailed explanation I'll try using your package for this!

@thibaudcolas
Copy link
Contributor Author

My pleasure @yasuf 👌 Let me know if you run into any hiccups.

@thibaudcolas
Copy link
Contributor Author

I’ve updated the user-land implementation in draftjs-conductor to use the new onCopy and onCut methods added in Draft.js v0.11.0 (d09ef3e). This makes the API a bit nicer, but doesn’t change implementation much. Unfortunately those new event handlers don’t allow falling back to the built-in Draft.js implementation like other methods do (#721), so under the hood there’s more hooking into Draft.js internals needed.


The next step would be to switch to using a custom MIME type to transfer the pasted content (application/x-draft-js), and use the new onPaste to read from this (thibaudcolas/draftjs-conductor#269). If I get to this I might update this PR to also change its implementation, but 2 years on it would be nice to get some signal from maintainers to make sure I’m not doing this for nothing.

@thibaudcolas
Copy link
Contributor Author

Closing because of #3136. I’m glad I didn’t waste any further time on this PR!

@thibaudcolas thibaudcolas deleted the feature/copy-paste-between-editors-787 branch May 10, 2022 08:22
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants