clipboard.js原始碼解析-複製到剪下板外掛

sihai發表於2018-11-12

clipboard.js 是一個小型的複製到剪下板外掛,只有3kb,非flash

1.前言

公司專案有用到clipboard.js,由於好奇心順手點開了原始碼看看其究竟是如何實現的,本以為是九曲十八彎錯綜複雜,其實還是挺容易看懂的,所以就分享下讀後感哈哈。

本篇讀後感分為五部分,分別為前言、使用、解析、demo、總結,五部分互不相連可根據需要分開看。

前言為介紹、使用為庫的使用、解析為原始碼的解析、demo是抽取原始碼的核心實現的小demo,總結為吹水,學以致用。

建議跟著原始碼結合本文閱讀,這樣更加容易理解!

  1. clipboard.js
  2. clipboard.js解析的Github地址

2.使用

在閱讀原始碼之前最好先了解其用法,有助於理解某些詭異的原始碼為何這樣寫。(下面是clipboard.js作者的demo)

<button class="btn">Copy</button>
<div>hello</div>

<script>
var clipboard = new ClipboardJS('.btn', {
    target: function() {
        return document.querySelector('div');
    }
});

clipboard.on('success', function(e) {
    console.log(e);
});

clipboard.on('error', function(e) {
    console.log(e);
});
</script>
複製程式碼

從作者給出的demo可以看到,點選btn後複製了div為hello的值,可以看成三步:

  1. 卡卡西(demo的btn)
  2. 複製(demo的ClipboardJS)
  3. 忍術(demo的div)

即拆解核心:trigger(卡卡西)target(忍術) 進行 copy(複製)

2.1 trigger

把trigger傳遞給ClipboardJS函式,函式接受三種型別

  1. dom元素
  2. nodeList
  3. 選擇器
<!-- 1.dom元素 -->
<div id="btn" data-clipboard-text="1"></div>

<script>
var btn = document.getElementById('btn');
var clipboard = new ClipboardJS(btn);
</script>

<!-- 2.nodeList -->
<button data-clipboard-text="1">Copy</button>
<button data-clipboard-text="2">Copy</button>
<button data-clipboard-text="3">Copy</button>

<script>
var btns = document.querySelectorAll('button');
var clipboard = new ClipboardJS(btns);
</script>

<!-- 3.選擇器 -->
<button class="btn" data-clipboard-text="1">Copy</button>
<button class="btn" data-clipboard-text="2">Copy</button>
<button class="btn" data-clipboard-text="3">Copy</button>

<script>
var clipboard = new ClipboardJS('.btn');
</script>
複製程式碼

2.2 target

target的目的是為了獲取複製的值(text),所以target不一定是dom。獲取text有兩種方式

  1. trigger屬性賦值
  2. target元素獲取
<!-- 1.trigger屬性賦值  data-clipboard-text -->
<button class="btn" data-clipboard-text="1">Copy</button>
<button class="btn" data-clipboard-text="2">Copy</button>
<button class="btn" data-clipboard-text="3">Copy</button>

<script>
var clipboard = new ClipboardJS('.btn');
</script>

<!-- 2.target物件獲取值 text -->
<button class="btn">Copy</button>
<div>hello</div>

<script>
var clipboard = new ClipboardJS('.btn', {
    target: function() {
        return document.querySelector('div');
    }
});
</script>

<!-- 2.target物件獲取值 value -->
<input id="foo" type="text" value="hello">
<button class="btn" data-clipboard-action="copy" data-clipboard-target="#foo">Copy</button>

<script>
var clipboard = new ClipboardJS('.btn');
</script>
複製程式碼

2.3 copy(預設copy / cut)

<!-- 1.複製:預設copy -->
<button class="btn">Copy</button>
<div>hello</div>

<script>
var clipboard = new ClipboardJS('.btn', {
    target: function() {
        return document.querySelector('div');
    }
});
</script>

<!-- 2.剪下:cut -->
<textarea id="bar">hello</textarea>
<button class="btn" data-clipboard-action="cut" data-clipboard-target="#bar">Cut</button>

<script>
var clipboard = new ClipboardJS('.btn');
</script>
複製程式碼

3.解析

原始碼主要包含兩個核心檔案clipboard.js和clipboard-action.js,但還需瞭解tiny-emitter.js。

  1. tiny-emitter.js:事件發射器,相當於鉤子,處理複製的回撥函式
  2. clipboard.js:處理複製所需的引數
  3. clipboard-action.js:複製的核心邏輯

3.1 tiny-emitter.js

tiny-emitter 是一個小型(小於1k)事件發射器(相當於node的events.EventEmitter)

你肯定很奇怪為什麼第一個解析的不是clipboard.js而是tiny-emitter.js,先看用法。

<div id="btn" data-clipboard-text="1">
    <span>Copy</span>
</div>

<script>
var btn = document.getElementById('btn');
var clipboard = new ClipboardJS(btn);

// tiny-emitter.js的作用,處理當複製成功或者失敗後的回撥函式
clipboard.on('success', function(data) {
    console.log(data);
});

clipboard.on('error', function(data) {
    console.log(data);
});
</script>
複製程式碼

既然定義了事件,原始碼在哪裡觸發事件觸發器的呢?從他的標識(success | error)自然而然的想到,是複製這個操作之後才觸發的。我們先來簡單看看clipboard-action.js裡的emit方法的程式碼,不影響後續的閱讀

class ClipboardAction{
  /**
   * 根據複製操作的結果觸發對應發射器
   * @param {Boolean} succeeded 複製操作後的返回值,用於判斷複製是否成功
   */
  handleResult(succeeded) {
      // 這裡this.emitter.emit相當於E.emit
      this.emitter.emit(succeeded ? 'success' : 'error', {
          action: this.action,
          text: this.selectedText,
          trigger: this.trigger,
          clearSelection: this.clearSelection.bind(this)
      });
  }
}
複製程式碼

clipboard.js中使用了tiny-emitter.js的onemit方法。tiny-emitter.js宣告一個物件(this.e),(success | error)定義標識,on方法用來新增該標識事件,emit方法用來標識發射事件。舉例:你是一個古代的皇帝,在開朝之初就招了一批後宮佳麗(on方法),某天你想檢查身體,就讓公公向後宮傳遞一個訊號(emit方法),就能雨露均沾了。

function E () {}
/**
 * @param {String} name 觸發事件的表識
 * @param {function} callback 觸發的事件
 * @param {object} ctx 函式呼叫上下文
 */
E.prototype = {
  on: function (name, callback, ctx) {
    // this.e儲存全域性事件
    var e = this.e || (this.e = {});
    
    // this.e的結構
    // this.e = {
    //   success: [
    //     {fn: callback, ctx: ctx}
    //   ],
    //   error: [...]
    // }
    
    (e[name] || (e[name] = [])).push({
        fn: callback,
        ctx: ctx
    });

    return this;
  },
  emit: function (name) {
        // 獲取標識後的引數,就是上面this.emitter.emit函式第二個引數物件{action, text, trigger, clearSelection}
        // 最終從回撥函式中獲取data。E.on(success, (data) => data) 
        var data = [].slice.call(arguments, 1);
        
        // 獲取標識對應的函式
        var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
        var i = 0;
        var len = evtArr.length;
    
        for (i; i < len; i++) {
          // 迴圈觸發函式陣列的函式,把data傳遞出去作為on的回撥函式的結果
          evtArr[i].fn.apply(evtArr[i].ctx, data);
        }
    
        return this;
    }
};
複製程式碼

簡單理解就是tiny-emitter.js內部維護了一個物件(this.e),this.e物件用記錄一系列的屬性(例如:success、error),屬性是陣列,當呼叫on方法往對應屬性的陣列新增觸發函式,呼叫emit方法就觸發對應屬性的所有函式

3.2 clipboard.js

clipboard.js主要由clipboard.js和clipboard-action.js組成。clipboard.js主要負責對接收傳遞進來的引數,並組裝成clipboard-action.js所需要的資料結構。clipboard-action.js就是複製的核心庫,負責複製的實現,我們先來看看clipboard.js

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);
    }
}
複製程式碼

從上面原始碼可以看到,Clipboard繼承自EmitterEmitter就是tiny-emitter.js的方法。而Clipboard初始化時有兩個步驟

  1. 格式化傳遞進來的引數
  2. 為目標元素新增點選事件,並進行復制操作

我們先看resolveOptions函式(注意區分trigger元素和target物件,trigger元素是用來繫結click事件的元素,target物件是複製的物件。也就是上面拆解核心:trigger(卡卡西)target(忍術) 進行 copy(複製)

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);
    }

    /**
     * 定義函式的屬性,如果外部有傳函式,使用外部的函式,否則使用內部的預設函式
     * @param {Object} options
     */
    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;
    }

    /**
     * 定義行為的回撥函式
     * @param {Element} trigger
     */
    defaultAction(trigger) {
        return getAttributeValue('action', trigger);
    }

    /**
     * 定義複製目標的回撥函式
     * @param {Element} trigger
     */
    defaultTarget(trigger) {
        const selector = getAttributeValue('target', trigger);

        if (selector) {
            return document.querySelector(selector);
        }
    }

    /**
     * 定義複製內容的回撥函式
     * @param {Element} trigger
     */
    defaultText(trigger) {
        return getAttributeValue('text', trigger);
    }
}

/**
 * 工具函式:獲取複製目標屬性的值
 * @param {String} suffix
 * @param {Element} element
 */
function getAttributeValue(suffix, element) {
    const attribute = `data-clipboard-${suffix}`;

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

    return element.getAttribute(attribute);
}
複製程式碼

極為清晰,從resolveOptions可以看到格式化了4個所需的引數。

  1. action事件的行為(複製copy、剪下cut)
  2. target複製的目標
  3. text複製的內容
  4. container包含元素(對於使用者不需要太關心這個,為實現複製功能暫時性的新增textarea作為輔助)

格式化的套路是一致的,判斷是否傳遞了相應的引數,傳遞了就使用,沒有的話就從trigger元素中通過屬性獲取(data-clipboard-xxx)


當格式化所需引數後,接下來看listenClick,對trigger元素繫結點選事件,實現複製功能

import Emitter from 'tiny-emitter';
import listen from 'good-listener';

class Clipboard extends Emitter {
    /**
     * @param {String|HTMLElement|HTMLCollection|NodeList} trigger
     * @param {Object} options
     */
    constructor(trigger, options) {
        super();
        // 定義屬性
        this.resolveOptions(options);

        // 定義事件
        this.listenClick(trigger);
    }
    
    /**
     * 為目標新增點選事件
     * @param {String|HTMLElement|HTMLCollection|NodeList} trigger
     */
    listenClick(trigger) {
        // 作者對繫結事件的封裝,可以理解為
        // trigger.addEventListener('click', (e) => this.onClick(e))
        this.listener = listen(trigger, 'click', (e) => this.onClick(e));
    }

    /**
     * 給目標新增clipboardAction屬性
     * @param {Event} e
     */
    onClick(e) {
        // trigger元素
        const trigger = e.delegateTarget || e.currentTarget;

        if (this.clipboardAction) {
            this.clipboardAction = null;
        }
        // 執行復制操作,把格式化的引數傳遞進去
        this.clipboardAction = new ClipboardAction({
            action    : this.action(trigger),
            target    : this.target(trigger),
            text      : this.text(trigger),
            container : this.container,
            trigger   : trigger,
            emitter   : this
        });
    }
}
複製程式碼

當格式化所需引數後,就可以呼叫clipboard-action.js,並把對應的引數傳遞下去,實現複製功能。猜想作者分兩個檔案來實現是為了以功能來區分模組,清晰明瞭不至於程式碼揉雜在一起過於雜亂無章

3.3 clipboard-action.js

class ClipboardAction {
    /**
     * @param {Object} options
     */
    constructor(options) {
        // 定義屬性
        this.resolveOptions(options);

        // 定義事件
        this.initSelection();
    }
    /**
     * 設定行為action,可以是copy(複製)和cut(剪下)
     * @param {String} action
     */
    set action(action = 'copy') {
        this._action = action;
        // action的值設定為除copy和cut之外都報錯
        if (this._action !== 'copy' && this._action !== 'cut') {
            throw new Error('Invalid "action" value, use either "copy" or "cut"');
        }
    }

    /**
     * 獲取行為action
     * @return {String}
     */
    get action() {
        return this._action;
    }

    /**
     * 使用將複製其內容的元素設定`target`屬性。
     * @param {Element} target
     */
    set target(target) {
        if (target !== undefined) {
            if (target && typeof target === 'object' && target.nodeType === 1) {
                if (this.action === 'copy' && target.hasAttribute('disabled')) {
                    throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');
                }

                if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {
                    throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');
                }

                this._target = target;
            }
            else {
                throw new Error('Invalid "target" value, use a valid Element');
            }
        }
    }

    /**
     * 獲取target(目標)
     * @return {String|HTMLElement}
     */
    get target() {
        return this._target;
    }
}
複製程式碼

我們先看constructor建構函式,作者的老套路,分兩部執行。先定義屬性值,然後執行。除了建構函式外,還需要注意一下classgetset函式,因為它重新定義了某些變數或函式的執行方式。 但從上面看到,作者重新定義了actiontarget,把this._actionthis._target作為了載體,限制了取值範圍而已,小case。


我們清楚了clipboard-action.js的初識設定後,就可以開始看建構函式裡的resolveOptions函式。

class ClipboardAction {
    /**
     * @param {Object} options
     */
    constructor(options) {
        // 定義屬性
        this.resolveOptions(options);

        // 定義事件
        this.initSelection();
    }

    /**
     * 定義基礎屬性(從類Clipboard傳遞進來的)
     * @param {Object} options
     */
    resolveOptions(options = {}) {
        // 行為copy / cut
        this.action    = options.action;
        // 包含元素
        this.container = options.container;
        // 鉤子函式
        this.emitter   = options.emitter;
        // 複製目標
        this.target    = options.target;
        // 複製內容
        this.text      = options.text;
        // 繫結元素
        this.trigger   = options.trigger;
        
        // 選中的複製內容
        this.selectedText = '';
    }
}
複製程式碼

把傳遞進來的值記錄在this上方便存取,但這裡為什麼會多一個this.selectedText呢?

這裡要區分開textselectedText。從文章開始使用上看庫的用法,this.text是使用者傳遞進來需要複製的值,而當傳遞this.target而沒有傳遞this.text時,這時候使用者希望複製的值是這個目標元素的值。所以瞭解用法後這裡的this.selectedText是最終需要複製的值,即this.text的值或者this.target的值


定義完屬性後就開始最為核心高潮的程式碼了!initSelection函式

class ClipboardAction {
    /**
     * @param {Object} options
     */
    constructor(options) {
        // 定義屬性
        this.resolveOptions(options);

        // 定義事件
        this.initSelection();
    }
     /**
     * 使用哪一種策覺取決於提供的text和target
     */
    initSelection() {
        if (this.text) {
            this.selectFake();
        }
        else if (this.target) {
            this.selectTarget();
        }
    }
    /**
     * 從傳遞的target屬性去選擇元素
     */
    selectTarget() {
        // 選中
        this.selectedText = select(this.target);
        // 複製
        this.copyText();
    }
}
複製程式碼

initSelection函式的作用是什麼呢,翻譯意思是初始化選擇,從命名其實可以透露出資訊(賣個關子嘿嘿)。這裡有兩條路可以走,this.textthis.target。我們選擇先走this.target的路selectTarget(方便理解)。

回顧下我們平時在瀏覽器中複製的操作是怎樣的:

  1. 用滑鼠點選頁面
  2. 按住滑鼠並且滑動,選中需要複製的值
  3. ctrl + c 或者 右鍵複製

selectTarget函式就是實現這三個步驟。我們可以看到選中的操作交給了select函式,下面看select函式。

function select(element) {
    var selectedText;
    // target為select時
    if (element.nodeName === 'SELECT') {
        // 選中
        element.focus();
        // 記錄值
        selectedText = element.value;
    }
    // target為input或者textarea時
    else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
        var isReadOnly = element.hasAttribute('readonly');
        // 如果屬性為只讀,不能選中
        if (!isReadOnly) {
            element.setAttribute('readonly', '');
        }
        // 選中target
        element.select();
        // 設定選中target的範圍
        element.setSelectionRange(0, element.value.length);

        if (!isReadOnly) {
            element.removeAttribute('readonly');
        }
        // 記錄值
        selectedText = element.value;
    }
    else {
        if (element.hasAttribute('contenteditable')) {
            element.focus();
        }
        // 建立getSelection,用來選中除input、testarea、select元素
        var selection = window.getSelection();
        // 建立createRange,用來設定getSelection的選中範圍
        var range = document.createRange();

        // 選中範圍設定為target元素
        range.selectNodeContents(element);

        // 清空getSelection已選中的範圍
        selection.removeAllRanges();

        // 把target元素設定為getSelection的選中範圍
        selection.addRange(range);

        // 記錄值
        selectedText = selection.toString();
    }

    return selectedText;
}
複製程式碼

作者這裡分三種情況,其實原理為兩步 (想深入的話自行了解瀏覽器提供下面幾個方法)

  1. 選中元素(element.select()window.getSelection()
  2. 設定選中的範圍(element.setSelectionRange(start, end)range.selectNodeContents(element)

在我們選中了需要複製的元素後,就可以進行復制操作啦 -- copyText函式

class ClipboardAction {
    /**
     * @param {Object} options
     */
    constructor(options) {
        // 定義屬性
        this.resolveOptions(options);

        // 定義事件
        this.initSelection();
    }

    /**
     * 定義基礎屬性(從類Clipboard傳遞進來的)
     * @param {Object} options
     */
    resolveOptions(options = {}) {
      // 行為copy / cut
      this.action    = options.action;
      // 包含元素
      this.container = options.container;
      // 鉤子函式
      this.emitter   = options.emitter;
      // 複製目標
      this.target    = options.target;
      // 複製內容
      this.text      = options.text;
      // 繫結元素
      this.trigger   = options.trigger;

      // 複製內容
      this.selectedText = '';
    }

    /**
     * 使用哪一種策覺取決於提供的text和target
     */
    initSelection() {
        if (this.text) {
            this.selectFake();
        }
        else if (this.target) {
            this.selectTarget();
        }
    }

    /**
     * 從傳遞的target屬性去選擇元素
     */
    selectTarget() {
        // 選中
        this.selectedText = select(this.target);
        // 複製
        this.copyText();
    }

    /**
     * 對目標執行復制操作
     */
    copyText() {
        let succeeded;

        try {
            succeeded = document.execCommand(this.action);
        }
        catch (err) {
            succeeded = false;
        }

        this.handleResult(succeeded);
    }

    /**
     * 根據複製操作的結果觸發對應發射器
     * @param {Boolean} succeeded
     */
    handleResult(succeeded) {
        this.emitter.emit(succeeded ? 'success' : 'error', {
            action: this.action,
            text: this.selectedText,
            trigger: this.trigger,
            clearSelection: this.clearSelection.bind(this)
        });
    }
}
複製程式碼

整個庫最為核心的方法就是document.execCommand了,檢視MDN文件

當一個HTML文件切換到設計模式 (designMode)時,document暴露 execCommand 方法,該方法允許執行命令來操縱可編輯區域的內容,大多數命令影響documentselection(粗體,斜體等)

  1. 命令(copy / cut)
  2. 可編輯區域的內容(我們選中的內容,例如input、textarea)
  3. 命令影響documentselection(當this.target不是inputtextarea時實現我們選中的內容)

最後,handleResult函式就是複製成功或者失敗後的鉤子函式,也即Clipboard所繼承Emitter,當例項化ClipboardAction時就把Emitter作為this.emitter傳遞進來,這是複製的整個過程了,哈哈是不是感覺挺好讀的。


原理是一樣的,只要理解了this.target這條分路,我們回去initSelection函式,看看this.text這條路作者是怎麼實現的

class ClipboardAction {
    /**
     * @param {Object} options
     */
    constructor(options) {
        // 定義屬性
        this.resolveOptions(options);

        // 定義事件
        this.initSelection();
    }

    /**
     * 定義基礎屬性(從類Clipboard傳遞進來的)
     * @param {Object} options
     */
    resolveOptions(options = {}) {
      // 行為copy / cut
      this.action    = options.action;
      // 父元素
      this.container = options.container;
      // 鉤子函式
      this.emitter   = options.emitter;
      // 複製目標
      this.target    = options.target;
      // 複製內容
      this.text      = options.text;
      // 繫結元素
      this.trigger   = options.trigger;

      // 複製內容
      this.selectedText = '';
    }

    /**
     * 使用哪一種策覺取決於提供的text和target
     */
    initSelection() {
        if (this.text) {
            this.selectFake();
        }
        else if (this.target) {
            this.selectTarget();
        }
    }

    /**
     * 建立一個假的textarea元素(fakeElem),設定它的值為text屬性的值並且選擇它
     */
    selectFake() {
        const isRTL = document.documentElement.getAttribute('dir') == 'rtl';

        // 移除已經存在的上一次的fakeElem
        this.removeFake();

        this.fakeHandlerCallback = () => this.removeFake();
        // 利用事件冒泡,當建立假元素並實現複製功能後,點選事件冒泡到其父元素,刪除該假元素
        this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true;

        this.fakeElem = document.createElement('textarea');
        // Prevent zooming on iOS
        this.fakeElem.style.fontSize = '12pt';
        // Reset box model
        this.fakeElem.style.border = '0';
        this.fakeElem.style.padding = '0';
        this.fakeElem.style.margin = '0';
        // Move element out of screen horizontally
        this.fakeElem.style.position = 'absolute';
        this.fakeElem.style[ isRTL ? 'right' : 'left' ] = '-9999px';
        // Move element to the same position vertically
        let yPosition = window.pageYOffset || document.documentElement.scrollTop;
        this.fakeElem.style.top = `${yPosition}px`;

        this.fakeElem.setAttribute('readonly', '');
        this.fakeElem.value = this.text;

        // 新增到容器中
        this.container.appendChild(this.fakeElem);

        // 選中fakeElem
        this.selectedText = select(this.fakeElem);
        // 複製
        this.copyText();
    }

    /**
     * 在使用者點選其他後再移除fakeElem。使用者依然可以使用Ctrl+C去複製,因為fakeElem依然存在
     */
    removeFake() {
        if (this.fakeHandler) {
            this.container.removeEventListener('click', this.fakeHandlerCallback);
            this.fakeHandler = null;
            this.fakeHandlerCallback = null;
        }

        if (this.fakeElem) {
            this.container.removeChild(this.fakeElem);
            this.fakeElem = null;
        }
    }

    /**
     * 對目標執行復制操作
     */
    copyText() {
        let succeeded;

        try {
            succeeded = document.execCommand(this.action);
        }
        catch (err) {
            succeeded = false;
        }

        this.handleResult(succeeded);
    }

    /**
     * 根據複製操作的結果觸發對應發射器
     * @param {Boolean} succeeded
     */
    handleResult(succeeded) {
        this.emitter.emit(succeeded ? 'success' : 'error', {
            action: this.action,
            text: this.selectedText,
            trigger: this.trigger,
            clearSelection: this.clearSelection.bind(this)
        });
    }
}
複製程式碼

回顧下複製的流程,當只給了文字而沒有元素時如何實現?我們可以自己模擬!作者構造了textarea元素,然後選中它即可,套路跟this.target一樣。

值得注意的是,作者巧妙的運用了事件冒泡機制。在selectFake函式中作者把移除textarea元素的事件繫結在this.container上。當我們點選trigger元素複製後,建立一個輔助的textarea元素實現複製,複製完之後點選事件冒泡到父級,父級繫結了移除textarea元素的事件,就順勢移除了。

4.demo

原始碼看了不練,跟白看有什麼區別。接下來提煉最為核心原理寫個demo,賊簡單(MDN的例子)

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<p>點選複製後在右邊textarea CTRL+V看一下</p>
<input type="text" id="inputText" value="測試文字"/>
<input type="button" id="btn" value="複製"/>
<textarea rows="4"></textarea>
<script type="text/javascript">
  var btn = document.getElementById('btn');
  btn.addEventListener('click', function(){
    var inputText = document.getElementById('inputText');
    
    inputText.focus()
    inputText.setSelectionRange(0, inputText.value.length);
    // or
    // inputText.select()
    document.execCommand('copy', true);
  });
</script>
</body>
</html>
複製程式碼

5.總結

這是第一篇文章,寫文章真的挺耗時間的比起自己看,但好處是反覆斟酌原始碼,細看到一些粗略看看不到的東西。有不足的地方多多提意見,會接受但不一定會改哈哈。還有哪些小而美的庫推薦推薦,相互交流,相互學習,相互交易。

py

相關文章