The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

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

我們有時候會在一些網站看到「複製到剪貼簿」功能,它不但可以複製我們肉眼看見的文字,還能複製完整的格式讓你可以貼到 Teams 或 Word 之中,有時還能複製完全客製化的內容,其實這背後都是透過瀏覽器內建的 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)

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

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

Product Backlog Item 42100: 轉換/轉申購

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

Outlook 郵件

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

GitHub Copy to clipboard

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

注意:Azure DevOps Service 的 Azure Boards 有支援在 Work Item 的欄位中按下 Ctrl-V 貼上使用者剪貼簿中的圖片,不過這個功能並不是透過 Clipboard API 實現的,而是透過 DOM 的 paste 事件實現的(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 的支援度,只有 Firefox 完全不支援「讀取」功能,應該是「安全」考量。除此之外,在手機上的 Safari on iOS 與 Android Browser 也都不支援「讀取」功能!🔥

我們在 MDN 的 Clipboard.read() API 文件中也可以看到 Clipboard.read() - reading_image_data - code sample 範例,各位可以體驗一下「從剪貼簿讀取圖片」的感覺。

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

謮取已複製到剪貼簿的文字和圖片

以下是一些範例程式:

  • 讀取剪貼簿之前先判斷使用者是否有授權

    const permission = await navigator.permissions.query({
      name: "clipboard-read",
    });
    if (permission.state === "denied") {
      throw new Error("Not allowed to read clipboard.");
    }
    

    注意: 在 iframe 中的網頁,預設「不允許」讀取剪貼簿內容,連跳出提示都不會喔!🔥

  • 讀取「文字」資料

    navigator.clipboard
      .readText()
      .then((clipText) => console.log(clipText));
    
  • 讀取「圖片」資料

    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() 文件中的範例。

如何寫入資料到剪貼簿中

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

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

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

以下是一些範例程式:

  • 寫入「文字」到使用者的剪貼簿

    以下範例寫入 Hello World 這個字串到使用者剪貼簿中:

    navigator.clipboard.writeText('Hello World').then(
      () => {
        console.log('clipboard successfully set')
      },
      () => {
        console.log('clipboard write failed');
      }
    );
    

    注意: 上述這段程式碼預設無法在 F12 DevTool 中執行,因為你只要一開啟 F12 DevTool 就會離開頁面焦點,導致權限不足,無法使用「寫入剪貼簿」功能。

  • 寫入「文字」到使用者的剪貼簿 (使用 navigator.clipboard.write() API)

    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 格式到使用者的剪貼簿

    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

  • 複製 Canvas 圖片到使用者的剪貼簿中

    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> 圖片到使用者的剪貼簿中

    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.

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

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

模擬已聚焦的網頁

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

相關連結

留言評論