Clipboard. js is a small copy to clipboard plug-in, only 3KB, non-Flash

1. Introduction

Company project useful to clipboard.js, due to the curiosity to open the source code to see how it is implemented, this is thought to be convoluted 18 twists and turns, in fact, it is very easy to understand, so I share the feeling after reading ha ha.

This reading is divided into five parts, respectively for the introduction, use, analysis, demo, summary, five parts are not connected to each other can be separated according to the need to see.

Foreword for introduction, use for the use of the library, parsing for the source code of the analysis, demo is the core of the source code of the small demo, summed up as blowing water, learn to apply.

It is recommended to read with the source code combined with this article, so that it is easier to understand!

  1. clipboard.js
  2. Github address resolved by clipboard.js

2. Use

It’s a good idea to know how to use the source code before reading it, to help you understand why some weird source code is written the way it is. (Below is a demo by the author of Clipboard.js)

<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>
Copy the code

As can be seen from the demo given by the author, the value of div hello is copied after clicking BTN, which can be seen as three steps:

  1. Kakashi (BTN of Demo)
  2. Copy (Demo ClipboardJS)
  3. Ninjutsu (Div of demo)

Trigger copy target (ninjutsu)

2.1 the trigger

Trigger is passed to the ClipboardJS function, which accepts three types

  1. Dom elements
  2. nodeList
  3. The selector
<! -- 1. Dom element -->
<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. Selector -->
<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>
Copy the code

2.2 the target

Target’s purpose is to get the copied value (text), so target doesn’t have to be dom. There are two ways to get text

  1. Trigger attribute assigned
  2. Target element fetch
<! -- 1. Trigger attribute assignment 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. Select a value from the target object.
<button class="btn">Copy</button>
<div>hello</div>

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

<! -- 2. Target object gets value 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>
Copy the code

2.3 Copy (Default Copy/Cut)

<! -- 1. copy: default copy -->
<button class="btn">Copy</button>
<div>hello</div>

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

<! -- 2. Cut: 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>
Copy the code

3. The parsing

Source code mainly contains two core files clipboard.js and clipboard-action.js, but also need to understand tiny-emitters.

  1. Tiny-emitters. Js: Event emitters, equivalent to hooks, that handle replicating callbacks
  2. Clipboard.js: Parameters needed to handle replication
  3. Clipboard-action.js: the core logic of replication

3.1 tiny – emitter. Js

Tiny-emitter is a small (less than 1k) EventEmitter (equivalent to node events.eventemitter).

You may be wondering why the first thing to parse is tiny-Emitters. Js instead of Clipboard. js.

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

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

// Tiny-emitters. Js handles callbacks when replication succeeds or fails
clipboard.on('success'.function(data) {
    console.log(data);
});

clipboard.on('error'.function(data) {
    console.log(data);
});
</script>
Copy the code

Now that events are defined, where does the source code fire the event trigger? From his identity (success | error) naturally think of, is to replicate this operation only after the trigger. Let’s take a quick look at the emit method code in clipboard-action.js

class ClipboardAction{
  /** * Triggers the corresponding emitter based on the result of the replication operation * @param {Boolean} Succeeded Returns the value after the replication operation, which is used to determine whether the replication is successful */
  handleResult(succeeded) {
      // This. Emitter. Emit equals e.mit
      this.emitter.emit(succeeded ? 'success' : 'error', {
          action: this.action,
          text: this.selectedText,
          trigger: this.trigger,
          clearSelection: this.clearSelection.bind(this)}); }}Copy the code

The on and EMIT methods of Tiny-Emitters are used in clipboard.js. Tiny – emitter. Js declare an object (this. E), (success | error) defines identity, on method is used to add the logo, emit method is used to identify the launch event. For example: you are an ancient emperor. At the beginning of his reign, you recruited a group of beauties to the harem (on method). One day, if you want to have a physical examination, you can ask your father-in-law to send a signal to the harem (emit method), and then all the rain and dew can be emitted.

function E () {}
/** * @param {String} name Name of the triggered event * @param {function} callback triggered event * @param {object} CTX function call context */
E.prototype = {
  on: function (name, callback, ctx) {
    // this.e stores global events
    var e = this.e || (this.e = {});
    
    // the this.e structure
    // 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}
        // Finally get data from the callback function. E.on(success, (data) => data)
        var data = [].slice.call(arguments.1);
        
        // Get the function corresponding to the identity
        var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
        var i = 0;
        var len = evtArr.length;
    
        for (i; i < len; i++) {
          // Loop over the array of functions, passing data as a result of the on callback
          evtArr[i].fn.apply(evtArr[i].ctx, data);
        }
    
        return this; }};Copy the code

Tiny-emitters. Js maintains an object (this.e) that records a series of properties (e.g. Success, error), attribute is array, when calling on method to add trigger function to the array of corresponding attribute, call emit method to trigger all functions of corresponding attribute

3.2 the clipboard. Js

Clipboard. js consists of clipboard.js and clipboard-action.js. Clipboard.js is responsible for receiving the parameters passed in and assembling the data structures needed by Clipboard-action.js. Clipboard-action.js is the core library for replication, which is responsible for the implementation of replication. Let’s take a look at clipboard.js

import Emitter from 'tiny-emitter';
class Clipboard extends Emitter {
    /** * @param {String|HTMLElement|HTMLCollection|NodeList} trigger * @param {Object} options */
    constructor(trigger, options) {
        super(a);// Define attributes
        this.resolveOptions(options);

        // Define events
        this.listenClick(trigger); }}Copy the code

As you can see from the above source, Clipboard is derived from Emitter, which is the method of tiny-Emitters. There are two steps to Clipboard initialization

  1. Format the parameters passed in
  2. Add a click event to the target element and copy it

Let’s start with the resolveOptions function (note the distinction between the trigger element, which is used to bind the click event, and the target object, which is copied). Trigger (kakashi) copies target (Ninjutsu)

import Emitter from 'tiny-emitter';
    class Clipboard extends Emitter {
    /** * @param {String|HTMLElement|HTMLCollection|NodeList} trigger * @param {Object} options */
    constructor(trigger, options) {
        super(a);// Define attributes
        this.resolveOptions(options);

        // Define events
        this.listenClick(trigger);
    }

    /** * Define the properties of the function, use the external function if there is an external function, otherwise use the internal default function * @param {Object} options */
    resolveOptions(options = {}) {
        // Event behavior
        this.action    = (typeof options.action    === 'function')? options.action :this.defaultAction;
        // Target for replication
        this.target    = (typeof options.target    === 'function')? options.target :this.defaultTarget;
        // Copy the content
        this.text      = (typeof options.text      === 'function')? options.text :this.defaultText;
        // Contains elements
        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);
}
Copy the code

Extremely clear, resolveOptions format the four required parameters.

  1. actionBehavior of events (copy, cut)
  2. targetTarget of replication
  3. textCopied content
  4. containerInclude elements (not too much for the user, temporary additions for replicationtextareaAs an aid)

The format is the same. If the corresponding parameter is passed, it is used. If not, it is obtained from the attribute of the trigger element (data-clipboard-xxx).


After formatting the required parameters, look at listenClick, which binds the click event to the trigger element for replication

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(a);// Define attributes
        this.resolveOptions(options);

        // Define events
        this.listenClick(trigger);
    }
    
    / * * * * @ for target to add click event param {String | HTMLElement | HTMLCollection | NodeList} the trigger * /
    listenClick(trigger) {
        // The author encapsulates the binding event
        // trigger.addEventListener('click', (e) => this.onClick(e))
        this.listener = listen(trigger, 'click', (e) => this.onClick(e));
    }

    /** * Add clipboardAction to the target * @param {Event} e */
    onClick(e) {
        / / the trigger element
        const trigger = e.delegateTarget || e.currentTarget;

        if (this.clipboardAction) {
            this.clipboardAction = null;
        }
        // Perform the copy operation and pass in the formatted arguments
        this.clipboardAction = new ClipboardAction({
            action    : this.action(trigger),
            target    : this.target(trigger),
            text      : this.text(trigger),
            container : this.container,
            trigger   : trigger,
            emitter   : this}); }}Copy the code

After formatting the required parameters, you can call clipboard-action.js and pass the corresponding parameters to achieve the replication function. I guess the author divided two files to implement in order to distinguish modules by function, clear and not too messy code

3.3 the clipboard – action. Js

class ClipboardAction {
    /** * @param {Object} options */
    constructor(options) {
        // Define attributes
        this.resolveOptions(options);

        // Define events
        this.initSelection();
    }
    /** * set the action, which can be copy and cut * @param {String} action */
    set action(action = 'copy') {
        this._action = action;
        // Action is set to copy and cut
        if (this._action ! = ='copy' && this._action ! = ='cut') {
            throw new Error('Invalid "action" value, use either "copy" or "cut"'); }}/** * get action * @return {String} */
    get action() {
        return this._action;
    }

    /** * Set the 'target' property with the element whose content will be copied. * @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'); }}}/ * * * access to the target (target) * @ return {String | HTMLElement} * /
    get target() {
        return this._target; }}Copy the code

Let’s start with the constructor constructor, the author’s old trick, executed in two parts. First define the property value, then execute. In addition to constructors, pay attention to the get and set functions of class, because they redefine how certain variables or functions are executed. This._action and this._target are used as vectors to limit the value range.


Now that we know the initial setup of clipboard-action.js, we can look at the resolveOptions function in the constructor.

class ClipboardAction {
    /** * @param {Object} options */
    constructor(options) {
        // Define attributes
        this.resolveOptions(options);

        // Define events
        this.initSelection();
    }

    /** * Define base properties (passed in from class Clipboard) * @param {Object} options */
    resolveOptions(options = {}) {
        // copy/cut
        this.action    = options.action;
        // Contains elements
        this.container = options.container;
        // The hook function
        this.emitter   = options.emitter;
        // Copy the target
        this.target    = options.target;
        // Copy the content
        this.text      = options.text;
        // Bind the element
        this.trigger   = options.trigger;
        
        // The selected copy content
        this.selectedText = ' '; }}Copy the code

The value passed in to this is easy to access, but why this. SelectedText?

I want to distinguish between text and selectedText. Text is the value passed in by the user to be copied. When this.target is passed but not this.text, the value the user wants copied is the value of the target element. So this. SelectedText here is the final value to copy, the value of this.text or the value of this.target


After the definition of attributes began the core climax of the code! InitSelection function

class ClipboardAction {
    /** * @param {Object} options */
    constructor(options) {
        // Define attributes
        this.resolveOptions(options);

        // Define events
        this.initSelection();
    }
     /** * Which idea to use depends on the supplied text and target */
    initSelection() {
        if (this.text) {
            this.selectFake();
        }
        else if (this.target) {
            this.selectTarget(); }}/** * Select the element */ from the passed target attribute
    selectTarget() {
        / / selected
        this.selectedText = select(this.target);
        / / copy
        this.copyText(); }}Copy the code

InitSelection is a function that initializes selection, and the name of the selection is a tell-all. There are two ways to go, this.text and this.target. We choose to go this.target first selectTarget.

Let’s review what we normally copy in the browser:

  1. Click the page with the mouse
  2. Hold down the mouse and slide to select the value you want to copy
  3. ctrl + cOr right click copy

The selectTarget function implements these three steps. We can see that the selected operation is given to the select function. Let’s look at the SELECT function.

function select(element) {
    var selectedText;
    // target is select
    if (element.nodeName === 'SELECT') {
        / / selected
        element.focus();
        / / record values
        selectedText = element.value;
    }
    // Target is input or textarea
    else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
        var isReadOnly = element.hasAttribute('readonly');
        // If the property is read-only, it cannot be selected
        if(! isReadOnly) { element.setAttribute('readonly'.' ');
        }
        / / selected target
        element.select();
        // Set the range of selected targets
        element.setSelectionRange(0, element.value.length);

        if(! isReadOnly) { element.removeAttribute('readonly');
        }
        / / record values
        selectedText = element.value;
    }
    else {
        if (element.hasAttribute('contenteditable')) {
            element.focus();
        }
        // create getSelection to select elements other than input, testArea, and select
        var selection = window.getSelection();
        // Create createRange to set the range of getSelection
        var range = document.createRange();

        // Select the range set to the target element
        range.selectNodeContents(element);

        // Clear getSelection of the selected range
        selection.removeAllRanges();

        // Set the target element to the range of getSelection
        selection.addRange(range);

        / / record values
        selectedText = selection.toString();
    }

    return selectedText;
}
Copy the code

The author here is divided into three cases, in fact, the principle of two steps (if you want to further understand the browser to provide the following methods)

  1. Select element (element.select()andwindow.getSelection())
  2. Sets the selected range (element.setSelectionRange(start, end)andrange.selectNodeContents(element))

After we have selected the elements we want to copy, we are ready to copy — the copyText function

class ClipboardAction {
    /** * @param {Object} options */
    constructor(options) {
        // Define attributes
        this.resolveOptions(options);

        // Define events
        this.initSelection();
    }

