JavaScript Copy Button

Goal

To create a copy button for my Math.SE comment template in order to save the trouble of copying and pasting.

My first attempt

I put the boilerplate inside a Markdown codeblock to prevent them from getting interpreted by Hugo’s Markdown parser. Under each codeblock, I placed the copy button.

Comment boilerplate goes here ...

Another comment boilerplate goes here ...

My page’s original layout

$(document).ready(function() {
  $('.copyBtn').click(function() {
    copy($(this).prev().children())
j  });
});

function copy(selector) {
  var screenTop = $(document).scrollTop();
  var $temp = $("<div>");
  $("body").append($temp);
  $temp.attr("contenteditable", true)
       .html($(selector).html()).select()
       .on("focus", function() { document.execCommand('selectAll',false,null) })
       .focus();
  document.execCommand("copy");
  $temp.remove();
  $('html, body').scrollTop(screenTop);
}

static/js/copyBtn.js at Git tag copyBtn0

It worked fine in Qutebrowser, but not in Firefox. Since the later is a popular web browser, I would like to fix my script so that it would function in Firefox.

My second attempt

I tried simplifying the code by getting rid of jQuery, after realising that the JavaScript substitute for the function $(document).ready() wasn’t simple at all.

Some Google search pointed me to the concept of event delegation. An event listener universal for all copy buttons had to invoke a doCopy(target) method, where target is the HTML element containing the comment to be copied. I personally find that JavaScript Info’s page is much easier to read. Nevertheless, once the model has been understood, the code written in David Walsh’s page is much easier to be maintained.

// https://davidwalsh.name/event-delegate
document.getElementsByTagName("article")[0].addEventListener("click", function(e) {
  if (e.target && e.target.nodeName == 'BUTTON') { // only act on buttons
    doCopy(e.target.previousElementSibling.firstElementChild);
  }
});

// https://stackoverflow.com/a/33713926/3184351
function doCopy(target) {
  var range = document.createRange();
  range.selectNodeContents(target);
  s = window.getSelection();
  s.addRange(range);
  document.execCommand('copy');

  // Reset selection
  // https://stackoverflow.com/a/3169849/3184351
  if (window.getSelection) {
    if (window.getSelection().empty) {  // Chrome
      s.empty();
    } else if (window.getSelection().removeAllRanges) {  // Firefox
      s.removeAllRanges();
    }
  } else if (document.selection) {  // IE?
    document.selection.empty();
  }
}

This version of static/js/copyBtn.js did work in both Qutebrowser and Firefox. However, sometimes, the whole page was copied instead of the target. I had tried resetting the selection, but that didn’t work. My primitive debugging technique console.log(range) didn’t work: the whole page could get copied while the range behaved normally.

Back to basics

Finally, I have decided to convert the codeblocks into immutable <textarea>s because W3Schools’s tutorial only works for HTML form elements. To prevent Hugo from parsing the contents inside the <textarea>s, they have to be wrapped with a <div> block. This allows a significant simplification of the script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
document.getElementsByTagName("article")[0].addEventListener("click", function(e) {
  if (e.target && e.target.nodeName == 'BUTTON') {
    doCopy(e.target.previousElementSibling.firstElementChild);
  }
});

function doCopy(target) {
  target.select();
  document.execCommand("copy");
}

Refined version of static/js/copyBtn.js at commit 4a4f46d1.


No comment