clipboard.js源碼解析與實(shí)踐

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

命令 描述
backColor 修改文檔的背景顏色晌杰。在 styleWithCss 模式下跷睦,則只影響容器元素的背景顏色。這需要一個<color> 類型的字符串值作為參數(shù)傳入肋演。注意抑诸,IE 瀏覽器用這個設(shè)置文字的背景顏色。
bold 開啟或關(guān)閉選中文字或插入點(diǎn)的粗體字效果爹殊。IE 瀏覽器使用 <strong>標(biāo)簽蜕乡,而不是<b>標(biāo)簽。
ClearAuthenticationCache 清除緩存中的所有身份驗(yàn)證憑據(jù)梗夸。
contentReadOnly 通過傳入一個布爾類型的參數(shù)來使能文檔內(nèi)容的可編輯性层玲。(IE 瀏覽器不支持)
copy 拷貝當(dāng)前選中內(nèi)容到剪貼板。啟用這個功能的條件因?yàn)g覽器不同而不同反症,而且不同時期辛块,其啟用條件也不盡相同。使用之前請檢查瀏覽器兼容表铅碍,以確定是否可用润绵。
createLink 將選中內(nèi)容創(chuàng)建為一個錨鏈接。這個命令需要一個hrefURI 字符串作為參數(shù)值傳入该酗。URI 必須包含至少一個字符授药,例如一個空格士嚎。(瀏覽器會創(chuàng)建一個空鏈接)
cut 剪貼當(dāng)前選中的文字并復(fù)制到剪貼板呜魄。啟用這個功能的條件因?yàn)g覽器不同而不同,而且不同時期莱衩,其啟用條件也不盡相同爵嗅。使用之前請檢查瀏覽器兼容表,以確定是否可用笨蚁。
decreaseFontSize 給選中文字加上 <small> 標(biāo)簽睹晒,或在選中點(diǎn)插入該標(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 在插入點(diǎn)或者選中文字部分修改字體名稱拖云。需要提供一個字體名稱字符串 (例如:"Arial") 作為參數(shù)。
fontSize 在插入點(diǎn)或者選中文字部分修改字體大小应又。需要提供一個 HTML 字體尺寸 (1-7) 作為參數(shù)宙项。
foreColor 在插入點(diǎn)或者選中文字部分修改字體顏色。需要提供一個顏色值字符串作為參數(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 更改選擇或插入點(diǎn)的背景顏色躁染。需要一個顏色值字符串作為值參數(shù)傳遞鸣哀。UseCSS 必須開啟此功能。(IE 瀏覽器不支持)
increaseFontSize 在選擇或插入點(diǎn)周圍添加一個 BIG 標(biāo)簽吞彤。(IE 瀏覽器不支持)
indent 縮進(jìn)選擇或插入點(diǎn)所在的行我衬,在 Firefox 中,如果選擇多行饰恕,但是這些行存在不同級別的縮進(jìn)挠羔,只有縮進(jìn)最少的行被縮進(jìn)。
insertBrOnReturn 控制當(dāng)按下 Enter 鍵時埋嵌,是插入 br 標(biāo)簽還是把當(dāng)前塊元素變成兩個破加。(IE 瀏覽器不支持)
insertHorizontalRule 在插入點(diǎn)插入一個水平線(刪除選中的部分)
insertHTML 在插入點(diǎn)插入一個 HTML 字符串(刪除選中的部分)。需要一個個 HTML 字符串作為參數(shù)雹嗦。(IE 瀏覽器不支持)
insertImage 在插入點(diǎn)插入一張圖片(刪除選中的部分)范舀。需要一個 URL 字符串作為參數(shù)合是。這個 URL 圖片地址至少包含一個字符《Щ罚空白字符也可以(IE 會創(chuàng)建一個鏈接其值為 null)
insertOrderedList 在插入點(diǎn)或者選中文字上創(chuàng)建一個有序列表
insertUnorderedList 在插入點(diǎn)或者選中文字上創(chuàng)建一個無序列表聪全。
insertParagraph 在選擇或當(dāng)前行周圍插入一個段落。(IE 會在插入點(diǎn)插入一個段落并刪除選中的部分.)
insertText 在光標(biāo)插入位置插入文本內(nèi)容或者覆蓋所選的文本內(nèi)容辅辩。
italic 在光標(biāo)插入點(diǎn)開啟或關(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)插入點(diǎn)開啟或關(guān)閉刪除線。
subscript 在光標(biāo)插入點(diǎn)開啟或關(guān)閉下角標(biāo)获搏。
superscript 在光標(biāo)插入點(diǎn)開啟或關(guān)閉上角標(biāo)赖条。
underline 在光標(biāo)插入點(diǎn)開啟或關(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是全部瀏覽器都支持的歧譬,此外還有paste涂圆,selectAll等等,在使用的時候最好做一下判斷硝逢,其他的命令基本都已不支持姨拥,就不推薦再使用了。

介紹完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>
   

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

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

源碼解析

可以邊參考源碼邊看:點(diǎn)擊查看源碼

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

tiny-emitter是一個簡單實(shí)現(xiàn)發(fā)布訂閱的包(查看tiny-emitter源碼),在函數(shù)的原型對象內(nèi)定義了on那婉、once板甘、emitoff四個原型方法详炬,具體的實(shí)現(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)聽點(diǎ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.actionthis.target礼仗、this.text吐咳、this.container進(jìn)行處理取值。

當(dāng)不想在元素中寫如data-clipboard-XXXX的自定義屬性的時候元践,actions韭脊、targettext都允許通過配置項(xiàng)的方式聲明一個函數(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)錨點(diǎn)冻押,如data-clipboard-target="div"中的target就是div,此外也可以是className類名或者#id節(jié)點(diǎn)id盛嘿,最終是通過document.querySelector(selector);來獲取節(jié)點(diǎn)洛巢。
  3. text是選中的文本內(nèi)容
  4. container是要將目標(biāo)節(jié)點(diǎn)插入的父級根節(jié)點(diǎn)位置,可以根據(jù)實(shí)際業(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點(diǎn)擊事件。會遍歷所有的節(jié)點(diǎn)蚤认,給節(jié)點(diǎn)添加監(jiān)聽事件米苹,這個是通過good-listener插件實(shí)現(xiàn),插件是由作者本人寫的砰琢,實(shí)現(xiàn)原理很簡單蘸嘶,通過檢查trigger是什么類型

  • 節(jié)點(diǎn)(node):直接給節(jié)點(diǎn)添加listen監(jiān)聽事件
    function listenNode(node, type, callback) {
        node.addEventListener(type, callback);
    
        return {
            destroy: function() {
                node.removeEventListener(type, callback);
            }
        }
    }
    
  • 節(jié)點(diǎn)列表(nodeList):會遍歷列表,然后給每個節(jié)點(diǎn)添加監(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)我們點(diǎn)擊之后會觸發(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();
      },
    });
}

實(shí)例化ClipboardAction后澳骤,創(chuàng)建一個監(jiān)聽事件,會監(jiān)聽所有點(diǎn)擊行為澜薄,當(dāng)監(jiān)聽到某一個節(jié)點(diǎn)點(diǎn)擊为肮,利用節(jié)點(diǎn)的Evente.delegateTarget || e.currentTarget取得目標(biāo)節(jié)點(diǎn)內(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é)點(diǎn)中聲明自定義屬性值,那么它將不會通過if(text)的判斷若债,直接跳過符相,往下執(zhí)行。隨后是target蠢琳,由于前面做了邊界異常處理啊终,所以這里作為兜底執(zhí)行,根據(jù)cutcopy分配給兩個不同的函數(shù)處理傲须,這里我們只介紹copy蓝牲,因?yàn)?code>cut和copy僅有執(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)兀渴且驗(yàn)閷τ趽Q行文本內(nèi)容使用textarea會更友好累澡,如果使用input標(biāo)簽梦抢,那么需要拷貝的是textarea多行文本內(nèi)容則會發(fā)生格式異常的問題,因?yàn)樽罱K是需要將選中的文本進(jìn)行input.value = value這樣賦值的愧哟,多行變成單行奥吩,而反之使用textarea支持拷貝input和它自身類型節(jié)點(diǎn)。
  2. 第二種是節(jié)點(diǎn)類型翅雏,不為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名挥,隨手重寫了盒子模型borderpadding主守、 margin三個的樣式禀倔,主要目的為了限制用戶惡意設(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)行
圖像 從左向右的運(yùn)動 從右向左運(yùn)動

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

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

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

至此,源碼就解析完了晚岭。

簡化版hook

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

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

  1. 創(chuàng)建textarea標(biāo)簽库说,并把需要添加剪切板的內(nèi)容賦值給該標(biāo)簽,隨后將其聚焦選中燎竖,最后添加到剪貼板后璃弄,移除該節(jié)點(diǎn)要销。
  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é)點(diǎn)
      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)信息在底部是卵牍,這個是如何實(shí)現(xiàn)的果港?

其實(shí)也很簡單,檢查復(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)前運(yùn)行的瀏覽器是否兼容 —— 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 可用于實(shí)現(xiàn)剪切即碗、復(fù)制焰情、粘貼的功能。
    只有在用戶事先授予網(wǎng)站或應(yīng)用對剪切板的訪問許可之后剥懒,才能使用異步剪切板讀寫方法内舟。許可操作必須通過取得權(quán)限 Permissions API"clipboard-read""clipboard-write" 獲得。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末初橘,一起剝皮案震驚了整個濱河市验游,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌保檐,老刑警劉巖耕蝉,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異夜只,居然都是意外死亡垒在,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門扔亥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來场躯,“玉大人,你說我怎么就攤上這事旅挤√吖兀” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵谦铃,是天一觀的道長耘成。 經(jīng)常有香客問我,道長驹闰,這世上最難降的妖魔是什么瘪菌? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮嘹朗,結(jié)果婚禮上师妙,老公的妹妹穿的比我還像新娘。我一直安慰自己屹培,他們只是感情好默穴,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布怔檩。 她就那樣靜靜地躺著,像睡著了一般蓄诽。 火紅的嫁衣襯著肌膚如雪薛训。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天仑氛,我揣著相機(jī)與錄音乙埃,去河邊找鬼。 笑死锯岖,一個胖子當(dāng)著我的面吹牛介袜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播出吹,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼遇伞,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了捶牢?” 一聲冷哼從身側(cè)響起鸠珠,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎叫确,沒想到半個月后跳芳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡竹勉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了娄琉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片次乓。...
    茶點(diǎn)故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖孽水,靈堂內(nèi)的尸體忽然破棺而出票腰,到底是詐尸還是另有隱情,我是刑警寧澤女气,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布杏慰,位于F島的核電站,受9級特大地震影響炼鞠,放射性物質(zhì)發(fā)生泄漏缘滥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一谒主、第九天 我趴在偏房一處隱蔽的房頂上張望朝扼。 院中可真熱鬧,春花似錦霎肯、人聲如沸擎颖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搂捧。三九已至驮俗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間允跑,已是汗流浹背意述。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吮蛹,地道東北人荤崇。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像潮针,于是被迫代替她去往敵國和親术荤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容