    /** * Define base properties (passed in from class Clipboard) * @param {Object} options */
    resolveOptions(options = {}) {
      // copy/cut
      this.action    = options.action;
      // Contains elements
      this.container = options.container;
      // The hook function
      this.emitter   = options.emitter;
      // Copy the target
      this.target    = options.target;
      // Copy the content
      this.text      = options.text;
      // Bind the element
      this.trigger   = options.trigger;

      // Copy the content
      this.selectedText = ' ';
    }

    /** * Which idea to use depends on the supplied text and target */
    initSelection() {
        if (this.text) {
            this.selectFake();
        }
        else if (this.target) {
            this.selectTarget(); }}/** * Select the element */ from the passed target attribute
    selectTarget() {
        / / selected
        this.selectedText = select(this.target);
        / / copy
        this.copyText();
    }

    /** * Performs a copy operation on the target */
    copyText() {
        let succeeded;

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

        this.handleResult(succeeded);
    }

    /** * The corresponding emitter is triggered based on the result of the replication operation * @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)}); }}Copy the code

The core method of the entire library is document.execCommand, which looks at the MDN document

When an HTML document switches to designMode, the document exposes the execCommand method, which allows commands to be run to manipulate the contents of the editable area. Most commands affect document selection (bold, italic, etc.)

  1. Command (copy/cut)
  2. The content of the editable area (the content we selected, such as input, Textarea)
  3. Command influencedocumenttheselection(whenthis.targetnotinput,textareaTo implement what we selected.)

Finally, the handleResult function is an Emitter function inherited from Clipboard after copying successfully or failed. When instantiating ClipboardAction, we pass the Emitter as this. Emitter. Ha ha doesn’t it feel good to read.


The principle is the same. Once we understand the this.target branch, let’s go back to initSelection and see how the author implements this.text

class ClipboardAction {
    /** * @param {Object} options */
    constructor(options) {
        // Define attributes
        this.resolveOptions(options);

        // Define events
        this.initSelection();
    }

    /** * Define base properties (passed in from class Clipboard) * @param {Object} options */
    resolveOptions(options = {}) {
      // copy/cut
      this.action    = options.action;
      / / the parent element
      this.container = options.container;
      // The hook function
      this.emitter   = options.emitter;
      // Copy the target
      this.target    = options.target;
      // Copy the content
      this.text      = options.text;
      // Bind the element
      this.trigger   = options.trigger;

      // Copy the content
      this.selectedText = ' ';
    }

    /** * Which idea to use depends on the supplied text and target */
    initSelection() {
        if (this.text) {
            this.selectFake();
        }
        else if (this.target) {
            this.selectTarget(); }}/** * Create a fake Textarea element (fakeElem), set its value to the value of the text property and select it */
    selectFake() {
        const isRTL = document.documentElement.getAttribute('dir') = ='rtl';

        // Remove the last fakeElem that already exists
        this.removeFake();

        this.fakeHandlerCallback = (a)= > this.removeFake();
        // After creating the fake element and implementing the copy function, click the event bubble to its parent element and delete the fake element
        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;

        // Add to the container
        this.container.appendChild(this.fakeElem);

        / / selected fakeElem
        this.selectedText = select(this.fakeElem);
        / / copy
        this.copyText();
    }

    /** * Remove fakeElem after the user clicks other. Users can still use Ctrl+C to copy because fakeElem still exists */
    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; }}/** * Performs a copy operation on the target */
    copyText() {
        let succeeded;

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

        this.handleResult(succeeded);
    }

    /** * The corresponding emitter is triggered based on the result of the replication operation * @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)}); }}Copy the code

Reviewing the process of copying, how is this implemented when only text is given but no elements? We can do it ourselves! The author constructs the textarea element and selects it, just like this.target.

It is worth noting that the author cleverly uses the event bubbling mechanism. In selectFake, the author binds the event that removes the Textarea element to this.container. When we click the trigger element to copy, we create a secondary Textarea element to copy. After the copy, the click event bubbles to the parent, which is bound to the event that removes the Textarea element.

4.demo

Source code read not practice, with white see what is the difference. Next refine the core principle to write a demo, thief simple (MDN example)


      
<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>Click Copy on the right textarea CTRL+V to have a look</p>
<input type="text" id="inputText" value="Test text"/>
<input type="button" id="btn" value="Copy"/>
<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>
Copy the code

5. To summarize

This is the first article, writing the article is really quite time-consuming than their own, but the advantage is to repeatedly consider the source code, see some of the things that can not be seen roughly. There are many deficiencies of the place to give advice, will accept but not necessarily will change ha ha. What are the small and beautiful library recommendation, mutual exchange, mutual learning, mutual trade.