> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/javascript-xi-lie/10-fu-zhi-nei-rong-dao-jian-tie-ban.md).

# 复制内容到剪贴板

JavaScript 有两种方式可以让浏览器与系统剪贴板交互：`Document.execCommand()` 方法以及现代的异步的 Clipboard API。

## document.execCommand()

`document.execCommand()` 方法允许运行命令来操纵**可编辑内容区域**（即元素的 `contenteditable` 属性值为true）的元素。

```java
document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
```

该方法返回布尔值，表示操作是否被支持或启用。

* **aCommandName：** 字符串，命令的名称：copy、cut、delete、forwardDelete（删除光标所在位置的字符）、paste、selectAll（选中编辑区里的全部内容）等等。
* **aShowDefaultUI：** 布尔值， 是否展示用户界面，一般为 false。（Mozilla 没有实现）。
* **aValueArgument：** 一些命令需要的额外参数。

`document.execCommand()` 方法常用指令：

```javascript
// 各种浏览器都支持：复制、剪切和粘贴这三个操作
document.execCommand('copy')
document.execCommand('cut')
document.execCommand('paste')
```

> **已废弃：** `document.execCommand()` 特性已经过时了。 虽然它可能仍然在一些浏览器中工作，但它的使用是不鼓励的，因为它可能随时被删除。 尽量避免使用它。

### 复制操作

`document.execCommand('copy')` 命令用于将选中的文本内容写入剪贴板。该命令无需任何特别的权限就可以使用。

```javascript
const inputDom = document.querySelector('#input')
inputDom.select()
document.execCommand('copy')
```

默认情况下，`document.execCommand('copy')` 只能将选中的文本内容写入到剪贴板。以下方法实现写入任意文本到剪贴板：

```js
function fnExecCommandCopy(str) {
    let inputDom = document.createElement("input")
    document.body.appendChild(inputDom)
    inputDom.value = str
    inputDom.focus()
    inputDom.select()
    if (document.execCommand("copy")) {
        console.log("复制成功")
    } else {
        console.log("不支持复制")
    }
    inputDom.blur()
    document.body.removeChild(inputDom)
}
```

**注意：** 复制操作最好放在事件监听函数里面，由用户触发（比如用户点击按钮）。如果脚本自主执行，某些浏览器可能会报错。

**注意：** `document.execCommand('copy')` 是同步操作，如果复制/粘贴大量数据，页面会出现卡顿。有些浏览器还会跳出提示框，要求用户许可，这时在用户做出选择前，页面会失去响应。

### 剪切操作

`document.execCommand('cut')` 命令用于将选中的文本内容写入剪贴板，同时删除选中的内容。

```javascript
const inputDom = document.querySelector('#input')
inputDom.select()
document.execCommand('cut')
```

### 粘贴操作

`document.execCommand('paste')` 命令用于将剪贴板里面的文本内容取出，输出到当前的焦点元素中。

```javascript
const inputDom = document.querySelector('#input')
inputDom.focus()
document.execCommand('paste')
```

**注意：** 出于安全考虑，浏览器通常不允许 `document.execCommand('paste')` 访问剪贴板，因为它可能包含敏感信息。因此，该命令一般是不启效的。

## Clipboard API

Clipboard 接口实现了 Clipboard API，如果用户授予了相应的权限，就能提供系统剪贴板的读写访问。Clipboard 对象是通过 `navigator.clipboard` 属性返回，所有剪贴板操作都通过这个对象进行。

该 API 被设计用来取代使用 `document.execCommand()` 的剪贴板访问方式。

使用 Clipboard API 需要注意以下问题：

* 异步的 Clipboard API 方法是一个近期新增的规范，并且这个规范可能不适用于所有浏览器。
* Chrome 浏览器规定，只有 HTTPS 协议的页面才能使用这个 API。不过，开发环境（localhost）允许使用非加密协议。
* 调用时需要明确获得用户的许可。权限的具体实现使用了 Permissions API，跟剪贴板相关的有两个权限： `clipboard-read` 和 `clipboard-write` 。`clipboard-write` 自动授予脚本，而 `clipboard-read` 必须用户明确同意给予。也就是说，写入剪贴板，脚本可以自动完成，但是读取剪贴板时，浏览器会弹出一个对话框，询问用户是否同意读取。
* \*\*脚本读取的总是当前页面的剪贴板。\*\*也就是说，如果把相关的代码粘贴到开发者工具中直接运行，可能会报错，因为这时的当前页面是开发者工具的窗口，而不是网页页面。

### Permissions

Permissions API 提供以下方法：

* **query()：** 返回给定 API 的用户权限状态。
* **request()：** 请求使用给定 API 的权限。**目前任何浏览器都不支持此功能。**
* **requestAll()：** 请求使用一组给定 API 的权限。**目前任何浏览器都不支持此功能。**
* **revoke()：** 撤销当前在给定 API 上设置的权限。

以下是查看是否有 `clipboard-read` 权限的方法：

```javascript
navigator.permissions.query({name:'clipboard-read'}).then(function(result) {
    if (result.state === 'granted') {
        console.log('granted')
        return
    }
    if (result.state === 'prompt') {
        console.log('prompt')
        return
    }
    console.log('fail')
})
```

遗憾的是，目前为止，浏览器还不支持 `Permissions.request()`、`Permissions.requestAll()` 这两个申请权限的方法。我们可以尝试用 Chrome 插件来开放 Clipboard 权限。

### 复制：将文本写入剪贴板

