clipboard.js源碼解析與實踐

日常業(yè)務(wù)中,會常常用到拷貝、剪切的需求,此外一些針對C端的平臺復(fù)制內(nèi)容下面會新增一段版權(quán)信息,那么這些都是如何實現(xiàn)的呢?
其實是用的window.execCommand方法,該方法允許運行命令來操作可編輯區(qū)域的元素,執(zhí)行系統(tǒng)的copy命令或者cut命令,實現(xiàn)拷貝和剪切內(nèi)容到系統(tǒng)剪切板中。window.execCommand可以執(zhí)行copycut外,還有其他的命令可參考如下表格:

命令 描述
backColor 修改文檔的背景顏色。在 styleWithCss 模式下,則只影響容器元素的背景顏色。這需要一個<color> 類型的字符串值作為參數(shù)傳入。注意,IE 瀏覽器用這個設(shè)置文字的背景顏色。
bold 開啟或關(guān)閉選中文字或插入點的粗體字效果。IE 瀏覽器使用 <strong>標(biāo)簽,而不是<b>標(biāo)簽。
ClearAuthenticationCache 清除緩存中的所有身份驗證憑據(jù)。
contentReadOnly 通過傳入一個布爾類型的參數(shù)來使能文檔內(nèi)容的可編輯性。(IE 瀏覽器不支持)
copy 拷貝當(dāng)前選中內(nèi)容到剪貼板。啟用這個功能的條件因瀏覽器不同而不同,而且不同時期,其啟用條件也不盡相同。使用之前請檢查瀏覽器兼容表,以確定是否可用。
createLink 將選中內(nèi)容創(chuàng)建為一個錨鏈接。這個命令需要一個hrefURI 字符串作為參數(shù)值傳入。URI 必須包含至少一個字符,例如一個空格。(瀏覽器會創(chuàng)建一個空鏈接)
cut 剪貼當(dāng)前選中的文字并復(fù)制到剪貼板。啟用這個功能的條件因瀏覽器不同而不同,而且不同時期,其啟用條件也不盡相同。使用之前請檢查瀏覽器兼容表,以確定是否可用。
decreaseFontSize 給選中文字加上 <small> 標(biāo)簽,或在選中點插入該標(biāo)簽。(IE 瀏覽器不支持)
defaultParagraphSeparator 更改在可編輯文本區(qū)域中創(chuàng)建新段落時使用的段落分隔符。有關(guān)更多詳細(xì)信息,請參閱標(biāo)記生成的差異。
delete 刪除選中部分。
enableAbsolutePositionEditor 啟用或禁用允許移動絕對定位元素的抓取器。Firefox 63 Beta/Dev Edition 默認(rèn)禁用此功能 (bug 1449564)。
enableInlineTableEditing 啟用或禁用表格行和列插入和刪除控件。(IE 瀏覽器不支持)
enableObjectResizing 啟用或禁用圖像和其他對象的大小可調(diào)整大小手柄。(IE 瀏覽器不支持)
fontName 在插入點或者選中文字部分修改字體名稱。需要提供一個字體名稱字符串 (例如:"Arial") 作為參數(shù)。
fontSize 在插入點或者選中文字部分修改字體大小。需要提供一個 HTML 字體尺寸 (1-7) 作為參數(shù)。
foreColor 在插入點或者選中文字部分修改字體顏色。需要提供一個顏色值字符串作為參數(shù)。
formatBlock 添加一個 HTML 塊式標(biāo)簽在包含當(dāng)前選擇的行,如果已經(jīng)存在了,更換包含該行的塊元素 (在 Firefox 中,BLOCKQUOTE 是一個例外 -它將包含任何包含塊元素). 需要提供一個標(biāo)簽名稱字符串作為參數(shù)。幾乎所有的塊樣式標(biāo)簽都可以使用 (例如。"H1", "P", "DL", "BLOCKQUOTE"). (IE 瀏覽器僅僅支持標(biāo)題標(biāo)簽 H1 - H6, ADDRESS,和 PRE,使用時還必須包含標(biāo)簽分隔符 < >, 例如 < H1 >
forwardDelete 刪除光標(biāo)所在位置的字符。和按下刪除鍵一樣。
heading 添加一個標(biāo)題標(biāo)簽在光標(biāo)處或者所選文字上。需要提供標(biāo)簽名稱字符串作為參數(shù)(例如:"H1"、"H6")(IE 和 Safari 不支持)
hiliteColor 更改選擇或插入點的背景顏色。需要一個顏色值字符串作為值參數(shù)傳遞。UseCSS 必須開啟此功能。(IE 瀏覽器不支持)
increaseFontSize 在選擇或插入點周圍添加一個 BIG 標(biāo)簽。(IE 瀏覽器不支持)
indent 縮進(jìn)選擇或插入點所在的行,在 Firefox 中,如果選擇多行,但是這些行存在不同級別的縮進(jìn),只有縮進(jìn)最少的行被縮進(jìn)。
insertBrOnReturn 控制當(dāng)按下 Enter 鍵時,是插入 br 標(biāo)簽還是把當(dāng)前塊元素變成兩個。(IE 瀏覽器不支持)
insertHorizontalRule 在插入點插入一個水平線(刪除選中的部分)
insertHTML 在插入點插入一個 HTML 字符串(刪除選中的部分)。需要一個個 HTML 字符串作為參數(shù)。(IE 瀏覽器不支持)
insertImage 在插入點插入一張圖片(刪除選中的部分)。需要一個 URL 字符串作為參數(shù)。這個 URL 圖片地址至少包含一個字符??瞻鬃址部梢裕↖E 會創(chuàng)建一個鏈接其值為 null)
insertOrderedList 在插入點或者選中文字上創(chuàng)建一個有序列表
insertUnorderedList 在插入點或者選中文字上創(chuàng)建一個無序列表。
insertParagraph 在選擇或當(dāng)前行周圍插入一個段落。(IE 會在插入點插入一個段落并刪除選中的部分.)
insertText 在光標(biāo)插入位置插入文本內(nèi)容或者覆蓋所選的文本內(nèi)容。
italic 在光標(biāo)插入點開啟或關(guān)閉斜體字。 (Internet Explorer 使用 EM 標(biāo)簽,而不是 I )
justifyCenter 對光標(biāo)插入位置或者所選內(nèi)容進(jìn)行文字居中。
justifyFull 對光標(biāo)插入位置或者所選內(nèi)容進(jìn)行文本對齊。
justifyLeft 對光標(biāo)插入位置或者所選內(nèi)容進(jìn)行左對齊。
justifyRight 對光標(biāo)插入位置或者所選內(nèi)容進(jìn)行右對齊。
outdent 對光標(biāo)插入行或者所選行內(nèi)容減少縮進(jìn)量。
paste 在光標(biāo)位置粘貼剪貼板的內(nèi)容,如果有被選中的內(nèi)容,會被替換。剪貼板功能必須在 user.js 配置文件中啟用。參閱 [1].
redo 重做被撤銷的操作。
removeFormat 對所選內(nèi)容去除所有格式
selectAll 選中編輯區(qū)里的全部內(nèi)容。
strikeThrough 在光標(biāo)插入點開啟或關(guān)閉刪除線。
subscript 在光標(biāo)插入點開啟或關(guān)閉下角標(biāo)。
superscript 在光標(biāo)插入點開啟或關(guān)閉上角標(biāo)。
underline 在光標(biāo)插入點開啟或關(guān)閉下劃線。
undo 撤銷最近執(zhí)行的命令。
unlink 去除所選的錨鏈接的<a>標(biāo)簽
styleWithCSS 用這個取代 useCSS 命令。參數(shù)如預(yù)期的那樣工作,i.e. true modifies/generates 風(fēng)格的標(biāo)記屬性,false 生成格式化元素。
AutoUrlDetect 更改瀏覽器自動鏈接行為(僅 IE 瀏覽器支持)

document.execCommand可以做的事情很多,但是需要說明的是它在最新的web標(biāo)準(zhǔn)中已經(jīng)被廢棄。來看一下MDN的介紹:

已棄用: 不再推薦使用該特性。雖然一些瀏覽器仍然支持它,但也許已從相關(guān)的 web 標(biāo)準(zhǔn)中移除,也許正準(zhǔn)備移除或出于兼容性而保留。請盡量不要使用該特性,并更新現(xiàn)有的代碼;參見本頁面底部的兼容性表格以指導(dǎo)你作出決定。請注意,該特性隨時可能無法正常工作。

瀏覽器兼容性——取自MDN

WX20221026-172742.png

上面只展示了幾個屬性,可以看到,copycut是全部瀏覽器都支持的,此外還有pasteselectAll等等,在使用的時候最好做一下判斷,其他的命令基本都已不支持,就不推薦再使用了。

介紹完document.execCommand,接下來我們準(zhǔn)備”上菜“!

有請今天的主角登場 —— clipboardjs

基本使用

首先,我們來看一下clipboardjs的使用方式:

<!DOCTYPE html>
<html lang="en">

 <head>
   <meta charset="UTF-8" />
   <title>target-div</title>
   <meta name="viewport" content="width=device-width, initial-scale=1" />
 </head>

 <body>
   <!-- 1. Define some markup -->
   <div>hello</div>
   <button class="btn" data-clipboard-action="copy" data-clipboard-target="div">
     Copy
   </button>

   <!-- 2. Include library -->
   <script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.10/clipboard.min.js"></script>

   <!-- 3. Instantiate clipboard -->
   <script>
     const clipboard = new ClipboardJS('.btn');

     clipboard.on('success', function (e) {
       console.info('Action:', e.action);
       console.info('Text:', e.text);
       console.info('Trigger:', e.trigger);
     });

     clipboard.on('error', function (e) {
       console.info('Action:', e.action);
       console.info('Text:', e.text);
       console.info('Trigger:', e.trigger);
     });
   </script>
 </body>

