Platforms tested:
- Chrome 61.0.3163.100 (macOS 10.13.0)
- Safari 11.0 (macOS 10.13)
- Safari 11.0 (iOS 11.0 on an iPhone SE)
- Edge 15.15063 (Windows 10.0 in a VirtualBox VM)
- Firefox 54.0 (macOS 10.13)
Chrome 61 | Safari 11 (macOS) | Safari 11 (iOS) | Edge 15 | Firefox 54 | |
---|---|---|---|---|---|
supported always returns true † |
✅ | ✅ | ✅ | ✅ | ✅ |
enabled without selection returns true † |
❌ | ❌ | ❌ | ❌ | ✅ |
exec works without selection † |
✅ | ✅ | ✅ | ||
enabled with selection returns true † |
✅ | ✅ | ✅ | ✅ | ✅ |
exec works with selection † |
✅ | ✅ | ✅ | ✅ | ✅ |
exec fails outside user gesture |
✅ | ✅ | ✅ | ✅ | ✅ |
setData() in listener works |
✅ | ✅ | ❌ ² | ✅ | ✅ |
getData() in listener shows if setData() worked |
✅ | ✅ | ❌ ³ | ✅ | |
Copies all types set with setData() |
✅ | ✅ | ✅ | ❌ ⁴ | ✅ |
exec reports success correctly |
✅ | ✅ | ❌ ⁵ | ✅ | |
contenteditable does not break document selection |
❌ | ❌ | ❌ | ✅ | ✅ |
user-select: none does not break document selection |
✅(Cr 64) | ❌ | ❌ | ✅(Edge 16) | ✅ (FF 57) |
Can construct new DataTransfer() |
✅ | ❌ | ❌ | ❌ | ❌ |
Writes CF_HTML on Windows |
✅ | N/A | N/A | ❌⁶ | ✅ |
† Here, we are only specifically interested in the case where the handler is called directly in response to a user gesture. I didn't test for behaviour when there is no user gesture.
- ¹
document.execCommand("copy")
triggers a successul copy action, but listeners for the document'scopy
event aren't fired. WebKit Bug #177715 - ² WebKit Bug #177715
- ³ Edge Bug #14110451
- ⁴ Edge Bug #14080506
- ⁵ Edge Bug #14080262
- ⁶ Edge Bug #14372529, GitHub issue #73
In all browsers, document.queryCommandSupported("copy")
always returns true.
When nothing on the page is selected, document.queryCommandEnabled("copy")
returns true in Firefox, but not any other browsers.
On all platforms, document.execCommand("copy")
always works (triggers a copy) during a user gesture, regardless of whether anything on the page is selected. However, on Safari listeners registered using document.addEventListener("copy")
don't fire (and therefore don't have an opportunity to set the data on the clipboard) if there is no selection.
On all browsers, document.queryCommandEnabled("copy")
returns true during a user gesture if some part of the page is selected (doesn't matter which part; can be the entire body or a single element). The selection may be made using Javascript during the user gesture handler itself.
On all platforms, document.execCommand("copy")
works during a user gesture, regardless of whether anything on the page is selected. Listeners registered with document.addEventListener("copy")
fire.
In all browsers, document.execCommand("copy")
fails when there is no user gesture, and returns false
.
This means that the following works:
document.addEventListener("copy", function(e) {
e.clipboardData.setData("text/plain", "plain text")
e.preventDefault();
});
On iOS, the setData
call doesn't work – it actually empties the clipboard (at least for that data type). This is supposedly fixed in WebKit as of September 19, 2017: https://bugs.webkit.org/show_bug.cgi?id=177715
Fortunately, it is possible to detect Safari's behaviour (when the value is not empty), because the following returns ""
even after the setData()
call:
e.clipboardData.getData("text/plain", "plain text")
In Edge, setData()
works inside the copy listener, but getData()
never reports the data that was set, and returns the empty string instead.
Note that on iOS Safari, getData()
also returns the empty string, but since setData()
doesn't work, this is the correct return value (and can be used to detect if setting a non-empty string succeeded).
This means that the following listeners put both plain text and HTML on the clipboard:
document.addEventListener("copy", function(e) {
e.clipboardData.setData("text/plain", "plain text")
e.clipboardData.setData("text/html", "<b>markup</b> text")
e.preventDefault();
});
document.addEventListener("copy", function(e) {
e.clipboardData.setData("text/html", "<b>markup</b> text")
e.clipboardData.setData("text/plain", "plain text")
e.preventDefault();
});
Edge only places the last provided data type on the clipboard.
Most platforms correctly report if document.execCommand("copy")
successfully copied something to the clipboard.
On iOS, document.execCommand("copy")
also returns true
when event.clipboardData.setData()
clears the clipboard. In this case, the clipboard is set to empty, but the return value is arguably correct once we account for the relevant bug.
Edge, however, always returns false
. Even when the copy attempt succeeds.
Consider the following code:
var sel = document.getSelection();
var range = document.createRange();
range.selectNodeContents(document.body);
sel.addRange(range);
This fails in Chrome and Safari if the last content in the DOM is the following:
<div contenteditable="true" class="editable"></div>
In Safari, the DOM selection API does not allow Javascript to select parts of the DOM that are not selectable by the user due to -webkit-user-select: none
.
Reported at #75
As a workaround for Safari, it is possible to select an element nested unside an unselectable element that explicitly uses -webkit-user-select: text
to enable selection. It seems that we should be able to rely on this, since it is the specified behaviour. However, note that other browsers (e.g. Firefox <21) have implemented behaviour that doesn't match the spec.
In Edge 16 and earlier, clipboardData.setData("text/html", data)
does not properly write HTML to the clipbard in the Windows CF_HTML
clipboard format.
Reported at #73
The new asynchronous clipboard API takes a DataTranfer
input. However, the only browser in which you can call the DataTransfer
constructor is Chrome. (The constructor was made publicly callable specifically for the async clipboard API.)
Firstly:
- Issue 1:
queryCommandEnabled()
doesn't tell us when copying will work.- Workaround: Don't consult
queryCommandEnabled()
; just tryexecCommand()
every time.
- Workaround: Don't consult
All platforms except iOS can share the same default implementation. However:
- Issue 2: Edge will only put the last provided data type on the clipboard.
- Workaround: File a bug against Edge. (Started: Edge Bug #14080506)
- Document that the caller should add the most important data type to the copy data last.
TODO: Add "Safari doesn't trigger listener without selection" issue.
iOS Safari requires the trickiest fallback:
- Issue 3: For iOS Safari, it seems we can't attach data types in the listener at all.
- Workaround: Detect the issue, and fall back to copying the
text/plain
data type with a different mechanism. - Document that callers should always provide a
text/plain
data type if they want copying to work on iOS.
- Workaround: Detect the issue, and fall back to copying the
The logic will be as follows:
- Is there a
text/plain
data type in the input?- No? ⇒ No fallback. Clipboard will likely end up blank on iOS. (Consider warning the user if they don't provide a value for the
text/plain
data type.) - Yes? ⇒ Check
setData()
againstgetData()
for thetext/plain
data type. Do they match?- Yes? ⇒ Do nothing. (This will result in a blank clipboard when the copied string is empty.)
- No? ⇒ Fall back.
- No? ⇒ No fallback. Clipboard will likely end up blank on iOS. (Consider warning the user if they don't provide a value for the
We fall back creating a temporary DOM element, assigning the text/plain
value to it using textContent
, selecting it using Javascript, and triggering execCommand("copy")
again. (The repeated copy command appears to work on iOS.) We will place the element within a shadow root in order to prevent outside formatting (e.g. page background color) from affecting the text, and use white-space: pre-wrap
to preserve newlines and whitespace. However:
- Issue 4: On iOS, the copied text will still have the explicit formatting style of the default text in shadow root (issue 3)
- Workaround: none.
- Document this.
The Windows problem looks a bit annoying.
On Windows, we perform the copy, but we will always get back false
.
- Issue 5: On Windows,
execCommand("copy")
always returns false.- Workaround 0: Report this bug to Edge, and hope they fix it. (Started: Edge Bug #14080262)
- Workaround 1: Pass on the return value blindly, and document that Windows has a bug.
- Workaround 2: Never check the return value of
execCommand("copy")
- Workaround 3: Detect Edge using a different mechanism (e.g. UA sniffing), and ignore the return value only when we think we're in Edge.
We also need to add some more polyfilling than we might like:
-
Issue 6: The caller can't construct a
DataTransfer
to pass to the polyfill on any platform except Chrome.- Workaround: Provide an object with a sufficiently ergonomic subset of the interface of
DataTransfer
that the caller can use. (We can swap out the implementation withDataTransfer
once platforms allow calling the constructor directly.)
- Workaround: Provide an object with a sufficiently ergonomic subset of the interface of
-
Issue 7: Internet Explorer did its own thing.
- Workaround: old implementation using
window.clipboardData
. Requires aPromise
polyfill. :-/
- Workaround: old implementation using