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}`; + } + + 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 @@
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@@ -47,4 +57,3 @@citation("<%=@package[:Package]%>")): ++ Citation (from within R, entercitation("<%=@package[:Package]%>")): + +<%= @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}`; } - 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.innerHTML = `${copyButtonIcon}`; } 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 {