﻿# 如何利用瀏覽器原生的 Clipboard API 讀寫使用者的剪貼簿資料

我們有時候會在一些網站看到「複製到剪貼簿」功能，它不但可以複製我們肉眼看見的文字，還能複製完整的格式讓你可以貼到 Teams 或 Word 之中，有時還能複製完全客製化的內容，其實這背後都是透過瀏覽器內建的 [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API) 達成的。今天這篇文章我就來梳理一下 Clipboard API 的一些用法與地雷。

### 常見的用法

我們在 Azure DevOps Service 的 Azure Boards 查看 Work Item (工作項目) 時，就有個相當隱密的功能，可以幫助你複製豐富格式的 HTML 到剪貼簿，方便你在 Teams 或其他支援豐富格式的編輯器中貼上(例如 Word、Outlook、Gmail、... 等等)。使用方式大概就是：

1.  先開啟任意 Work Item
2.  滑鼠移到標題的文字輸入框上
3.  此時在文字輸入框最右邊會出現一個「複製工作項目標題」的按鈕
4.  按下去之後就會複製一個擁有 HTML 格式的版本到剪貼簿中！

![複製工作項目標題(Copy work item title)](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/83dbd9c4a77d9f212e5a-221415167-896b1b23-72cc-4fe6-9956-f318d809e842.png)

> 注意: 複製到剪貼簿功能也經常設計成鍵盤快速鍵的形式，如上圖就可以看到用 `Shift+Alt+C` 組合鍵也可以快速複製標題。

若是貼上到文字編輯器，內容會長這樣：

```
Product Backlog Item 42100: 轉換/轉申購
```

若是貼上到 Teams 或 Outlook 郵件中，內容會長這樣：

![Outlook 郵件](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/08d63bcf4e317b28e4e2-221415743-b5fe50a5-75b7-416f-babd-42976cf0215d.png)

還有另一種就是在 GitHub 常會使用到的「複製程式碼到剪貼簿」功能，如下圖示，不過這裡就沒有連同複製豐富格式了，只有「純文字」的程式碼而已：

![GitHub Copy to clipboard](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/977e11d57d3873b7ede8-221416214-f1e5472b-b0c8-490c-93c0-f34ed01cf563.png)

上述範例主要是「寫入剪貼簿」的用法，還有另一種是「讀取剪貼簿」的用法，而且還可以不僅僅讀取文字而已，連剪貼簿中的「圖片」也可以讀到，不過，使用者在第一次使用該功能時，會需要使用者明確授權才能用，所以其實比較少看到有網站實作這個功能，而且並不是所有瀏覽器都有支援「讀取剪貼簿」功能！