使用 Clipboard API 的 `writeText()` 或 `write()` 方法来将数据写入剪贴板。Clipboard API 比 execCommand 更加灵活，因为你不仅可以将当前选中的内容写入到剪贴板，还可以指定要写入剪贴板的内容。

> \*\*注意：\*\*请先确认是否有 Permissions API 的 `clipboardWrite` 权限。因为这是一个新的 API，不同浏览器的具体要求不同。

`writeText()` 方法用于写入特定字符串到操作系统的剪贴板：

```javascript
navigator.clipboard.writeText("霁光浮瓦碧参差。").then((res) => {
    console.log('writeText：写入成功！')
}).catch((err) => {
    console.log(err)
})
```

`write()` 方法用于将任意数据写入剪贴板，可以是文本数据，也可以是二进制数据，如图片。

**需要注意的是：** `write()` 方法的传参必需一个 ClipboardItem 实例。

```javascript
navigator.clipboard.write([
    new ClipboardItem({
        "text/plain": new Blob(['要复制的文本'], { type: "text/plain" })
    })
]).then((res) => {
    console.log('write：写入成功！')
}).catch((err) => {
    console.log(err)
})
```

```javascript
const image = await fetch('tupian_27781.png').then(response => response.blob())
const text = new Blob(['一夕轻雷落万丝，\n霁光浮瓦碧参差。\n有情芍药含春泪，\n无力蔷薇卧晓枝。'], {type: 'text/plain'})
const data = new ClipboardItem({
    'text/plain': text,
    'image/png': image
})
navigator.clipboard.write([data]).then(() => {
    console.log('write：写入成功！')
}).catch((err) => {
    console.log(err)
})
```

### 粘贴：从剪贴板中读取文本

使用 Clipboard API 的 `readText()` 和 `read()` 方法可从剪贴板读取任意文本或二进制数据。

`readText()` 方法用于读取剪贴板的文本数据。注意：在尝试读取剪贴板的内容时，浏览器会提醒用户是否允许该请求：

```javascript
navigator.clipboard.readText().then((res) => {
    console.log(`readText：${res}`)
}).catch((err) => {
    console.log(err)
})
```

`read()` 方法用于读取剪贴板的数据，与 `readText()` 的不同是，该方法可以读取剪贴板中的任意数据，如图片：

```javascript
navigator.clipboard.read().then(data => {
    data.forEach(item => {
        item.types.forEach(async (type) => {
            let readRes = await item.getType(type)
            switch(type) {
                case 'text/plain': {
                    let tx = await readRes.text()
                    console.log(tx)
                    break
                }
                case 'image/png': {
                    console.log(URL.createObjectURL(readRes))
                    break
                }
                default: {
                    console.log('未处理的数据类型!')
                    break
                }
            }
        })
    })
}).catch(err => {
    console.error('无法读取剪贴板: ', err)
})
```

[查看剪贴板读写DEMO](https://1927344728.github.io/demo-lizh/html/others_01.html?type=8)

## 剪贴板事件

剪贴板有3个操作： 复制、剪切、粘贴。这3个操作共对应下列 6 个剪贴板事件：

* **copy：** 复制时触发；
* **cut：** 剪切时触发；
* **paste：** 粘贴时触发；
* **beforecopy：** 复制前触发；
* **beforecut：** 剪切前触发；
* **beforepaste：** 粘贴前触发。

**注意：** 后三个事件，浏览器不一定支持。比如：Chrome 只支持 `beforecopy`、`beforecut`，不支持 `beforepaste`。

通常，我们只需要关注前三个事件即可：

```javascript
document.addEventListener('copy', (event) => {});
document.addEventListener('cut', (event) => {});
document.addEventListener('paste', (event) => {});
```

这三个事件的触发条件：

* 快捷键： `ctrl+c`、`ctrl+x`、`ctrl+v` 操作。
* 鼠标右键菜单：复制、剪切、粘贴操作。
* execCommand：`copy`、`cut`、`paste` 指令。

**特别注意：** 这三个事件在使用异步 Clipboard API 访问剪贴板时**不会**触发。

在剪贴板事件中，可以调用 `event.preventDefault()` 取消剪贴板操作，并且剪贴板 `event` 对象有一个 `clipboardData` 属性，它是一个 [`DataTransfer`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer) 对象。它允许我们覆盖将写入剪贴板的数据，让我们有机会以其他格式写入数据，例如 `text/html`：

```javascript
document.addEventListener('copy', (event) => {
    event.preventDefault()
    event.clipboardData.setData('text/plain', 'COPY ME!!!')
    event.clipboardData.setData('text/html', '<p>COPY ME!!!</p>')
})
```

`event.clipboardData` 有以下属性和方法：

* **setData(type, data)：** 修改剪贴板数据，需要指定数据类型。
* **getData(type)：** 获取剪贴板数据，需要指定数据类型。
* **clearData(\[type])：** 清除剪贴板数据，可以指定数据类型。如果不指定类型，将清除所有类型的数据。
* **items：** 一个类似数组的对象，包含了所有剪贴项，不过通常只有一个剪贴项。

## 参考资料

[MDN Clipboard](https://developer.mozilla.org/zh-CN/docs/Web/API/Clipboard)

[MDN Interact with the clipboard](https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard)

[MDN Permissions](https://developer.mozilla.org/en-US/docs/Web/API/Permissions)

[MDN DataTransfer](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer)

[异步剪贴板 API：使用 JavaScript 访问剪贴板](https://arnellebalane.com/blog/async-clipboard-api/)

[阮一峰 剪贴板操作 Clipboard API 教程](https://www.ruanyifeng.com/blog/2021/01/clipboard-api.html)
