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..64265c6f0 100644 --- a/assets/js/code_blocks.js +++ b/assets/js/code_blocks.js @@ -1,7 +1,144 @@ -document.addEventListener("DOMContentLoaded", () => { +const copyButtonIcon = ` + +`; +const defaultCopyLabel = "Copy"; +let copyFallbackWarningShown = false; + +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, state = "default") => { + const defaultLabel = button.dataset.copyLabel || defaultCopyLabel; + const successLabel = button.dataset.copySuccessLabel || "Copied"; + const errorLabel = button.dataset.copyErrorLabel || "Copy failed"; + 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"); + 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.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); + if (!copied) { + throw new Error("Copy command failed"); + } +}; + +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, "copied"); + window.setTimeout(() => setCopyButtonState(button), 2000); + } catch { + setCopyButtonState(button, "error"); + window.setTimeout(() => setCopyButtonState(button), 2000); + } +}; + +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 || defaultCopyLabel}`; + } + + setCopyButtonState(button); + 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 = defaultCopyLabel; + 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..06ac75785 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 * { @@ -22,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 { @@ -114,3 +120,74 @@ 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.copy-error { + background: var(--error-e500); + border-color: var(--error-e500); +} + +.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") %>