</html>
   

點擊button按鈕之后,控制臺打印如下內(nèi)容:

image.png
  • Action:執(zhí)行命令
  • Text:放入到剪切板的內(nèi)容
  • Trigger:觸發(fā)的節(jié)點

源碼解析

可以邊參考源碼邊看:點擊查看源碼

打開源碼進(jìn)入clipboardjs頁面,可以看到定義了一個Clipboard類并繼承于EmitterEmitter是負(fù)責(zé)訂閱命令執(zhí)行之后剪切板操作成功和失敗的關(guān)鍵,用到了Emitteronemit。

tiny-emitter是一個簡單實現(xiàn)發(fā)布訂閱的包(查看tiny-emitter源碼),在函數(shù)的原型對象內(nèi)定義了ononce、emit、off四個原型方法,具體的實現(xiàn)非常簡單,代碼量很少,這里不做過多介紹,感興趣可以看tiny-emiter源碼。

import Emitter from 'tiny-emitter';

class Clipboard extends Emitter {
  /**
   * @param {String|HTMLElement|HTMLCollection|NodeList} trigger
   * @param {Object} options
   */
  constructor(trigger, options) {
    super();

    this.resolveOptions(options);
    this.listenClick(trigger);
  }

  // 省略...
}

構(gòu)造函數(shù)內(nèi)有resolveOptionslistenClick方法分別是處理參數(shù)和監(jiān)聽點擊事件。我們來一一分析。

resolveOption

resolveOptions(options = {}) {
    this.action =
      typeof options.action === 'function'
        ? options.action
        : this.defaultAction;
    this.target =
      typeof options.target === 'function'
        ? options.target
        : this.defaultTarget;
    this.text =
      typeof options.text === 'function' ? options.text : this.defaultText;
    this.container =
      typeof options.container === 'object' ? options.container : document.body;
  }

resolveOptions函數(shù)對this.action、this.targetthis.text、this.container進(jìn)行處理取值。

當(dāng)不想在元素中寫如data-clipboard-XXXX的自定義屬性的時候,actions、targettext都允許通過配置項的方式聲明一個函數(shù),做自己的業(yè)務(wù)需求,最后返回一個值。所以在上面我們會看到,是通過typeof判斷是否為function類型,如果不是函數(shù)類型則取默認(rèn)值,默認(rèn)值則是通過自定義屬性進(jìn)行匹配,格式如:data-clipboard-XXXX

元素的data-clipboard-XXXX會有函數(shù)專門處理并讀取對應(yīng)的自定義屬性值:

/**
* Helper function to retrieve attribute value.
* @param {String} suffix
* @param {Element} element
*/
function getAttributeValue(suffix, element) {
   const attribute = `data-clipboard-${suffix}`;

   if (!element.hasAttribute(attribute)) {
       return;
   }
   return element.getAttribute(attribute);
}   

下面來解釋一下每個屬性的含義:

  1. action是執(zhí)行的方法,可以是copy復(fù)制,也可以是cut剪切。
  2. target是目標(biāo)錨點,如data-clipboard-target="div"中的target就是div,此外也可以是className類名或者#id節(jié)點id,最終是通過document.querySelector(selector);來獲取節(jié)點。
  3. text是選中的文本內(nèi)容
  4. container是要將目標(biāo)節(jié)點插入的父級根節(jié)點位置,可以根據(jù)實際業(yè)務(wù)需求配置,比如在vue中我們希望插入的是#app容器內(nèi),則可以傳入document.getElementById('app'),否則默認(rèn)是插入到document.body

以上就是resolveOptions所做的工作。下面來講解一下this.listenClick函數(shù)。

listenClick

import listen from 'good-listener';
   
/**
* Adds a click event listener to the passed trigger.
* @param {String|HTMLElement|HTMLCollection|NodeList} trigger
*/
listenClick(trigger) {
   this.listener = listen(trigger, 'click', (e) => this.onClick(e));
}

listenClick函數(shù)負(fù)責(zé)監(jiān)聽click點擊事件。會遍歷所有的節(jié)點,給節(jié)點添加監(jiān)聽事件,這個是通過good-listener插件實現(xiàn),插件是由作者本人寫的,實現(xiàn)原理很簡單,通過檢查trigger是什么類型

  • 節(jié)點(node):直接給節(jié)點添加listen監(jiān)聽事件
    function listenNode(node, type, callback) {
        node.addEventListener(type, callback);
    
        return {
            destroy: function() {
                node.removeEventListener(type, callback);
            }
        }
    }
    
  • 節(jié)點列表(nodeList):會遍歷列表,然后給每個節(jié)點添加監(jiān)聽事件
    function listenNodeList(nodeList, type, callback) {
        Array.prototype.forEach.call(nodeList, function(node) {
            node.addEventListener(type, callback);
        });
    
        return {
            destroy: function() {
                Array.prototype.forEach.call(nodeList, function(node) {
                    node.removeEventListener(type, callback);
                });
            }
        }
    }
    
  • 字符串 :傳入的是字符串則直接監(jiān)聽document.body
    // 傳入是字符串
    function listenSelector(selector, type, callback) {
        return delegate(document.body, selector, type, callback);
    }
    // 監(jiān)聽document.body
    function delegate(element, selector, type, callback, useCapture) {
        var listenerFn = listener.apply(this, arguments);
        element.addEventListener(type, listenerFn, useCapture);
        return {
            destroy: function() {
                element.removeEventListener(type, listenerFn, useCapture);
            }
        }
    }
    

查看源碼

回到listenClick函數(shù),當(dāng)我們點擊之后會觸發(fā)執(zhí)行this.onClick函數(shù),會觸發(fā)執(zhí)行剪切板命令,最后返回選中的文本內(nèi)容,利用Emitter.emit發(fā)布結(jié)果。我們可以通過clipboard.on('success')訂閱其結(jié)果。這個就是繼承于tiny-emitter的作用。

/**
* Defines a new `ClipboardAction` on each click event.
* @param {Event} e
*/
onClick(e) {
    const trigger = e.delegateTarget || e.currentTarget;
    const action = this.action(trigger) || 'copy';
    const text = ClipboardActionDefault({
      action,
      container: this.container,
      target: this.target(trigger),
      text: this.text(trigger),
    });

    // Fires an event based on the copy operation result.
    this.emit(text ? 'success' : 'error', {
      action,
      text,
      trigger,
      clearSelection() {
        if (trigger) {
          trigger.focus();
        }
        window.getSelection().removeAllRanges();
      },
    });
}

實例化ClipboardAction后,創(chuàng)建一個監(jiān)聽事件,會監(jiān)聽所有點擊行為,當(dāng)監(jiān)聽到某一個節(jié)點點擊,利用節(jié)點的Evente.delegateTarget || e.currentTarget取得目標(biāo)節(jié)點內(nèi)容對象信息。根據(jù)trigger獲取到action,this.action可能是對象,也可能是字符串,當(dāng)this.action不是對象的時候會異常,然后直接賦值copy

text是由ClipboardActionDefault函數(shù)得到,我們來看看它做了什么處理。

import ClipboardActionCut from './cut';
import ClipboardActionCopy from './copy';

const ClipboardActionDefault = (options = {}) => {
  const { action = 'copy', container, target, text } = options;
    
  // 省略action和target的邊界處理代碼... 
    
  // 定義基于“文本”屬性的選擇策略。
  if (text) {
    return ClipboardActionCopy(text, { container });
  }
  // 定義基于' target '屬性的選擇策略。
  if (target) {
    return action === 'cut'
      ? ClipboardActionCut(target)
      : ClipboardActionCopy(target, { container });
  }
};

根據(jù)resolveOptions函數(shù)對this.textthis.target的處理,我們可以知道,它們有兩種數(shù)據(jù)類型,一種是函數(shù)(typeof === function)類型,一種是通過data-clipboard-xxxx自定義屬性獲取的值。那么如果text沒有聲明function或者在節(jié)點中聲明自定義屬性值,那么它將不會通過if(text)的判斷,直接跳過,往下執(zhí)行。隨后是target,由于前面做了邊界異常處理,所以這里作為兜底執(zhí)行,根據(jù)cutcopy分配給兩個不同的函數(shù)處理,這里我們只介紹copy,因為cutcopy僅有執(zhí)行命令document.execCommand執(zhí)行的差異。

我們來看看ClipboardActionCopy做的工作:

import select from 'select';
import command from '../common/command';
import createFakeElement from '../common/create-fake-element';

/**
 * Create fake copy action wrapper using a fake element.
 * @param {String} target
 * @param {Object} options
 * @return {String}
 */
const fakeCopyAction = (value, options) => {
  const fakeElement = createFakeElement(value);
  options.container.appendChild(fakeElement);
  const selectedText = select(fakeElement);
  command('copy');
  fakeElement.remove();

  return selectedText;
};

/**
 * Copy action wrapper.
 * @param {String|HTMLElement} target
 * @param {Object} options
 * @return {String}
 */
const ClipboardActionCopy = (
  target,
  options = { container: document.body }
) => {
  let selectedText = '';
  if (typeof target === 'string') {
    selectedText = fakeCopyAction(target, options);
  } else if (
    target instanceof HTMLInputElement &&
    !['text', 'search', 'url', 'tel', 'password'].includes(target?.type)
  ) {
    // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange
    selectedText = fakeCopyAction(target.value, options);
  } else {
    selectedText = select(target);
    command('copy');
  }
  return selectedText;
};

export default ClipboardActionCopy;

先來看ClipboardActionCopy函數(shù),針對三種類型做了判斷,分別是

  1. 是字符串類型,說明目標(biāo)是一個純文本,就需要額外創(chuàng)建一個textarea標(biāo)簽,為什么不是input標(biāo)簽?zāi)??是因為對于換行文本內(nèi)容使用textarea會更友好,如果使用input標(biāo)簽,那么需要拷貝的是textarea多行文本內(nèi)容則會發(fā)生格式異常的問題,因為最終是需要將選中的文本進(jìn)行input.value = value這樣賦值的,多行變成單行,而反之使用textarea支持拷貝input和它自身類型節(jié)點。
  2. 第二種是節(jié)點類型,不為text', 'search', 'url', 'tel', 'password'這幾種類型之一的,均通過創(chuàng)建textarea的方式進(jìn)行拷貝。
  3. 第三種是為text', 'search', 'url', 'tel'類型其中之一的直接執(zhí)行document.execCommand('copy')指令。

上面fakeCopyAction方法內(nèi)有一個createFakeElement函數(shù),它是負(fù)責(zé)創(chuàng)建textarae的,我們來看看作者是如何寫的:

/**
* 創(chuàng)建一個帶有值的textarea元素。
* @param {String} value
* @return {HTMLElement}
*/
export default function createFakeElement(value) {
 const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
 const fakeElement = document.createElement('textarea');
 // 防止在IOS上縮放
 fakeElement.style.fontSize = '12pt';
 // 重寫盒子模型
 fakeElement.style.border = '0';
 fakeElement.style.padding = '0';
 fakeElement.style.margin = '0';
 // 設(shè)置絕對定位,將元素移動出屏幕外面
 fakeElement.style.position = 'absolute';
 fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px';
 // 垂直移動到屏幕相同位置
 let yPosition = window.pageYOffset || document.documentElement.scrollTop;
 fakeElement.style.top = `${yPosition}px`;

 fakeElement.setAttribute('readonly', '');
 fakeElement.value = value;

 return fakeElement;
}

源碼很簡單,就是創(chuàng)建textarea標(biāo)簽,為了防止在IOS中縮放,設(shè)置了字體大小為12pt,隨手重寫了盒子模型border、paddingmargin三個的樣式,主要目的為了限制用戶惡意設(shè)置全局盒子模型樣式。然后將元素移出到屏幕以外,這里用到了isRTL來判斷是移動至屏幕左邊還是屏幕右邊,我們看到上面判斷了頁面元素中是否存在dir屬性,該屬性是否為rtl,它有什么意義呢?

說到rtl就需要說到另外一個ltr,它們是完全相反的關(guān)系,過去會在頁面中寫dir標(biāo)簽,但是現(xiàn)在已經(jīng)不贊成使用了,感興趣的可以自行查閱對應(yīng)資料,這里簡單的說說rtlltr的區(qū)別:

元素 LTR RTL
文本 句子從左向右閱讀 句子從有向左閱讀
時間線 事件序列從左向右進(jìn)行 事件序列從右向左進(jìn)行
圖像 從左向右的運動 從右向左運動

回到上面代碼,設(shè)置了標(biāo)簽擺放的位置后,需要將元素設(shè)置為可編輯狀態(tài)才行,所以取消了readonly,隨后給textarea標(biāo)簽賦值,最終把處理的元素返回。

以上剪切板功能就實現(xiàn)了,那么我們需要訂閱剪切板是成功還是失敗改怎么辦,tiny-emitter的發(fā)布訂閱功能就派上用場啦!

我們來看onClick函數(shù)下的this.emit就是做的這個工作。把actions、texttrigger、clearSelection四個屬性暴露出去,我們就可以實現(xiàn)訂閱。前面三個前面有介紹過了,這里介紹一下clearSelection屬性,它的職責(zé)是負(fù)責(zé)清理內(nèi)容選中光標(biāo)的,還記得前面講fakeCopyAction函數(shù)中用到了select(fakeElement),這里的select也是作者自己寫的插件,插件很簡單才43行代碼,實現(xiàn)原理是就是對選中的內(nèi)容的元素設(shè)置focus聚集光標(biāo),感興趣可以看源碼

至此,源碼就解析完了。

簡化版hook

了解了原理之后,我們能不能自己實現(xiàn)一個簡單的剪切板功能呢?能在vue3、react中直接使用的hook

要實現(xiàn)剪切板最核心就只有兩點:

  1. 創(chuàng)建textarea標(biāo)簽,并把需要添加剪切板的內(nèi)容賦值給該標(biāo)簽,隨后將其聚焦選中,最后添加到剪貼板后,移除該節(jié)點。
  2. 執(zhí)行document.execCommand拷貝到剪切板。

了解了核心之后,我們就來寫一個簡單的版本。剪切板執(zhí)行完畢之后,我們給一個回調(diào)讓用戶做其他業(yè)務(wù)邏輯。

