Skip to content

Commit d528dbc

Browse files
tomasr8hugovk
andauthored
Add a copy button to code samples (#231)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
1 parent 6ac5c06 commit d528dbc

File tree

3 files changed

+76
-65
lines changed

3 files changed

+76
-65
lines changed

python_docs_theme/static/copybutton.js

Lines changed: 49 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,59 @@
1-
// ``function*`` denotes a generator in JavaScript, see
2-
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
3-
function* getHideableCopyButtonElements(rootElement) {
4-
// yield all elements with the "go" (Generic.Output),
5-
// "gp" (Generic.Prompt), or "gt" (Generic.Traceback) CSS class
6-
for (const el of rootElement.querySelectorAll('.go, .gp, .gt')) {
7-
yield el
8-
}
9-
// tracebacks (.gt) contain bare text elements that need to be
10-
// wrapped in a span to hide or show the element
11-
for (let el of rootElement.querySelectorAll('.gt')) {
12-
while ((el = el.nextSibling) && el.nodeType !== Node.DOCUMENT_NODE) {
13-
// stop wrapping text nodes when we hit the next output or
14-
// prompt element
15-
if (el.nodeType === Node.ELEMENT_NODE && el.matches(".gp, .go")) {
16-
break
17-
}
18-
// if the node is a text node with content, wrap it in a
19-
// span element so that we can control visibility
20-
if (el.nodeType === Node.TEXT_NODE && el.textContent.trim()) {
21-
const wrapper = document.createElement("span")
22-
el.after(wrapper)
23-
wrapper.appendChild(el)
24-
el = wrapper
25-
}
26-
yield el
1+
// Extract copyable text from the code block ignoring the
2+
// prompts and output.
3+
function getCopyableText(rootElement) {
4+
rootElement = rootElement.cloneNode(true)
5+
// tracebacks (.gt) contain bare text elements that
6+
// need to be removed
7+
const tracebacks = rootElement.querySelectorAll(".gt")
8+
for (const el of tracebacks) {
9+
while (
10+
el.nextSibling &&
11+
(el.nextSibling.nodeType !== Node.DOCUMENT_NODE ||
12+
!el.nextSibling.matches(".gp, .go"))
13+
) {
14+
el.nextSibling.remove()
2715
}
2816
}
17+
// Remove all elements with the "go" (Generic.Output),
18+
// "gp" (Generic.Prompt), or "gt" (Generic.Traceback) CSS class
19+
const elements = rootElement.querySelectorAll(".gp, .go, .gt")
20+
for (const el of elements) {
21+
el.remove()
22+
}
23+
return rootElement.innerText.trim()
2924
}
3025

31-
3226
const loadCopyButton = () => {
33-
/* Add a [>>>] button in the top-right corner of code samples to hide
34-
* the >>> and ... prompts and the output and thus make the code
35-
* copyable. */
36-
const hide_text = _("Hide the prompts and output")
37-
const show_text = _("Show the prompts and output")
38-
39-
const button = document.createElement("span")
27+
const button = document.createElement("button")
4028
button.classList.add("copybutton")
41-
button.innerText = ">>>"
42-
button.title = hide_text
43-
button.dataset.hidden = "false"
44-
const buttonClick = event => {
29+
button.type = "button"
30+
button.innerText = _("Copy")
31+
button.title = _("Copy to clipboard")
32+
33+
const makeOnButtonClick = () => {
34+
let timeout = null
4535
// define the behavior of the button when it's clicked
46-
event.preventDefault()
47-
const buttonEl = event.currentTarget
48-
const codeEl = buttonEl.nextElementSibling
49-
if (buttonEl.dataset.hidden === "false") {
50-
// hide the code output
51-
for (const el of getHideableCopyButtonElements(codeEl)) {
52-
el.hidden = true
36+
return async event => {
37+
// check if the clipboard is available
38+
if (!navigator.clipboard || !navigator.clipboard.writeText) {
39+
return;
5340
}
54-
buttonEl.title = show_text
55-
buttonEl.dataset.hidden = "true"
56-
} else {
57-
// show the code output
58-
for (const el of getHideableCopyButtonElements(codeEl)) {
59-
el.hidden = false
41+
42+
clearTimeout(timeout)
43+
const buttonEl = event.currentTarget
44+
const codeEl = buttonEl.nextElementSibling
45+
46+
try {
47+
await navigator.clipboard.writeText(getCopyableText(codeEl))
48+
} catch (e) {
49+
console.error(e.message)
50+
return
6051
}
61-
buttonEl.title = hide_text
62-
buttonEl.dataset.hidden = "false"
52+
53+
buttonEl.innerText = _("Copied!")
54+
timeout = setTimeout(() => {
55+
buttonEl.innerText = _("Copy")
56+
}, 1500)
6357
}
6458
}
6559

@@ -78,10 +72,8 @@ const loadCopyButton = () => {
7872
// if we find a console prompt (.gp), prepend the (deeply cloned) button
7973
const clonedButton = button.cloneNode(true)
8074
// the onclick attribute is not cloned, set it on the new element
81-
clonedButton.onclick = buttonClick
82-
if (el.querySelector(".gp") !== null) {
83-
el.prepend(clonedButton)
84-
}
75+
clonedButton.onclick = makeOnButtonClick()
76+
el.prepend(clonedButton)
8577
})
8678
}
8779

python_docs_theme/static/pydoctheme.css

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -442,17 +442,23 @@ div.genindex-jumpbox a {
442442
top: 0;
443443
right: 0;
444444
font-family: Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
445-
padding-left: 0.2em;
446-
padding-right: 0.2em;
445+
font-size: 80%;
446+
padding-left: .5em;
447+
padding-right: .5em;
448+
height: 100%;
449+
max-height: min(100%, 2.4em);
447450
border-radius: 0 3px 0 0;
448-
color: #ac9; /* follows div.body pre */
449-
border-color: #ac9; /* follows div.body pre */
450-
border-style: solid; /* follows div.body pre */
451-
border-width: 1px; /* follows div.body pre */
451+
color: #000;
452+
background-color: #fff;
453+
border: 1px solid #ac9; /* follows div.body pre */
454+
}
455+
456+
.copybutton:hover {
457+
background-color: #eee;
452458
}
453459

454-
.copybutton[data-hidden='true'] {
455-
text-decoration: line-through;
460+
.copybutton:active {
461+
background-color: #ddd;
456462
}
457463

458464
@media (max-width: 1023px) {

python_docs_theme/static/pydoctheme_dark.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,16 @@ img.invert-in-dark-mode {
176176
--versionchanged: var(--middle-color);
177177
--deprecated: var(--bad-color);
178178
}
179+
180+
.copybutton {
181+
color: #ac9; /* follows div.body pre */
182+
background-color: #222222; /* follows body */
183+
}
184+
185+
.copybutton:hover {
186+
background-color: #434343;
187+
}
188+
189+
.copybutton:active {
190+
background-color: #656565;
191+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy