Friday, November 5, 2021

Javascript/CSS: Add Copy-to-Clipboard Button in HTML Code Example

When feeling tired to select the text before copying them, I decide to add a COPY button to every code example block in my blog. The button will copy the whole example block to clipboard. I reached it through 4 steps.

Step 1. Create CSS style for button display behaviour
<style type="text/css">
  /* Copy button common properties */
  .dpw-copy-btn {
     right: 30px; /*Button shows in the right side of block*/
     height: 28px;
     width: 60px;
     position: absolute; /*positioned relative to its parent element*/
     border: none;
     border-radius: 4px;
     font-family: Arial, sans-serif;
     font-size: 12px;
     font-weight: normal;
     font-stretch: normal;
     font-style: normal;
     line-height: 1.33;
     letter-spacing: normal;
     text-align: center;
     color: #ffffff;
     background-position-x: 8px;
     background-position-y: 6px;
     margin-top: -3px;
     user-select: none;   
  }
  /* Color of copy button when enabled (normal status)*/
  .dpw-copy-btn-enabled {
     background-color: #4c825c;
  }
  /* Color of enabled copy button which mouse hovers over */
  .dpw-copy-btn-enabled:hover {
     background-color: #3f6b4b;
  }
  /* Color of copy button after which is clicked */
  .dpw-copy-btn-copied {
     background-color: #312d2a;
     outline: none;
  }
</style>
Class selector "dpw-copy-btn" (line 3-23) defines a style which I am going to use for copy button. It includes location, size, font, color, etc. You can change the value of the properties to set your preferred button style. The properties "right" (line 4) tells browser the button should be displayed on the right within parent element, you can define "left" property instead of "right" to place button on left side. The value of property position (line 7) should be "absolute", it assures that the button will be placed within its parent element (code example block).

Class selector "dpw-copy-btn-enabled" (line 25-27) sets the color of button when the button is enabled, this is normal status of a copy button.

Line 29-31 set the color of enabled button when mouse hovers over the button.

Line 33-36 (class selector "dpw-copy-btn-copied") set the color of button after the button is clicked and the text is copied to clipboard.

Step 2. Create Javascript function for button onclick event
<script type="text/javascript">
  function copyToClipboard(code){
    /*Duplicate code to dummy code block*/
    let dummyCodeBlock = code.cloneNode(true);
    const buttons = dummyCodeBlock.getElementsByTagName("button");
    /*Remove buttons from dummy code block,
      otherwise, the button's caption will be also copied*/
    for(var i = 0; i < buttons.length; i++) {
        dummyCodeBlock.removeChild(buttons[i]);
    }
    /*Copy text from dummy code block to clipboard*/
    let codeText = dummyCodeBlock.innerText;
    const dummyTextArea = document.createElement('textarea');
    dummyTextArea.style.position = 'fixed';
    dummyTextArea.style.left = '0';
    dummyTextArea.style.top = '0';
    dummyTextArea.style.opacity = '0';
    dummyTextArea.value = codeText;
    document.body.appendChild(dummyTextArea);
    dummyTextArea.focus();
    dummyTextArea.select();
    document.execCommand('copy');
    document.body.removeChild(dummyTextArea);
  };
  function showCopied(button) {
    let originalCaption = button.innerText;
    button.innerText = "Copied";
    button.classList.remove("dpw-copy-btn-enabled");
    button.classList.add("dpw-copy-btn-copied");
    setTimeout(function () {
      button.classList.remove('dpw-copy-btn-copied');
      button.innerText = originalCaption;
      button.classList.add('dpw-copy-btn-enabled');
    },1000);
  };
  function copyButtonClick(button) {
    copyToClipboard(button.parentElement);
    showCopied(button);
  };
</script>
It creates three functions "copyToClipboard", "showCopied" and "copyButtonClick". 

Function "copyButtonClick" (line 36-39) is used as button onclick function, it is called when the button is clicked. It calls function "copyToClipboard" to copy the code example text to clipboard, then calls function "showCopied" to change button color and caption.

Normally, code example block contains two part, one is copy button, the other is code example text. Therefore, function "copyToClipboard" has three steps.

First, duplicate a dummy code block object from original example code block (referenced as function input parameter "code"), this is done by line 4. Then remove button element from dummy block (line 5-10). Therefore, only example code text is left.

Second, create a dummy "textarea" element. Since there is no easy way to copy text directly from dymmy block object to clipboard, a "textarea" element is created as transfer stop. Line 13 creates the dummy "textarea" element, and line 18 copies example text from dummy block object to dummy textarea.

Last, copy code example text from dummy textarea to clipboard. It is done by line 20-22.

Function "showCopied" is created for reader-friendly purpose. It will tell reader, "Yes, I did what you asked". It changes the color and caption of the button for one second (1000 millisecond) and then changes back to normal.

Step 3. Copy CSS style created in step 1 and Javascript function created in step 2 to the header section of your HTML page, then add "button" elecment to your code example block as following,
<pre>
  <button class="dpw-copy-btn dpw-copy-btn-enabled" onclick="copyButtonClick(this)">Copy</button>
  <<< Your example code >>>
</pre>
Following is a sample display,
CREATE TABLE part_time_employees ( empno NUMBER(8),  name VARCHAR2(30), hourly_rate NUMBER (7,2)) SEGMENT CREATION DEFERRED;
Is it perfect? Not really. I have two problems now,

1. As we see, the button is blocking the example code.
2. I have to manully enter "button" element for each example code block in my blog. I only want to enter example code and computer should look after the creation of button element.

Therefore, I add following Javascript to my HTML pages,
<script type="text/javascript">
  window.addEventListener("load", function () {
    /* List all example code blocks */
    var codes = document.getElementsByClassName("dpw-code");
    /* For each example code block */
    for (var i = 0; i < codes.length; i++) {
        var button = document.createElement("button");
        button.className = "dpw-copy-btn dpw-copy-btn-enabled";
        button.style.display = "none";
        button.innerText = "Copy";
        /* Add onclick handler to copy button */
        button.addEventListener("click", function() {
          copyToClipboard(this.parentElement);
          showCopied(this);
        });
        code.appendChild(button);
        /* show/hide copy buttion when mouse move over/out code block */
        code.addEventListener("mouseover", function() {
           button.style.display = "block";
        });
        code.addEventListener("mouseout", function() {
           button.style.display = "none";
        })
    }
  }, false);
</script>
The Javascript will be executed when the window is loaded. The details as following,

Line 4 lists all example code blocks which element class is set to "dpw-code". You can use other class name instead of "dpw-code".

Line 6-24 is a for-loop for each example code block.

Line 7-10 creates a button within each block.

Line 12-15 registers click event to new created button element. When the button is clicked, it will run line 13-14.

Line 18-20 show button when mouse moves over the example code block.

Line 21-23 hide button when mouse moves out of the example code block.

Now, if I set class of my example code block to "dpw-code", the button will be created automatically within each block and not visiable when mouse does not move over the block. Therefore, there is no code blocking anymore.

Example code should be entered as following,
<pre class="dpw-code">
  <<< Your example code >>>
</pre>
The class of example code block ("pre" element) has to be "dpw-code" which is referenced in line 4 of previouse Javascript. If you use a different name here, you have to set to same name at line 4 of previous Javascript.

No comments: