Skip to content

Commit d528dbc

Browse files
tomasr8hugovk
andauthored
Add a copy button to code samples (#231)
Co-authored-by: Hugo van Kemenade <[email protected]>
1 parent 6ac5c06 commit d528dbc

File tree

3 files changed

+76
-65
lines changed

3 files changed

+76
-65
lines changed

Diff for: python_docs_theme/static/copybutton.js

+49-57
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

Diff for: python_docs_theme/static/pydoctheme.css

+14-8
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) {

Diff for: python_docs_theme/static/pydoctheme_dark.css

+13
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)