UNPKG

clipboard-polyfill

Version:

A polyfill for the asynchronous clipboard API

192 lines (124 loc) 11.1 kB
# 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. :-/