Add copy for SyntaxHighlighter snippets - arcdev/engram404 GitHub Wiki

originally posted 2016-04-28 at https://engram404.net/add-copy-for-syntaxhighlighter-snippets/

Skip to the code, or continue reading the story…

I'm using Alex Gorbatchev's SyntaxHighlighter for the various code snippets here. The one thing it seems to be missing is a ‘copy' function – one button to copy the entire content of the code snippet.

Years ago the only way to copy content to the clipboard from a browser (automatically) was to use something like Adobe Flash. [shudder] I started to look to see if there was a current option. And there is! MDN documents a function:

Document.execCommand()

Caveat emptor – apparently, this specification is still considered in "draft" and may not be compatible with all browser. MDN claims it's compatible with Chrome, Firefox, IE9, and Opera. That's good enough for me, but we'll address that later (using document.queryCommandSupported). By the way – it also claims Safari doesn't support it.

If we assume that the user has already highlighted (selected) what they want to copy, then this is simple:

document.execCommand("copy");

Well, that only sorta helps a little. Plus, it kinda defeats the purpose. If the user has already highlighted what they want, then performing a copy operation is simple.

Let's see if there's an option. Yup – MDN to the rescue: Selection (again, flagged as '"experimental'")

That helps some, but we need to be able to select a particular Range. So let's put some of this together.

This snippet:

  • finds a node (using jQuery)
  • selects its contents
  • copies it
  • remove the selection
var nodeToCopy = jQuery(".container").last()[0];
 
var range = document.createRange();
range.selectNode(nodeToCopy);
 
var selection = document.getSelection();
selection.addRange(range);
 
var success = document.execCommand("copy");
selection.removeAllRanges();

Seems that's about it, right? Well, not quite… if the user has already highlighted (selected) something, this will probably throw an exception (because selection.addRange usually can't support non-contiguous ranges).

We can clear the current selection with selection.removeAllRanges, but that's not very nice of us. So let's store off the previous selection and put it back after we're done. The JavaScript should execute so fast (these days) that the user will never notice.

var range, selection, success, prevRanges;
 
prevRanges = [];
 
range = document.createRange();
range.selectNode(node);
selection = document.getSelection();
for(var i=0; i < selection.rangeCount; i++){
    // likely this is only ever 1, but...
    prevRanges.push(selection.getRangeAt(i));
}
selection.removeAllRanges();    
 
selection.addRange(range);
success = document.execCommand("copy");
//console.log("copy was", success);
selection.removeAllRanges();
while(prevRanges.length > 0){
    selection.addRange(prevRanges.pop());
}

Better.

Now, let's start working with what SyntaxHighlighter does for us. We need to add a button to every place that SyntaxHighlighter modifies:

function addCopyButtons() {
    var $sh = jQuery(".syntaxhighlighter");
    var buttons = [];
    $sh.each(function (idx, element) {
        var $button = jQuery("<button class='copy'>copy</button>");
        buttons.push($button[0]);
        $button.insertBefore(jQuery(this).children().first());
    });
    $buttons = jQuery(buttons);
    $buttons.css("float", "right");
    if (!document.queryCommandSupported("copy")){
        $buttons.attr("disabled", "true").addClass("disabled").attr("title", "Not supported in your browser.");
        return;
    }
    $buttons.on("click", btnCopy_click);
}

And we'll need the event handler:

function btnCopy_click() {
    var $self,
    $node;
    $self = jQuery(this);
    $node = $self.parent().find(".container");
    syntaxHighlighterCopy($node[0]);
}

That's all well and good, but now we're back to the age old '"ordering'" problem. Our new code snippet will probably execute before SyntaxHighlighter can do its work. We could use something like RequireJS to help us out… unless, of course, we're working within someone else's framework, like WordPress. So let's solve it the cheap way. We could just wait for at least one SyntaxHighlighter element or style to be present, but what happens if it's still working? Let's try to give it some time to become quiescent.

