asciidoctor / asciidoctor-extensions-lab

A lab for testing and demonstrating Asciidoctor extensions. Please do not use this code in production. If you want to use one of these extensions in your application, create a new project, import the code, and distribute it as a RubyGem. You can then request to make it a top-level project under the Asciidoctor organization.
Other
105 stars 101 forks source link

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

Closed ggrossetie closed 11 months ago

ggrossetie commented 8 years ago

I think we should use https://github.com/zenorocha/clipboard.js

Inspiration: http://foundation.zurb.com/sites/docs/installation.html#command-line-tool

mojavelinux commented 8 years ago

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 6 years ago

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>
vogella commented 5 years ago

Any plans to provide this as a default option?

jnerlich commented 5 years ago

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

mojavelinux commented 5 years ago

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).

jnerlich commented 5 years ago

@mojavelinux thanks for the answer.

xstefank commented 4 years ago

Hi, is there any progress on this, please?

danyill commented 4 years ago

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.

danyill commented 4 years ago

To implement the approach shown above in https://github.com/asciidoctor/asciidoctor-extensions-lab/issues/61#issuecomment-385727145 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.

chuck-confluent commented 3 years ago

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?

schonbachler commented 2 years ago

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>
AshkanV commented 2 years ago

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>
mojavelinux commented 2 years ago

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.

mojavelinux commented 11 months ago

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