代碼如下:

 function useClipboard(value,cb) {
      const fakeElement = document.createElement('textarea');
      // 防止在IOS上縮放
      fakeElement.style.fontSize = '12pt';
      // 重寫盒子模型
      fakeElement.style.border = '0';
      fakeElement.style.padding = '0';
      fakeElement.style.margin = '0';
      // 設(shè)置絕對定位,將標(biāo)簽移動出屏幕外面
      fakeElement.style.position = 'absolute';
      fakeElement.style.right = '-9999px';
      // 垂直移動到屏幕相同位置
      let yPosition = window.pageYOffset || document.documentElement.scrollTop;
      fakeElement.style.top = `${yPosition}px`;
       // 設(shè)置為可讀
      fakeElement.setAttribute('readonly', '');
      // 賦值
      fakeElement.value = value;
      // 添加到根節(jié)點
      document.body.appendChild(fakeElement);
      // 選中內(nèi)容
      fakeElement.select();
      // 檢查命令是否支持
      if(document.execCommand('copy')){
        document.execCommand('copy')
        // 執(zhí)行回調(diào)
        typeof cb === 'function' && cb()
      } else {
        alert("系統(tǒng)不支持,請手動復(fù)制!")
      }
      // 拷貝到剪切板之后,移除自身標(biāo)簽
      fakeElement.remove()
}

使用方法

<script lang="ts" setup>
import { useClipboard } from "composable/index"
    
const handleCopy = (text: string): void => {
   useClipboard(text, () => {
        console.log("復(fù)制成功啦")
   })
}
</script>

LeetCode、CSDN網(wǎng)站復(fù)制內(nèi)容長度比較長的時候,都會添加版權(quán)信息在底部是,這個是如何實現(xiàn)的?

其實也很簡單,檢查復(fù)制的內(nèi)容長度是否超出閾值,超出則添加版權(quán)信息。

<script lang="ts" setup>
import { useClipboard } from "composable/index"
    
const handleCopy = (text: string): void => {
   let url = window.location.href;
   let newText = text
  // 如果超出規(guī)定長度,則添加版權(quán)信息
  if (text.length >= 50) {
    newText =`${text}
  
      原文出自 smallzip - url 轉(zhuǎn)載請保留原文鏈接
      `
  }
   useClipboard(newText, () => {
        console.log("復(fù)制成功啦")
   })
}
</script>

兼容與降級

由于document.execCommand存在兼容問題,那么我該如何檢查是否兼容呢?

  1. 通過執(zhí)行命令查看返回值是為true還是false
      if (document.execCommand("copy")) {
            document.execCommand("copy")
        } else {
            window.alert("當(dāng)前系統(tǒng)不支持復(fù)制操作~")
        }
    
  1. document.execCommand相關(guān)的一個API可以檢查當(dāng)前運行的瀏覽器是否兼容 —— document.queryCommandSupported,它會檢查執(zhí)行的命令是否支持。

    if(document.queryCommandSupported && document.queryCommandSupported('copy')){
       document.execCommand("copy")
    } else {
       window.alert("當(dāng)前系統(tǒng)不支持復(fù)制操作~")    
    }
    

    當(dāng)然我可以封裝成一個函數(shù),來專門判斷是否支持當(dāng)前瀏覽器,更方便業(yè)務(wù)調(diào)用:

    export function isSupported(action = ['copy', 'cut']) {
      const actions = typeof action === 'string' ? [action] : action;
      let support = !!document.queryCommandSupported;
    
      actions.forEach((action) => {
        support = support && !!document.queryCommandSupported(action);
      });
    
      return support;
    }
    
    // 使用方式
    isSupported("copy")
    isSupported(["copy", "cut", "selectAll"])
    
    1. 使用Navigator.cliporad,這個是新的剪切板指令,兼容全部瀏覽器。推薦使用該方法,支持異步讀寫。下面貼一段來自MDN的介紹,更信息請自行查閱。

    剪貼板 Clipboard APINavigator 接口添加了只讀屬性 clipboard,該屬性返回一個可以讀寫剪切板內(nèi)容的 Clipboard 對象。在 Web 應(yīng)用中,剪切板 API 可用于實現(xiàn)剪切、復(fù)制、粘貼的功能。
    只有在用戶事先授予網(wǎng)站或應(yīng)用對剪切板的訪問許可之后,才能使用異步剪切板讀寫方法。許可操作必須通過取得權(quán)限 Permissions API"clipboard-read""clipboard-write" 獲得。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容