function waitForSyntaxHighlighter(){
    var handle, syntaxCount, interval, maxWait;
    interval = 100; // ms
    maxWait = 5 * 1000 / interval; // = interval * maxWait; currently 5,000ms = 5sec
    handle = setInterval(function(){
        maxWait--;
        if (maxWait < 0){
            //console.log("waitForSyntaxHighlighter - timeout");
            clearInterval(handle);
            return;
        }
        var len = jQuery(".syntaxhighlighter").length;
        if (len === 0){
            //console.log("waitForSyntaxHighlighter - zero");
            return;
        }
        if (len !== syntaxCount){
            syntaxCount = len;
            //console.log("waitForSyntaxHighlighter - still adding");
            return;
        }
        clearInterval(handle);
        //console.log("waitForSyntaxHighlighter - yay");
        addCopyButtons();
    }, interval);
}

The Code

Now let's put all of the code together:

(function($, undefined){
    function addCopyButtons() {
        var $sh = jQuery(".syntaxhighlighter");
        var buttons = [];
        $sh.each(function (idx, element) {
            var $button = jQuery("<button class='copy'>copy</button>");
            buttons.push($button[0]);
            $button.insertBefore(jQuery(this).children().first());
        });
        $buttons = jQuery(buttons);
        $buttons.css("float", "right");
        if (!document.queryCommandSupported("copy")){
            $buttons.attr("disabled", "true").addClass("disabled").attr("title", "Not supported in your browser.");
            return;
        }
        $buttons.on("click", btnCopy_click);
    }
 
    function btnCopy_click() {
        var $self,
        $node;
        $self = jQuery(this);
        $node = $self.parent().find(".container");
        syntaxHighlighterCopy($node[0]);
    }
 
    function syntaxHighlighterCopy(node) {
        var range, selection, success, prevRanges;
         
        prevRanges = [];
 
        range = document.createRange();
        range.selectNode(node);
        selection = document.getSelection();
        for(var i=0; i < selection.rangeCount; i++){
            // likely this is only ever 1, but...
            prevRanges.push(selection.getRangeAt(i));
        }
        selection.removeAllRanges();    
         
        selection.addRange(range);
        success = document.execCommand("copy");
        //console.log("copy was", success);
        selection.removeAllRanges();
        while(prevRanges.length > 0){
            selection.addRange(prevRanges.pop());
        }
    }
 
    function waitForSyntaxHighlighter(){
        var handle, syntaxCount, interval, maxWait;
        interval = 100; // ms
        maxWait = 5 * 1000 / interval; // = interval * maxWait; currently 5,000ms = 5sec
        handle = setInterval(function(){
            maxWait--;
            if (maxWait < 0){
                //console.log("waitForSyntaxHighlighter - timeout");
                clearInterval(handle);
                return;
            }
            var len = jQuery(".syntaxhighlighter").length;
            if (len === 0){
                //console.log("waitForSyntaxHighlighter - zero");
                return;
            }
            if (len !== syntaxCount){
                syntaxCount = len;
                //console.log("waitForSyntaxHighlighter - still adding");
                return;
            }
            clearInterval(handle);
            //console.log("waitForSyntaxHighlighter - yay");
            addCopyButtons();
        }, interval);
    }
 
    waitForSyntaxHighlighter();
})(jQuery);

I've tested this successfully on:

  • Chrome 49,50
  • Firefox 45,46
  • IE 11
  • Edge 25
  • Vivaldi 1.1 And, just in case you're curious, you can add this to your own WordPress site by modifying your functions.php and adding something like this:
add_action( 'wp_enqueue_scripts', 'theme_enqueue_scripts' );
function theme_enqueue_scripts() {
    wp_enqueue_script('syntaxHighlighter-add-copy-button', get_stylesheet_directory_uri () . '/js/syntaxHighlighter-add-copy-button.js', array( 'jquery','syntaxhighlighter-core' ));
}
⚠️ **GitHub.com Fallback** ⚠️