Roundup Tracker

Copy issue reference to clipboard enhancement

Originally published at: https://dev.to/rouilj/copy-issue-reference-to-clipboard-enhancement-4ka1.

On the Roundup Issue Tracker mailing list a user asked for a mechanism to create a reference for the current issue and copy it to the clipboard.

The request was for the reference to look like:

  issue2345: title of issue

In Roundup, object designators like issue2345 are automatically hyperlinked to the corresponding object.

The code I recommended used a button to trigger this operation. It also used the clipboard API. When clicking on the button (or hitting space or return while focused on the button) the reference format above will be copied to the clipboard. Triggering the clipboard API must be done by a user interaction. For example: activating a button. If the browser does not support the clipboard API, a simple alert() is shown.

In addition to copying to the clipboard, the user gets feedback when the text of the button changes to "Reference copied". Then it resets to the original message after 2 seconds. Since clicking the button multiple times is idempotent, there is no sense in disabling or debouncing the button.

Note, this is unlikely to be a11y compliant as I don't think the change of button text is announced. If anybody has some ideas on how to make this more compliant, leave them in the comments. Maybe using aria-live="polite" or "assertive" on the button element would do the trick?

Here is the code I suggested:

<button id="copyreference" type="button"
            tal:condition="context/is_edit_ok">
   Copy Reference
</button>

<script tal:attributes="nonce request/client/client_nonce">
  (function () {
    "use strict";

    let crb = document.querySelector("#copyreference");
    if ( ! crb ) return
    crb.addEventListener("click", async (e) => {
        e.preventDefault()
        if ( ! navigator.clipboard ) {
            alert("Clipboard is not available")
            return
        }

        let issueDesignator = new URL(document.URL)
            .pathname
            .split("/")
            .pop()
        let issueTitleText = document.querySelector("#title").value;

        await navigator.clipboard.writeText(`${issueDesignator}: ${issueTitleText}`)

        let originalCrbInnerText = crb.innerText
        crb.innerText = "Reference copied"
        setTimeout(() => {
            crb.innerText = originalCrbInnerText}, 2000)
        }
    );
  })();

The flexibility of Roundup allows the administrator to rewrite all of the HTML used in the web interface. As a result, I based my guess of CSS selectors on the HTML generated by the classic `issue.item.html` TAL template.

The button could be placed anywhere on the page and the script (including the nonce required by the CSP) would be placed anywhere after the button. Probably at the end of the page so it doesn't block rendering.

The classic structure of the issue display page for users with editing capability included an input with the id title. This is retrieved using the id and included in the string written to the clipboard.

A user without editing capability for the issue (or without editing capability for the title attribute of an issue) does not have an input with id="title". In these cases, the admin would have to modify the issue.item.html template to add an id of title to the enclosing element. Then the code above would be modified to replace:

let issueTitleText = document.querySelector("#title").value;

with:

 let issueTitle = document.querySelector("input#title");
   if ( ! issueTitle ) {
     issueTitle = document.querySelector("#title");
     issueTitleText = issueTitle.innerText
   } else {
     issueTitleText = issueTitle.value
   }

In my original example, the button element is generated only if the user can edit the issue. Removing the tal:condition attribute would always display the button.

There is nothing with an id or CSS selector that contains the object designator. I use the final element of the path of the URL to get the designator.

I chose to use the IIFE code structure. This allows me to use the early return pattern if the button is not found. If anybody knows how to do the equivalent of an early return without an IIFE or other function in a script tag, leave your trick in the comments.