b761b0b0fc3cd9e830db0f11a536e0da29d7c9e5
max
  Wed Sep 9 08:58:30 2026 -0700
A copy-to-clipboard button goes back to its own label after three seconds

Saying "Copied" and then keeping that as the label left no sign that the button
could be used again.  It now says "Copied" for three seconds and then puts back
its own label and icon.  A second copy while the message is up restarts the
three seconds rather than adopting "Copied" as the label to go back to.

The assembly search page carries its own borrowed copy of copyToClipboard and
does not load utils.js, so it gets the same treatment there.

refs #38294

diff --git src/hg/js/utils.js src/hg/js/utils.js
index 1295e16b42f..dad1b8f33cf 100644
--- src/hg/js/utils.js
+++ src/hg/js/utils.js
@@ -32,30 +32,45 @@
 function createInfoIcon(text) {
     /* Create an info icon (i in circle) with tooltip text.
      * Returns a span element containing the SVG icon.
      * Uses addMouseover() for consistent tooltip behavior. */
     var span = document.createElement("span");
     span.style.marginLeft = "4px";
     span.innerHTML = "<svg style='height:1.1em; vertical-align:top' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>" +
         "<circle cx='12' cy='12' r='10' stroke='#1C274C' stroke-width='1.5'/>" +
         "<path d='M12 17V11' stroke='#1C274C' stroke-width='1.5' stroke-linecap='round'/>" +
         "<circle cx='1' cy='1' r='1' transform='matrix(1 0 0 -1 11 9)' fill='#1C274C'/>" +
         "</svg>";
     addMouseover(span, text);
     return span;
 }
 
+function copyButtonSaysCopied(buttonEl) {
+    /* Say "Copied" on a copy-to-clipboard button, then put the button's own label back after three
+     * seconds, so it is clear that the button can be used again.  The label is remembered on the
+     * element itself, so a second copy while the message is up does not take "Copied" for it. */
+    if (buttonEl.copyButtonLabel === undefined)
+        buttonEl.copyButtonLabel = buttonEl.innerHTML;
+    if (buttonEl.copyButtonTimer)
+        clearTimeout(buttonEl.copyButtonTimer);
+    buttonEl.innerHTML = 'Copied';
+    buttonEl.copyButtonTimer = setTimeout(function() {
+        buttonEl.innerHTML = buttonEl.copyButtonLabel;
+        buttonEl.copyButtonTimer = null;
+    }, 3000);
+}
+
 function copyToClipboard(ev) {
     /* copy a piece of text to clipboard. event.target is some DIV or SVG that is an icon. 
      * The attribute data-target of this element is the ID of the element that contains the text to copy. 
      * The text is either in the attribute data-copy or the innerText.
      * see C function printCopyToClipboardButton(iconId, targetId);
      * Returns true if the text really reached the clipboard.  A browser will refuse a copy that no
      * click of the user's asked for, so a caller that copies on its own behalf has to check.
      * */
      
     ev.preventDefault();
 
     var buttonEl = ev.target.closest("button"); // user can click SVG or BUTTON element
 
     var targetId = buttonEl.getAttribute("data-target");
     if (targetId===null)
@@ -70,31 +85,31 @@
     // Avoid scrolling to bottom
     textArea.style.top = "0";
     textArea.style.left = "0";
     textArea.style.position = "fixed";
     document.body.appendChild(textArea);
     textArea.focus();
     textArea.select();
     var ok = false;
     try {
         ok = document.execCommand('copy');
     } catch (e) {
         ok = false;
     }
     document.body.removeChild(textArea);
     if (ok)
-        buttonEl.innerHTML = 'Copied';
+        copyButtonSaysCopied(buttonEl);
     return ok;
 }
 
 function cfgPageOnVisChange(ev) {
     /* configuration page event listener when user changes visibility in dropdown */
     if (ev.target.value === 'hide')
         ev.target.classList.replace("normalText", "hiddenText");
     else
         ev.target.classList.replace("hiddenText", "normalText");
 }
 
 function cfgPageAddListeners() {
     /* add event listener to dropdowns */
     var els = document.querySelectorAll(".trackVis");
     for (var i=0; i < els.length; i++) {