Skip to content

Commit 52fe6a9

Browse files
committed
Add a copy button to all code samples
1 parent b8b2d49 commit 52fe6a9

File tree

3 files changed

+89
-79
lines changed

3 files changed

+89
-79
lines changed
Lines changed: 52 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,70 @@
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")
27+
const button = document.createElement("button");
28+
button.classList.add("copybutton");
29+
button.type = "button";
30+
button.innerText = _("Copy");
31+
button.title = _("Copy code to clipboard");
3832

39-
const button = document.createElement("span")
40-
button.classList.add("copybutton")
41-
button.innerText = ">>>"
42-
button.title = hide_text
43-
button.dataset.hidden = "false"
44-
const buttonClick = event => {
33+
let timeout;
34+
const buttonClick = (event) => {
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
53-
}
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
60-
}
61-
buttonEl.title = hide_text
62-
buttonEl.dataset.hidden = "false"
63-
}
64-
}
36+
clearTimeout(timeout);
37+
const buttonEl = event.currentTarget;
38+
const codeEl = buttonEl.nextElementSibling;
39+
navigator.clipboard.writeText(getCopyableText(codeEl));
40+
buttonEl.innerText = _("Copied!");
41+
timeout = setTimeout(() => {
42+
buttonEl.innerText = _("Copy");
43+
}, 1500);
44+
};
6545

6646
const highlightedElements = document.querySelectorAll(
67-
".highlight-python .highlight,"
68-
+ ".highlight-python3 .highlight,"
69-
+ ".highlight-pycon .highlight,"
70-
+ ".highlight-pycon3 .highlight,"
71-
+ ".highlight-default .highlight"
72-
)
47+
".highlight-python .highlight," +
48+
".highlight-python3 .highlight," +
49+
".highlight-pycon .highlight," +
50+
".highlight-pycon3 .highlight," +
51+
".highlight-default .highlight"
52+
);
7353

7454
// create and add the button to all the code blocks that contain >>>
75-
highlightedElements.forEach(el => {
76-
el.style.position = "relative"
55+
highlightedElements.forEach((el) => {
56+
el.style.position = "relative";
7757

7858
// if we find a console prompt (.gp), prepend the (deeply cloned) button
79-
const clonedButton = button.cloneNode(true)
59+
const clonedButton = button.cloneNode(true);
8060
// 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-
}
85-
})
86-
}
61+
clonedButton.onclick = buttonClick;
62+
el.prepend(clonedButton);
63+
});
64+
};
8765

8866
if (document.readyState !== "loading") {
89-
loadCopyButton()
67+
loadCopyButton();
9068
} else {
91-
document.addEventListener("DOMContentLoaded", loadCopyButton)
69+
document.addEventListener("DOMContentLoaded", loadCopyButton);
9270
}

python_docs_theme/static/pydoctheme.css

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -442,17 +442,36 @@ 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+
height: 100%;
446+
max-height: min(100%, 32px);
447+
padding-left: 7px;
448+
padding-right: 7px;
447449
border-radius: 0 3px 0 0;
448-
color: #ac9; /* follows div.body pre */
449450
border-color: #ac9; /* follows div.body pre */
450451
border-style: solid; /* follows div.body pre */
451452
border-width: 1px; /* follows div.body pre */
453+
color: #000;
454+
background-color: #fff;
452455
}
453456

454-
.copybutton[data-hidden='true'] {
455-
text-decoration: line-through;
457+
.copybutton:hover {
458+
background-color: #eee;
459+
}
460+
461+
.copybutton:active {
462+
background-color: #ddd;
463+
}
464+
465+
/* div.highlight {
466+
border-radius: 3px;
467+
border: 1px solid #ac9;
468+
} */
469+
470+
div.highlight > pre {
471+
/* overwrite classi.css */
472+
line-height: 125%; /* to match pygments_dark.css */
473+
/* border: none;
474+
border-radius: 0; */
456475
}
457476

458477
@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;
183+
}
184+
185+
.copybutton:hover {
186+
background-color: #434343;
187+
}
188+
189+
.copybutton:active {
190+
background-color: #656565;
191+
}

0 commit comments

Comments
 (0)