-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
Fix for HTML pasting not working in IE #882
Fix for HTML pasting not working in IE #882
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @mahlero thanks so much for taking the time to write up this PR! I wrote a bunch of review comments inline, and some questions for you too.
Thanks!
examples/index.js
Outdated
@@ -1,4 +1,4 @@ | |||
|
|||
import 'babel-polyfill' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this needed for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first, I could not get the examples running in IE 11. Importing babel-polyfill at that location helped. I think it had to do with the loading order of react-dom and babel-polyfill, similar to what is described here: https://stackoverflow.com/a/40928047.
Now after rebuilding node_modules it works, seems to be solved with react-dom 15.5.6. I could dig further into this and reinstall the old version, but for now I reverted that commit.
src/components/content.js
Outdated
|
||
debug('onPaste', { event, data }) | ||
this.props.onPaste(event, data) | ||
if (doesNotSupportHtmlFromClipboard) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you change this to just if (IS_IE) {
?
And then can you add a comment about explaining exactly why we need this code, so that for future editing we don't break it. (Search the codebase for COMPAT:
comments to see examples of how much detail to use, and include today's date.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure.
* If pasted html can be retreived, it is added to the given `data` object, setting the `type` to `html`. | ||
* As this method is asynchronous, a callback is needed to return the `data` object. | ||
* | ||
* Solution adapted from http://stackoverflow.com/a/6804718). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing out.
import { findDOMNode } from 'react-dom' | ||
|
||
/** | ||
* Get clipboard html data by capturing the html inserted by the browser's native paste action. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For all comments (including this one): Can you capitalize HTML
, and make them formatted as paragraphs instead of individual line breaks, and always have punctuation at the end of the sentences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok.
src/components/content.js
Outdated
@@ -698,15 +699,25 @@ class Content extends React.Component { | |||
if (this.props.readOnly) return | |||
if (!this.isInEditor(event.target)) return | |||
|
|||
event.preventDefault() | |||
const handleData = (data) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to refactor this code always have handleData
be called either synchronously, or always on the next tick (setTimeout
), instead of varying depending on the browser. I think this will lead to more predictable debugging of issues/plugins/etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we need the behavior to be synchronous, otherwise we don't have the opportunity to change the pasted content—either to prevent the paste, or to paste using the internal data model.
Is there any way for IE to be fixed without letting the paste occur already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah wait, I think that's why you've got the remove/re-add logic for the node. Does that logic work even though those elements are controlled by React?
In that case I totally get the sync vs. async sometimes. That's unfortunate since it seems like a way for bugs to creep in, but makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that handling the paste event in a async way for IE 11 and synchronously for other browsers increases the complexity and makes debugging more difficult. But I don't know of any other workaround for this.
That logic with the temporary cloned and focused contenteditable element does work and as far as I know, there is no interference with react as the native paste action happens directly on the next tick after the onPaste event fires.
function getHtmlFromNativePaste(component, data, callback) { | ||
const contentNode = findDOMNode(component) | ||
|
||
const clipboardNode = contentNode.cloneNode() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you change these variable names to contentNode -> el
and clipboardNode -> clone
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense.
clipboardNode.setAttribute('class', '') | ||
clipboardNode.setAttribute('style', 'position: fixed; left: -9999px') | ||
|
||
contentNode.parentNode.insertBefore(clipboardNode, contentNode) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be inserted before the other one is removed? Or is there a reason why to not do the switch at the same point in time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be done with appendChild
as well. The original element is not removed, the cloned element is just a safe copy that receives the native paste action so the original element is left untouched.
src/utils/get-transfer-data.js
Outdated
*/ | ||
|
||
function getHtml(transfer) { | ||
return transfer.types && transfer.types.length ? transfer.getData('text/html') || null : null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason that these ones use && transfer.types.length
and the ones above use && transform.types.indexOf(type)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really, I unified this in my refactoring.
src/utils/get-transfer-data.js
Outdated
@@ -81,6 +81,61 @@ function getTransferType(data) { | |||
} | |||
|
|||
/** | |||
* Get fragment from transfers's `data` if possible, otherwise return null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could all of these helpers be replaced by a single getType(transform, type)
helper instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I refactored that.
Hey @ianstormtaylor, thanks a lot for your detailed comments! I adjusted my PR accordingly and gave you some replies - curious to hear what you think. Thank you! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @mahlero looks good, just one last change I think. Thanks for being responsive!
src/components/content.js
Outdated
@@ -698,15 +699,27 @@ class Content extends React.Component { | |||
if (this.props.readOnly) return | |||
if (!this.isInEditor(event.target)) return | |||
|
|||
event.preventDefault() | |||
const data = getTransferData(event.clipboardData) | |||
const handleData = (data) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you refactor this such that handleData
is removed, by pulling these two lines out to above the if
:
data.isShift = !!this.tmp.isShifting
debug('onPaste', { event, data })
And by then just using this.props.onPaste
directly in both places. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem. In what way would you call this.props.onPaste
:
-
from a callback given to the utility function like this
getHtmlFromNativePaste(this, data, dataWithHtml => this.props.onPaste(event, dataWithHtml))
-
or slightly different
getHtmlFromNativePaste(this, htmlData => this.props.onPaste(event, { ...data, htmlData }))
withcallback({ html, type: 'html' })
from inside the utility function -
or
getHtmlFromNativePaste(this, event, data)
and calling directly from inside the utility functionthis.props.onPaste(event, { ...data, html, type: 'html' })
The first two versions still use a 'proxy' callback, in the last version, it's not obvious that there is an asynchronous handling without looking into the utility function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually we could probably have the callback just return html
, and if it's null then have the logic call handled in the component layer. So maybe something along the lines of:
getHtmlFromNativePaste(this, (html) => {
...
})
But even that makes me wonder if we shouldn't be using event.target
instead of passing around this
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your suggestions. I produced a corresponding version and hope we can solve this now!
Thanks for both of your hard work on this- is there any way I can help to get this merged? We could really use this fix! |
Thanks so much for this @mahlero, looks great! Sorry for the slowness. |
Thank you both! |
* imported babel-polyfill to make examples run in IE * Fix for HTML pasting not working in IE * Revert "imported babel-polyfill to make examples run in IE" This reverts commit a840507. * Refactored and corrected comments of fix: HTML pasting not working in IE * Removed handleData and tuned getTransferData call of fix: HTML pasting not working in IE
* ianstormtaylor/master: (61 commits) 0.21.2 alphabetize package.json scripts Speed up getting blocks at a range Remove instanceOf checks to allow Slate objects to be identifiable across module instances (ianstormtaylor#930) Refactor render arrow functions (ianstormtaylor#969) update prosemirror description in readme update prosemirror description in docs update prosemirror comparison in readme 0.21.1 Fix for HTML pasting not working in IE (ianstormtaylor#882) Remove unneeded check. (ianstormtaylor#961) Upgrade react-frame-component. (ianstormtaylor#962) adding forced-layout-example (ianstormtaylor#954) Reckon with inconsistencies between parse5 and native DOMParser (ianstormtaylor#952) Omit version on gzip size badge (ianstormtaylor#947) update issue template and contributing docs add reboo editor to resources add showcase to resources 0.21.0 update changelog ...
Thank you for this amazing stuff and I have noticed 2 bugs (kind of) regarding this fix. 1). Every time when I paste something in the middle (or at the end) of the paragraph it messes up the content of the paragraph. Instead of pasting the new content at the right cursor position, it splits the content and put it in the wrong order. For e.g: Based on the Current Cursor Position(CCP) while pasting we can split the content into contentBeforeCCP and contentAfterCCP. Once we get the new content from the clipboard the right order to insert should be contentBeforeCCP + newClipboardContent + contentAfterCCP. However, it inserts in a wrong order like newClipboardContent + contentAfterCCP + contentBeforeCCP. In Draft.js there is a way to fix this but I don’t know how it works in slate.js. This behavior only occurs in Internet Explorer. 2). Pasting HTML content in IE works fine by just clicking on the right position and then pasting it (renders the HTML content but in the wrong order). However, if I press enter and then paste it on the new line then it pastes the text instead of the HTML. Also, it renders the expected HTML inside a break tag as the last child. I think we need to add browser native support like onPaste to get the HTML content. Which is explained in the link you mentioned: https://stackoverflow.com/questions/2176861/javascript-get-clipboard-data-on-paste-event-cross-browser/6804718#6804718 |
@kumar2013 although I spent a lot of time building an editor based on slate which is fully IE 11 compatible and provides working HTML copy/paste, I never experienced what you describe in 1) and 2). Maybe you could give some more hints and provide some code to explain your use case. |
@mahlero About the point 2). I tried to paste HTML content by pressing enter at the end of the paragraph, this creates a new line and then pasted it. This should create an HTML content instead it created text content. Also, I can see the pasted HTML content separately (hidden inside a span and a break tag) as the last child of the parent element (via inspect element). After copy & paste in a new line: Inspect Element shows the expected HTML content for numbered list and table (hidden inside br tag): This behaviour occurs only in IE 11 (didn't check the lower versions) |
@mahlero I can temporarily fix the point 1). by changing 'P' elements to 'Div' element. About point 2). Every time when I press the enter button it creates a new line (in my case, new div element) which has a 'br' tag inside, this has been received as an argument in getHtmlFromNativePaste(component, callback), here the 'component' receives 'br' tag. Which creates the above-mentioned problem. I have added a check for 'br' tag in your code and that fixes this problem. Please take a look. Let me know if you can reproduce this so that we can get this fixed in the next release, thanks! |
This PR aims at closing the
Pasting HTML doesn't work
issue of #663Basically it works by capturing HTML inserted by the browser's native paste action.
Successfully tested with the Paste HTML example (and with own project) in IE 11.