asciidoctor/asciidoctor-extensions-lab

Create an extension to add a "Copy" button on source block

Closed this issue ยท 15 comments

Agreed! I especially like the "no flash" part.

We'll have to think about where to put the button because we're already using the upper right for the source language. Perhaps we can implement something like a double click to copy. That seems natural. We could put a floating hint under the listing block with the instructions to provide the hint.

Related issues:

minad commented

The following works for me, the button will be shown for pre blocks in the top corner or just below the language. The button will be shown when hovering the pre element. I would love to see that included in asciidoctor.

<script src="assets/clipboard.js"></script>
<style>
 .listingblock:hover .clipboard {
     display: block;
 }

 .clipboard {
     display: none;
     border: 0;
     font-size: .75em;
     text-transform: uppercase;
     font-weight: 500;
     padding: 6px;
     color: #999;
     position: absolute;
     top: .425rem;
     right: .5rem;
     background: transparent;
 }

 code + .clipboard {
     top: 2rem !important;
 }

 .clipboard:hover, .clipboard:focus, .clipboard:active {
     outline: 0;
     background-color: #eee9e6;
 }
</style>
<script>
 $(function() {
     var pre = document.getElementsByTagName('pre');
     for (var i = 0; i < pre.length; i++) {
	 var b = document.createElement('button');
	 b.className = 'clipboard';
	 b.textContent = 'Copy';
         if (pre[i].childNodes.length === 1 && pre[i].childNodes[0].nodeType === 3) {
	     var div = document.createElement('div');
	     div.textContent = pre[i].textContent;
             pre[i].textContent = '';
             pre[i].appendChild(div);
         }
	 pre[i].appendChild(b);
     }
     new ClipboardJS('.clipboard', {
	 target: function(b) {
             var p = b.parentNode;
	     return p.className.includes("highlight")
                  ? p.getElementsByClassName("code")[0]
                  : p.childNodes[0];
         }
     }).on('success', function(e) {
	 e.clearSelection();
	 e.trigger.textContent = 'Copied';
	 setTimeout(function() {
	     e.trigger.textContent = 'Copy';
	 }, 2000);
     });
 });
</script>

Any plans to provide this as a default option?

We would really welcome this functionality. Is there a chance that this gets included in the next release?

I think it can be an extension, perhaps even a dedicated project. It's certainly a great addition, but not something I'm not yet comfortable putting into core. Though we are planning to add it to Antora (see https://gitlab.com/antora/antora-ui-default/issues/70).

@mojavelinux thanks for the answer.

Hi, is there any progress on this, please?

If you use asciidoctorj you could look at the Puravida extension and also the documentation. The way the extension works is shown in the docs.

In this case it's been implemented as a PostProcessor with resources added directly to the output of the processing. It could be implemented in a bunch of different ways but to get similar effect perhaps a Docinfo Processor and a PostProcessor would be one way.

To implement the approach shown above in #61 (comment) you could probably just use a Docinfo processor but this wouldn't provide flexibility to enable and disable on particular blocks or documents which might be desirable.

The following works for me, the button will be shown for pre blocks in the top corner or just below the language. The button will be shown when hovering the pre element. I would love to see that included in asciidoctor.

<script src="assets/clipboard.js"></script>
<style>
 .listingblock:hover .clipboard {
     display: block;
 }

 .clipboard {
     display: none;
     border: 0;
     font-size: .75em;
     text-transform: uppercase;
     font-weight: 500;
     padding: 6px;
     color: #999;
     position: absolute;
     top: .425rem;
     right: .5rem;
     background: transparent;
 }

 code + .clipboard {
     top: 2rem !important;
 }

 .clipboard:hover, .clipboard:focus, .clipboard:active {
     outline: 0;
     background-color: #eee9e6;
 }
</style>
<script>
 $(function() {
     var pre = document.getElementsByTagName('pre');
     for (var i = 0; i < pre.length; i++) {
	 var b = document.createElement('button');
	 b.className = 'clipboard';
	 b.textContent = 'Copy';
         if (pre[i].childNodes.length === 1 && pre[i].childNodes[0].nodeType === 3) {
	     var div = document.createElement('div');
	     div.textContent = pre[i].textContent;
             pre[i].textContent = '';
             pre[i].appendChild(div);
         }
	 pre[i].appendChild(b);
     }
     new ClipboardJS('.clipboard', {
	 target: function(b) {
             var p = b.parentNode;
	     return p.className.includes("highlight")
                  ? p.getElementsByClassName("code")[0]
                  : p.childNodes[0];
         }
     }).on('success', function(e) {
	 e.clearSelection();
	 e.trigger.textContent = 'Copied';
	 setTimeout(function() {
	     e.trigger.textContent = 'Copy';
	 }, 2000);
     });
 });
</script>

@minad Pardon my ignorance of web dev, but I tried pasting the above snippet into the <head> section of an html file rendered from asciidoctor just to see what would happen, and I wasn't able to get it to work. I made a local clipboard.js file from the contents here and changed <script src="assets/clipboard.js"> to just <script src="clipboard.js"> since it's all in the same directory. It didn't work for me. I'm sure I'm missing something embarrassingly simple, but I just wanted a quick proof of concept.

Could you explain how to get this to work to a web dev novice?

Here is a slightly adapted variant, which works for me. You have to provide clipboard.min.js in the assets folder.

<script src="assets/clipboard.min.js"></script>
<style>
    .listingblock:hover .clipboard {
        display: block;
    }

    .clipboard {
        display: none;
        border: 0;
        font-size: .75em;
        text-transform: uppercase;
        font-weight: 500;
        padding: 6px;
        color: #999;
        position: absolute;
        top: .425rem;
        right: .5rem;
        background: transparent;
    }

    code + .clipboard {
        top: 2rem !important;
    }

    .clipboard:hover, .clipboard:focus, .clipboard:active {
        outline: 0;
        background-color: #eee9e6;
    }
</style>
<script >
    window.onload = function() {
        var pre = document.getElementsByTagName('pre');
        for (var i = 0; i < pre.length; i++) {
            var b = document.createElement('button');
            b.className = 'clipboard';
            b.textContent = 'Copy';
            if (pre[i].childNodes.length === 1 && pre[i].childNodes[0].nodeType === 3) {
                var div = document.createElement('div');
                div.textContent = pre[i].textContent;
                pre[i].textContent = '';
                pre[i].appendChild(div);
            }
            pre[i].appendChild(b);
        }
        var clipboard = new ClipboardJS('.clipboard', {
           target: function(b) {
                var p = b.parentNode;
                if (p.className.includes("highlight")) {
                    var elems = p.getElementsByTagName("code");
                    if (elems.length > 0)
                        return elems[0];
                }
                return p.childNodes[0];
            }
        });
        clipboard.on('success', function(e) {
            e.clearSelection();
            e.trigger.textContent = 'Copied';
            setTimeout(function() {
                e.trigger.textContent = 'Copy';
            }, 2000);
        });
        clipboard.on('error', function(e) {
            console.error('Action:', e.action, e);
            console.error('Trigger:', e.trigger);
        });
    };
</script>

Thanks to you all. I modified the code a little to be more like AsciiDoctor website layout, and I also add a rotation animation instead of copied text:

<script src="assets/clipboard.min.js"></script>
<style>
    .listingblock code[data-lang]::before {
        display: none;
        content: attr(data-lang) " |";
        position: absolute;
        font-size: 0.75em;
        top: 0.425rem;
        right: 1.7rem;
        line-height: 1;
        text-transform: uppercase;
        color: inherit;
        opacity: 0.5
    }
    
    .listingblock:hover code[data-lang]::before {
        display: block
    }

    .listingblock:hover .clipboard {
        display: block;
    }

    .clipboard {
        display: none;
        content: "  ";
        position: absolute;
        top: 0.3rem;
        right: 0.5em;
        height: 1em;
        color: inherit;
        border: none;
        opacity: 0.5;
        cursor: pointer;
        filter: invert(50.2%);
        background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3E%3Cpath fill-rule='evenodd' d='M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z'%3E%3C/path%3E%3C/svg%3E") no-repeat;
    }

    @keyframes clibpard_rotation_frame {
        50% { transform: rotateY(180deg); }
    }

    .clipboard_success {
        filter: invert(100%);
        animation: clibpard_rotation_frame 1s;
    }
</style>

<script >
    window.onload = function() {
        var pre = document.getElementsByTagName('pre');

        for (var i = 0; i < pre.length; i++) {
            var b = document.createElement('button');
            b.className = 'clipboard';
            b.textContent = ' ';
            if (pre[i].childNodes.length === 1 && pre[i].childNodes[0].nodeType === 3) {
                var div = document.createElement('div');
                div.textContent = pre[i].textContent;
                pre[i].textContent = '';
                pre[i].appendChild(div);
            }
            pre[i].appendChild(b);
        }

        var clipboard = new ClipboardJS('.clipboard', {
           target: function(b) {
                var p = b.parentNode;
                if (p.className.includes("highlight")) {
                    var elems = p.getElementsByTagName("code");
                    if (elems.length > 0)
                        return elems[0];
                }
                return p.childNodes[0];
            }
        });

        clipboard.on('success', function(e) {
            e.clearSelection();
            e.trigger.classList.add('clipboard_success');
            setTimeout(function() {
                e.trigger.classList.remove('clipboard_success');
            }, 1300);
        });

        clipboard.on('error', function(e) {
            console.error('Action:', e.action, e);
            console.error('Trigger:', e.trigger);
        });
    };
</script>

I implemented this behavior in the default Antora UI using the built-in clipboard API of the browser. I'll share the steps for how to integrate into a standalone HTML document generated by Asciidoctor. Please note that this code is MPL-2.0.

  1. Download the script https://gitlab.com/antora/antora-ui-default/-/raw/master/src/js/06-copy-to-clipboard.js into your output directory and rename it to copy-to-clipboard.js
  2. Download the icons SVG https://gitlab.com/antora/antora-ui-default/-/raw/master/src/img/octicons-16.svg into the img folder of the output directory (i.e, img/octicons-16.svg)
  3. Add [.doc] above your document title. The script assumes that the CSS class "doc" is set on the content container.
  4. Define the document attribute :docinfo: shared-footer in the document header to enable the docinfo footer.
  5. Create the file docinfo-footer.html in the source directory (which may be the output directory) and populate it with the following contents:
<style>
.doc .listingblock > .content {
  position: relative;
}

.doc .listingblock code[data-lang]::before {
  content: none;
}

.doc .source-toolbox {
  display: flex;
  position: absolute;
  visibility: hidden;
  top: 0.25rem;
  right: 0.5rem;
  color: #808080;
  white-space: nowrap;
  font-size: 0.85em;
}

.doc .listingblock:hover .source-toolbox {
  visibility: visible;
}

.doc .source-toolbox .source-lang {
  font-family: "Droid Sans Mono", "DejaVu Sans Mono", monospace;
  text-transform: uppercase;
  letter-spacing: 0.075em;
}

.doc .source-toolbox > :not(:last-child)::after {
  content: "|";
  letter-spacing: 0;
  padding: 0 1ch;
}

.doc .source-toolbox .copy-button {
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  background: none;
  border: none;
  color: inherit;
  outline: none;
  padding: 0;
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  width: 1em;
  height: 1em;
}

.doc .source-toolbox .copy-icon {
  flex: none;
  width: inherit;
  height: inherit;
  filter: invert(50.2%);
  margin-top: 0.05em;
}

.doc .source-toolbox .copy-toast {
  flex: none;
  position: relative;
  display: inline-flex;
  justify-content: center;
  margin-top: 1em;
  border-radius: 0.25em;
  padding: 0.5em;
  cursor: auto;
  opacity: 0;
  transition: opacity 0.5s ease 0.75s;
  background: rgba(0, 0, 0, 0.8);
  color: #fff;
}

.doc .source-toolbox .copy-toast::after {
  content: "";
  position: absolute;
  top: 0;
  width: 1em;
  height: 1em;
  border: 0.55em solid transparent;
  border-left-color: rgba(0, 0, 0, 0.8);
  transform: rotate(-90deg) translateX(50%) translateY(50%);
  transform-origin: left;
}

.doc .source-toolbox .copy-button.clicked .copy-toast {
  opacity: 1;
  transition: none;
}
</style>
<script src="copy-to-clipboard.js"></script>

Now convert the document using Asciidoctor.

There's a fair amount of CSS required to support this feature since it needs to be placed very specifically within the listing block and react to the interaction.

Please note that the clipboard API is not available when the file is served over http unless the host is localhost.

I've gone ahead and added this extension to the lab.

Hi,

i'm using VS Code, is there something new about it ?

Thanks.

====

I tried this and it worked perfectly with a navigator (not with vscode neither AsciidocFX. I hope it will added by default.