From 03d799da30e00cb880e98ad70672a0da3b3a0dee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:43:44 +0000 Subject: [PATCH 1/6] Initial plan From 93a070cc3e0f7bead6ec07d28c2fdd46b74c9d24 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:53:02 +0000 Subject: [PATCH 2/6] Add copy buttons for install and citation blocks --- assets/js/bioconductor.js | 3 + assets/js/code_blocks.js | 118 +++++++++++++++++- assets/style/components/code.css | 67 ++++++++++ content/install.html | 22 ++-- layouts/_bioc_views_package_detail.html | 13 +- layouts/components/packages/installation.html | 2 +- 6 files changed, 210 insertions(+), 15 deletions(-) diff --git a/assets/js/bioconductor.js b/assets/js/bioconductor.js index 6c86338b3..6c63fc67a 100644 --- a/assets/js/bioconductor.js +++ b/assets/js/bioconductor.js @@ -342,6 +342,9 @@ var handleCitations = function () { data = data.replace(" (????)", ""); jQuery("#bioc-citation").html(data); + if (window.initializeCopyButtons) { + window.initializeCopyButtons(); + } jQuery("#bioc-citation-outer").show(); }, error: function (data, textStatus, jqXHR) { diff --git a/assets/js/code_blocks.js b/assets/js/code_blocks.js index e59847f4c..8bf2678af 100644 --- a/assets/js/code_blocks.js +++ b/assets/js/code_blocks.js @@ -1,7 +1,123 @@ -document.addEventListener("DOMContentLoaded", () => { +const copyButtonIcon = ` + +`; + +const trimCodeBlocks = () => { Array.from(document.getElementsByTagName("code")).forEach((block) => { block.innerHTML = block.innerHTML.trim(); }); +}; + +const getCopyText = (button) => { + const copyTarget = button.dataset.copyTarget; + + if (copyTarget) { + const target = document.querySelector(copyTarget); + + if (target) { + return (target.innerText || target.textContent || "").trim(); + } + } + + const codeBlock = button.closest("pre")?.querySelector("code"); + + if (codeBlock) { + return (codeBlock.innerText || codeBlock.textContent || "").trim(); + } + + return ""; +}; + +const setCopyButtonState = (button, copied) => { + const defaultLabel = button.dataset.copyLabel || "Copy"; + const successLabel = button.dataset.copySuccessLabel || "Copied"; + const label = copied ? successLabel : defaultLabel; + + button.classList.toggle("copied", copied); + button.setAttribute("aria-label", label); + button.setAttribute("title", label); + + const text = button.querySelector(".copy-button-label"); + + if (text) { + text.textContent = label; + } +}; + +const fallbackCopyText = (text) => { + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.setAttribute("readonly", ""); + textArea.className = "sr-only"; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand("copy"); + document.body.removeChild(textArea); +}; +const handleCopyClick = async (event) => { + const button = event.currentTarget; + const text = getCopyText(button); + + if (!text) { + return; + } + + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + } else { + fallbackCopyText(text); + } + + setCopyButtonState(button, true); + window.setTimeout(() => setCopyButtonState(button, false), 2000); + } catch (error) { + setCopyButtonState(button, false); + } +}; + +const initializeCopyButton = (button) => { + if (button.dataset.copyButtonReady === "true") { + return; + } + + button.type = "button"; + button.classList.add("copy-button"); + + if (!button.querySelector(".copy-button-label")) { + button.innerHTML = `${copyButtonIcon}${button.dataset.copyLabel || "Copy"}`; + } + + setCopyButtonState(button, false); + button.addEventListener("click", handleCopyClick); + button.dataset.copyButtonReady = "true"; +}; + +const addCodeCopyButton = (pre) => { + if (pre.querySelector(".copy-button")) { + return; + } + + const button = document.createElement("button"); + button.className = "copy-button code-copy-button"; + button.dataset.copyLabel = "Copy"; + button.dataset.copySuccessLabel = "Copied"; + pre.appendChild(button); + initializeCopyButton(button); +}; + +const initializeCopyButtons = () => { + document.querySelectorAll("pre[data-copyable]").forEach(addCodeCopyButton); + document.querySelectorAll(".copy-button[data-copy-target]").forEach(initializeCopyButton); +}; + +document.addEventListener("DOMContentLoaded", () => { + trimCodeBlocks(); + initializeCopyButtons(); window.hljs.highlightAll(); }); + +window.initializeCopyButtons = initializeCopyButtons; diff --git a/assets/style/components/code.css b/assets/style/components/code.css index cf864611b..8c65c0dd6 100644 --- a/assets/style/components/code.css +++ b/assets/style/components/code.css @@ -4,6 +4,7 @@ pre:has(code) { background: var(--gradient-brandreverse); white-space: normal; margin-bottom: 2rem; + position: relative; } code * { @@ -114,3 +115,69 @@ pre code.light .hljs-title { overflow-x:auto; white-space:pre; } + +.copy-button { + align-items: center; + background: rgb(255 255 255 / 12%); + border: 1px solid rgb(255 255 255 / 30%); + border-radius: 999px; + color: white; + cursor: pointer; + display: inline-flex; + gap: 0.35rem; + line-height: 1; + padding: 0.4rem 0.7rem; + transition: background 0.2s ease; +} + +.copy-button:hover, +.copy-button:focus { + background: rgb(255 255 255 / 20%); +} + +.copy-button.copied { + background: var(--primary-p400); + border-color: var(--primary-p400); +} + +.copy-button svg { + fill: currentcolor; + height: 0.9rem; + width: 0.9rem; +} + +.code-copy-button { + position: absolute; + right: 0.75rem; + top: 0.75rem; + z-index: 1; +} + +.copy-button-label { + font-size: 0.85rem; + font-family: inherit; +} + +.bioc-citation-header { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.bioc-citation-header .copy-button { + background: var(--primary-p400); + border-color: var(--primary-p400); +} + +.bioc-citation-header .copy-button:hover, +.bioc-citation-header .copy-button:focus { + background: var(--primary-p500); +} + +@media (prefers-reduced-motion: reduce) { + .copy-button { + transition: none; + } +} diff --git a/content/install.html b/content/install.html index 515a45eb6..0487ac726 100644 --- a/content/install.html +++ b/content/install.html @@ -2,7 +2,7 @@

Install Bioconductor Packages

To install core packages, type the following in an R command window:

-
+  
 
 if (!require("BiocManager", quietly = TRUE))
   install.packages("BiocManager")
@@ -12,7 +12,7 @@ 

Install Bioconductor Packages

To install core packages, type the following in an R command window:

-
+  
 
 BiocManager::install(c("GenomicFeatures", "AnnotationDbi"))
 
@@ -38,7 +38,7 @@ 

Find Bioconductor Packages

To search through available packages programmatically, use the following:

-
+  
 
 BiocManager::available()
 
@@ -59,7 +59,7 @@ 

version of Bioconductor, start a new session of R and enter:

-
+  
 
 BiocManager::install()
 
@@ -78,7 +78,7 @@ 
Updgrading Installed Bioconductor Packages
for your version of R, enter:

-
+  
 
 if (!require("BiocManager", quietly = TRUE))
     install.packages("BiocManager")
@@ -113,7 +113,7 @@ 
Recompiling Installed Bioconductor Packages
to address this might be to start a new R session and enter:

-
+  
 
 if (!require("BiocManager", quietly = TRUE))
     install.packages("BiocManager")
@@ -136,7 +136,7 @@ 

Use the commands:

-
+  
 
 BiocManager::valid()     ## R version 3.5 or later
 
@@ -212,7 +212,7 @@ 

Why use BiocManager::install()?

R in use regardless of the R and Bioconductor release cycles.

-
+  
 
 > library(BiocManager)
 Bioconductor version 3.9 (BiocManager 1.30.4), ?BiocManager::install
@@ -233,7 +233,7 @@ 

Why use BiocManager::install()?

would like to update.

-
+  
 
 > BiocManager::install()
 Bioconductor version 3.9 (BiocManager 1.30.4), R 3.6.0 Patched
@@ -250,7 +250,7 @@ 

Why use BiocManager::install()?

version of Bioconductor.

-
+  
 
 > BiocManager::install(version = "devel")
 Upgrade 89 packages to Bioconductor version '3.10'? [y/n]: y
@@ -286,7 +286,7 @@ 

Why use BiocManager::install()?

a repository; regular users would seldom have these).

-
+  
 
 > BiocManager::valid()
 
diff --git a/layouts/_bioc_views_package_detail.html b/layouts/_bioc_views_package_detail.html
index 4575078e6..b8f977f37 100644
--- a/layouts/_bioc_views_package_detail.html
+++ b/layouts/_bioc_views_package_detail.html
@@ -35,7 +35,17 @@ 

<%= @package[:Title]%>

Maintainer: <%= filter_emails(@package[:Maintainer])%>

- Citation (from within R, enter citation("<%=@package[:Package]%>")): +
+ Citation (from within R, enter citation("<%=@package[:Package]%>")): + +
@@ -47,4 +57,3 @@

<%= @package[:Title]%>

<%= render("/components/packages/details/", :package => @package) %> <%= render("/components/packages/archives/", :package => @package) %> - diff --git a/layouts/components/packages/installation.html b/layouts/components/packages/installation.html index efe4469bb..d14e355bb 100644 --- a/layouts/components/packages/installation.html +++ b/layouts/components/packages/installation.html @@ -4,7 +4,7 @@

Installation

To install this package, start R (version "<%= r_ver_for_bioc_ver(@package[:bioc_version_num]) %>") and enter:

-

+

 if (!require("BiocManager", quietly = TRUE))
     install.packages("BiocManager")
 <% if (@package[:bioc_version_str] == "Development") %>

From 18bb54aa2f218e849f5a9ab1f14b1c7bf96cc180 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 17 Jul 2026 14:54:28 +0000
Subject: [PATCH 3/6] Handle copy button success and error states

---
 assets/js/code_blocks.js         | 24 ++++++++++++++++--------
 assets/style/components/code.css |  5 +++++
 2 files changed, 21 insertions(+), 8 deletions(-)

diff --git a/assets/js/code_blocks.js b/assets/js/code_blocks.js
index 8bf2678af..b4869b4d3 100644
--- a/assets/js/code_blocks.js
+++ b/assets/js/code_blocks.js
@@ -30,12 +30,19 @@ const getCopyText = (button) => {
   return "";
 };
 
-const setCopyButtonState = (button, copied) => {
+const setCopyButtonState = (button, state = "default") => {
   const defaultLabel = button.dataset.copyLabel || "Copy";
   const successLabel = button.dataset.copySuccessLabel || "Copied";
-  const label = copied ? successLabel : defaultLabel;
-
-  button.classList.toggle("copied", copied);
+  const errorLabel = button.dataset.copyErrorLabel || "Copy failed";
+  const label =
+    state === "copied"
+      ? successLabel
+      : state === "error"
+        ? errorLabel
+        : defaultLabel;
+
+  button.classList.toggle("copied", state === "copied");
+  button.classList.toggle("copy-error", state === "error");
   button.setAttribute("aria-label", label);
   button.setAttribute("title", label);
 
@@ -72,10 +79,11 @@ const handleCopyClick = async (event) => {
       fallbackCopyText(text);
     }
 
-    setCopyButtonState(button, true);
-    window.setTimeout(() => setCopyButtonState(button, false), 2000);
-  } catch (error) {
-    setCopyButtonState(button, false);
+    setCopyButtonState(button, "copied");
+    window.setTimeout(() => setCopyButtonState(button), 2000);
+  } catch {
+    setCopyButtonState(button, "error");
+    window.setTimeout(() => setCopyButtonState(button), 2000);
   }
 };
 
diff --git a/assets/style/components/code.css b/assets/style/components/code.css
index 8c65c0dd6..2a65a32dc 100644
--- a/assets/style/components/code.css
+++ b/assets/style/components/code.css
@@ -140,6 +140,11 @@ pre code.light .hljs-title {
   border-color: var(--primary-p400);
 }
 
+.copy-button.copy-error {
+  background: var(--error-e500);
+  border-color: var(--error-e500);
+}
+
 .copy-button svg {
   fill: currentcolor;
   height: 0.9rem;

From 75eb6973e1c911b28f10f4667c99b8d27661a1d2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 17 Jul 2026 14:55:39 +0000
Subject: [PATCH 4/6] Tighten copy fallback handling

---
 assets/js/code_blocks.js | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/assets/js/code_blocks.js b/assets/js/code_blocks.js
index b4869b4d3..42f7152e2 100644
--- a/assets/js/code_blocks.js
+++ b/assets/js/code_blocks.js
@@ -60,8 +60,12 @@ const fallbackCopyText = (text) => {
   textArea.className = "sr-only";
   document.body.appendChild(textArea);
   textArea.select();
-  document.execCommand("copy");
+  const copied = document.execCommand("copy");
   document.body.removeChild(textArea);
+
+  if (!copied) {
+    throw new Error("Copy command failed");
+  }
 };
 
 const handleCopyClick = async (event) => {
@@ -99,7 +103,7 @@ const initializeCopyButton = (button) => {
     button.innerHTML = `${copyButtonIcon}${button.dataset.copyLabel || "Copy"}`;
   }
 
-  setCopyButtonState(button, false);
+  setCopyButtonState(button);
   button.addEventListener("click", handleCopyClick);
   button.dataset.copyButtonReady = "true";
 };

From 5d8656de052448363ecc0b666fd66e0e42af4234 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 17 Jul 2026 14:56:44 +0000
Subject: [PATCH 5/6] Polish copy button state logic

---
 assets/js/code_blocks.js | 20 +++++++++++---------
 1 file changed, 11 insertions(+), 9 deletions(-)

diff --git a/assets/js/code_blocks.js b/assets/js/code_blocks.js
index 42f7152e2..1fba1ae3c 100644
--- a/assets/js/code_blocks.js
+++ b/assets/js/code_blocks.js
@@ -3,6 +3,7 @@ const copyButtonIcon = `
     
   
 `;
+const defaultCopyLabel = "Copy";
 
 const trimCodeBlocks = () => {
   Array.from(document.getElementsByTagName("code")).forEach((block) => {
@@ -31,15 +32,16 @@ const getCopyText = (button) => {
 };
 
 const setCopyButtonState = (button, state = "default") => {
-  const defaultLabel = button.dataset.copyLabel || "Copy";
+  const defaultLabel = button.dataset.copyLabel || defaultCopyLabel;
   const successLabel = button.dataset.copySuccessLabel || "Copied";
   const errorLabel = button.dataset.copyErrorLabel || "Copy failed";
-  const label =
-    state === "copied"
-      ? successLabel
-      : state === "error"
-        ? errorLabel
-        : defaultLabel;
+  let label = defaultLabel;
+
+  if (state === "copied") {
+    label = successLabel;
+  } else if (state === "error") {
+    label = errorLabel;
+  }
 
   button.classList.toggle("copied", state === "copied");
   button.classList.toggle("copy-error", state === "error");
@@ -100,7 +102,7 @@ const initializeCopyButton = (button) => {
   button.classList.add("copy-button");
 
   if (!button.querySelector(".copy-button-label")) {
-    button.innerHTML = `${copyButtonIcon}${button.dataset.copyLabel || "Copy"}`;
+    button.innerHTML = `${copyButtonIcon}${button.dataset.copyLabel || defaultCopyLabel}`;
   }
 
   setCopyButtonState(button);
@@ -115,7 +117,7 @@ const addCodeCopyButton = (pre) => {
 
   const button = document.createElement("button");
   button.className = "copy-button code-copy-button";
-  button.dataset.copyLabel = "Copy";
+  button.dataset.copyLabel = defaultCopyLabel;
   button.dataset.copySuccessLabel = "Copied";
   pre.appendChild(button);
   initializeCopyButton(button);

From 5c194ea86cd99db0155956d5fb9ba4833b9322d4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 17 Jul 2026 14:57:58 +0000
Subject: [PATCH 6/6] Harden copy button fallback behavior

---
 assets/js/code_blocks.js         | 9 ++++++++-
 assets/style/components/code.css | 5 +++++
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/assets/js/code_blocks.js b/assets/js/code_blocks.js
index 1fba1ae3c..64265c6f0 100644
--- a/assets/js/code_blocks.js
+++ b/assets/js/code_blocks.js
@@ -4,6 +4,7 @@ const copyButtonIcon = `
   
 `;
 const defaultCopyLabel = "Copy";
+let copyFallbackWarningShown = false;
 
 const trimCodeBlocks = () => {
   Array.from(document.getElementsByTagName("code")).forEach((block) => {
@@ -59,9 +60,15 @@ const fallbackCopyText = (text) => {
   const textArea = document.createElement("textarea");
   textArea.value = text;
   textArea.setAttribute("readonly", "");
-  textArea.className = "sr-only";
+  textArea.style.position = "fixed";
+  textArea.style.left = "-9999px";
+  textArea.style.top = "0";
   document.body.appendChild(textArea);
   textArea.select();
+  if (!copyFallbackWarningShown) {
+    console.warn("Falling back to document.execCommand('copy').");
+    copyFallbackWarningShown = true;
+  }
   const copied = document.execCommand("copy");
   document.body.removeChild(textArea);
 
diff --git a/assets/style/components/code.css b/assets/style/components/code.css
index 2a65a32dc..06ac75785 100644
--- a/assets/style/components/code.css
+++ b/assets/style/components/code.css
@@ -23,6 +23,11 @@ pre code.hljs {
   font-family: Courier, monospace;
 }
 
+pre[data-copyable] code,
+pre[data-copyable] code.hljs {
+  padding-top: 3rem;
+}
+
 pre code,
 pre code.hljs,
 .hljs-punctuation {