clipboard-polyfill
Version:
A polyfill for the asynchronous clipboard API
192 lines (124 loc) • 11.1 kB
Markdown
# Copying
# Test results
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's `copy` event aren't fired. [WebKit Bug #177715](https://bugs.webkit.org/show_bug.cgi?id=156529)
- ² [WebKit Bug #177715](https://bugs.webkit.org/show_bug.cgi?id=177715)
- ³ [Edge Bug #14110451](https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14110451/)
- ⁴ [Edge Bug #14080506](https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14080506/)
- ⁵ [Edge Bug #14080262](https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14080262/)
- ⁶ [Edge Bug #14372529](https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14372529/), [GitHub issue #73](https://github.com/lgarron/clipboard-polyfill/issues/73)
## `supported` always returns true
In all browsers, `document.queryCommandSupported("copy")` always returns true.
## `enabled` **without** selection returns true (see issue 1 below)
When nothing on the page is selected, `document.queryCommandEnabled("copy")` returns true in Firefox, but not any other browsers.
## `exec` fires listener **without** selection
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.
## `enabled` **with** selection returns true
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.
## `exec` fires listener **with** selection
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.
## `enabled` returns false outside user gesture
In all browsers, `document.execCommand("copy")` fails when there is no user gesture, and returns `false`.
## `setData()` works in listener (see issue 3 and issue 4 below)
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")
## `getData()` in listener shows if `setData()` worked
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).
## Copies all types set with `setData()` (see issue 2 below)
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.
## `exec` reports success correctly (see issue 5 below)
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.
## `contenteditable` does not break document seleciton
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>
## `user-select: none` does not break document selection
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 https://github.com/lgarron/clipboard-polyfill/issues/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](https://drafts.csswg.org/css-ui-4/#valdef-user-select-none). However, note that other browsers (e.g. [Firefox <21](https://developer.mozilla.org/en-US/docs/Web/CSS/user-select)) have implemented behaviour that doesn't match the spec.
## Writes `CF_HTML` on Windows
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 https://github.com/lgarron/clipboard-polyfill/issues/73
## Can construct `new DataTransfer()` (see issue 6 below)
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.)
# Strategy
Firstly:
- **Issue 1**: `queryCommandEnabled()` doesn't tell us when copying will work.
- Workaround: Don't consult `queryCommandEnabled()`; just try `execCommand()` every time.
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](https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/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.
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()` against `getData()` for the `text/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.
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](https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/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 with `DataTransfer` once platforms allow calling the constructor directly.)
- **Issue 7**: Internet Explorer did its own thing.
- Workaround: [old implementation](https://github.com/lgarron/clipboard-polyfill/blob/94c9df4aa2ce1ca1b08280bf36923b65648d9f72/clipboard-polyfill#L167) using `window.clipboardData`. Requires a `Promise` polyfill. :-/