> 注意：Azure DevOps Service 的 Azure Boards 有支援在 Work Item 的欄位中按下 `Ctrl-V` 貼上使用者剪貼簿中的圖片，不過這個功能並不是透過 Clipboard API 實現的，而是透過 DOM 的 `paste` 事件實現的([ClipboardEvent](https://developer.mozilla.org/zh-TW/docs/Web/API/ClipboardEvent))，這部分不在本文討論範圍，但你可以輕易的透過 ChatGPT 找到範例程式，咒語是: **Please act as a Senior Frontend developer. I want to paste image or text from user's clipboard to the web page using paste event. How can I deal with these functions?**，中文的咒語也可以: **請扮演一位資深的前端開發人員，我想在網頁上透過 paste 事件讀取使用者的剪貼簿資料，並貼上圖片或文字到網頁中，我該如何透過 JavaScript 完成這些功能？能否給我一些 JavaScript 寫的範例程式。**

### 如何從剪貼簿中讀取資料

你可以透過 `navigator.clipboard.readText()` 直接讀取目前使用者剪貼簿中的「文字」內容，也可以透過 `navigator.clipboard.read()` 讀取使用者剪貼簿中的「任意格式」內容，包含圖片都可以。

> 注意: 目前各瀏覽器對 [Asynchronous Clipboard API](https://caniuse.com/async-clipboard) 的支援度，只有 Firefox 完全不支援「讀取」功能，應該是「安全」考量。除此之外，在手機上的 Safari on iOS 與 Android Browser 也都不支援「讀取」功能！🔥

我們在 MDN 的 [Clipboard.read()](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/read) API 文件中也可以看到 [Clipboard.read() - reading\_image\_data - code sample](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/read) 範例，各位可以體驗一下「從剪貼簿讀取圖片」的感覺。

不過，使用者在你的網站第一次使用「讀取剪貼簿」功能時，會自動跳出一個授權畫面 (如下圖示)，使用者必須同意該網站透過 JS 讀取剪貼簿，程式才能正常執行！

![謮取已複製到剪貼簿的文字和圖片](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/d25228d5bf74abbcd827-221417284-facad960-8811-4173-a332-cf7cca7762b9.png)

以下是一些範例程式：

-   讀取剪貼簿之前先判斷使用者是否有授權
    
    ```javascript
    const permission = await navigator.permissions.query({
      name: "clipboard-read",
    });
    if (permission.state === "denied") {
      throw new Error("Not allowed to read clipboard.");
    }
    ```
    
    > 注意: 在 iframe 中的網頁，預設「不允許」讀取剪貼簿內容，連跳出提示都不會喔！🔥
    
-   讀取「文字」資料
    
    ```javascript
    navigator.clipboard
      .readText()
      .then((clipText) => console.log(clipText));
    ```
    
-   讀取「圖片」資料
    
    ```javascript
    const clipboardContents = await navigator.clipboard.read();
    for (const item of clipboardContents) {
      if (!item.types.includes("image/png")) {
        throw new Error("Clipboard contains non-image data.");
      }
      const blob = await item.getType("image/png");
      destinationImage.src = URL.createObjectURL(blob);
    }
    ```
    
    > 完整範例請參考 MDN 的 [Clipboard.read()](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/read#examples) 文件中的範例。
    

### 如何寫入資料到剪貼簿中

你可以透過 `navigator.clipboard.writeText()` 直接寫入「文字」到目前使用者的剪貼簿中，也可以透過 `navigator.clipboard.write()` 寫入「任意格式內容」到使用者的剪貼簿中，包含圖片都可以。

> 注意: Firefox 僅支援 `navigator.clipboard.writeText()` 寫入「文字」，不支援其他格式寫入，我想應該也是「安全」考量。

就瀏覽器的執行權限來說，只要是使用者正在使用的瀏覽器頁籤，該頁籤上的顯示的網頁預設都可以直接寫入剪貼簿，不需要使用者額外授權就能寫入！所以，一般來說剪貼簿功能都會設計成需要與使用者互動，例如準備一個按鈕讓使用者按下就能複製到剪貼簿，或是透過鍵盤快速鍵來複製等等，不太會設計成頁面載入時就自動寫入使用者的剪貼簿！(因為載入時頁面可能該頁面不在使用者目前的瀏覽器頁籤上)

以下是一些範例程式：

-   寫入「文字」到使用者的剪貼簿
    
    以下範例寫入 `Hello World` 這個字串到使用者剪貼簿中：
    
    ```javascript
    navigator.clipboard.writeText('Hello World').then(
      () => {
        console.log('clipboard successfully set')
      },
      () => {
        console.log('clipboard write failed');
      }
    );
    ```
    
    > 注意: 上述這段程式碼預設無法在 F12 DevTool 中執行，因為你只要一開啟 F12 DevTool 就會離開頁面焦點，導致權限不足，無法使用「寫入剪貼簿」功能。
    
-   寫入「文字」到使用者的剪貼簿 (使用 `navigator.clipboard.write()` API)
    
    ```javascript
    function setClipboard(text) {
      const type = "text/plain";
      const blob = new Blob([text], { type });
      const data = [new ClipboardItem({ [type]: blob })];
    
      navigator.clipboard.write(data).then(
        () => {
          /* success */
        },
        () => {
          /* failure */
        }
      );
    }
    ```
    
-   寫入 HTML 格式到使用者的剪貼簿
    
    ```javascript
    try {
      const content = document.getElementsByClassName('js-output')[0].innerHTML;
      const blobInput = new Blob([content], {type: 'text/html'});
      const clipboardItemInput = new ClipboardItem({'text/html' : blobInput});
      navigator.clipboard.write([clipboardItemInput]);
    } catch(e) {
      // Handle error with user feedback - "Copy failed!" kind of thing
      console.log(e);
    }
    ```
    
    > 參考自 [Copy rich HTML with the native Clipboard API](https://dev.to/stegriff/copy-rich-html-with-the-native-clipboard-api-5ah8)
    
-   複製 Canvas 圖片到使用者的剪貼簿中
    
    ```javascript
    function copyCanvasContentsToClipboard(canvas, onDone, onError) {
      canvas.toBlob((blob) => {
        let data = [new ClipboardItem({ [blob.type]: blob })];
    
        navigator.clipboard.write(data).then(
          () => {
            onDone();
          },
          (err) => {
            onError(err);
          }
        );
      });
    }
    ```
    
-   複製 `<img>` 圖片到使用者的剪貼簿中
    
    ```javascript
    async function copyImageToClipboard() {
      const imageUrl = document.querySelector('img').src;
    
      const response = await fetch(imageUrl);
      const imageData = await response.blob();
    
      const clipboardItem = new ClipboardItem({
        'image/png': imageData
      });
    
      try {
        await navigator.clipboard.write([clipboardItem]);
        console.log('Image copied to clipboard!');
      } catch (error) {
        console.error('Failed to copy image: ', error);
      }
    }
    ```
    

### 無法在 F12 DevTool 中執行 Clipboard API 的地雷

我還記得我第一次在玩 Clipboard API 的時候，一直在 F12 DevTool 中鬼打牆很久，因為怎樣都無法讀/寫剪貼簿資料，就是一定要寫到一份網頁中，不能直接在 F12 DevTool 測試這些 API，錯誤訊息如下：

```
Uncaught (in promise) DOMException: Document is not focused.
```

![Uncaught (in promise) DOMException: Document is not focused.](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/4eb4fd830c7c05a00143-221419416-acdee0ba-988b-4de0-a448-eaa969addcef.png)

解決方法有點深奧，你必須依照以下步驟進行設定才行：

1.  按下 `F12` 開啟 DevTool 視窗
2.  按下 `Escape` 鍵開啟主控台導覽匣
3.  切換到**算繪**(`Rendering`)頁籤
4.  勾選**模擬已聚焦的網頁**(`Emulate a focused page`)

![模擬已聚焦的網頁](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/0e741c8485284c181256-221419633-7c25891c-98de-44d6-8011-e4e57d494af7.png)

設定完成後，你就可以直接在「主控台」直接測試任何 Clipboard API 的效果了！👍

### 相關連結

-   [Clipboard API - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API)
-   [How can I inspect html element that disappears from DOM on lost focus? - Stack Overflow](https://stackoverflow.com/a/63905339/910074)
-   [Copy rich HTML with the native Clipboard API](https://dev.to/stegriff/copy-rich-html-with-the-native-clipboard-api-5ah8)
-   [Unblocking clipboard access](https://web.dev/articles/async-clipboard?hl=en)
