Skip to content

Commit e703624

Browse files
authored
Merge pull request #1861 from cuthbertLab/fix-copybutton
Fix copybutton in docs
2 parents 308d84c + 5fc644c commit e703624

1 file changed

Lines changed: 169 additions & 59 deletions

File tree

Lines changed: 169 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,174 @@
11
// found in the _themes/m21/static folder
2+
// MSAC: I can't remember where this came from, but in 2026 rewritten
3+
// to use modern JS and no jQuery
24

3-
$(document).ready(function() {
4-
/* Add a [>>>] button on the top-right corner of code samples to hide
5-
* the >>> and ... prompts and the output and thus make the code
6-
* copyable. */
7-
var div = $('.highlight-python .highlight,' +
8-
'.highlight-python3 .highlight,' +
9-
'.highlight-default .highlight');
10-
var pre = div.find('pre');
5+
/** Add a [>>>] button on the top-right corner of code samples to hide
6+
* the >>> and ... prompts and the output and thus make the code
7+
* copyable. */
8+
document.addEventListener('DOMContentLoaded', () => {
9+
const divs = document.querySelectorAll(
10+
'.highlight-python .highlight,'
11+
+ '.highlight-python3 .highlight,'
12+
+ '.highlight-default .highlight'
13+
);
1114

12-
// get the styles from the current theme
13-
pre.parent().parent().css('position', 'relative');
14-
var hide_text = 'Hide the prompts and output';
15-
var show_text = 'Show the prompts and output';
16-
var border_width = pre.css('border-top-width');
17-
var border_style = pre.css('border-top-style');
18-
var border_color = pre.css('border-top-color');
19-
var button_styles = {
20-
'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0',
21-
'border-color': border_color, 'border-style': border_style,
22-
'border-width': border_width, 'color': border_color, 'text-size': '75%',
23-
'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em',
24-
'border-radius': '0 3px 0 0'
25-
}
15+
// We take the first <pre> we find (if any) to read theme styles
16+
// and apply them to the buttons
17+
let firstPre = null;
18+
for (const this_div of divs) {
19+
const maybePre = this_div.querySelector('pre');
20+
if (maybePre) {
21+
firstPre = maybePre;
22+
break;
23+
}
24+
}
2625

27-
// create and add the button to all the code blocks that contain >>>
28-
div.each(function(index) {
29-
var jthis = $(this);
30-
if (jthis.find('.gp').length > 0) {
31-
var button = $('<span class="copybutton">&gt;&gt;&gt;</span>');
32-
button.css(button_styles)
33-
button.attr('title', hide_text);
34-
button.data('hidden', 'false');
35-
jthis.prepend(button);
36-
}
37-
// tracebacks (.gt) contain bare text elements that need to be
38-
// wrapped in a span to work with .nextUntil() (see later)
39-
jthis.find('pre:has(.gt)').contents().filter(function() {
40-
return ((this.nodeType == 3) && (this.data.trim().length > 0));
41-
}).wrap('<span>');
42-
});
26+
// get the styles from the current theme
27+
if (firstPre && firstPre.parentElement && firstPre.parentElement.parentElement) {
28+
firstPre.parentElement.parentElement.style.position = 'relative';
29+
}
30+
const hide_text = 'Hide the prompts and output';
31+
const show_text = 'Show the prompts and output';
4332

44-
// define the behavior of the button when it's clicked
45-
$('.copybutton').click(function(e){
46-
e.preventDefault();
47-
var button = $(this);
48-
if (button.data('hidden') === 'false') {
49-
// hide the code output
50-
button.parent().find('.go, .gp, .gt').hide();
51-
button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden');
52-
button.css('text-decoration', 'line-through');
53-
button.attr('title', show_text);
54-
button.data('hidden', 'true');
55-
} else {
56-
// show the code output
57-
button.parent().find('.go, .gp, .gt').show();
58-
button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible');
59-
button.css('text-decoration', 'none');
60-
button.attr('title', hide_text);
61-
button.data('hidden', 'false');
62-
}
63-
});
64-
});
33+
let border_width = '';
34+
let border_style = '';
35+
let border_color = '';
36+
if (firstPre) {
37+
const cs = window.getComputedStyle(firstPre);
38+
border_width = cs.borderTopWidth;
39+
border_style = cs.borderTopStyle;
40+
border_color = cs.borderTopColor;
41+
}
42+
43+
function apply_button_styles(button) {
44+
button.style.cursor = 'pointer';
45+
button.style.position = 'absolute';
46+
button.style.top = '0';
47+
button.style.right = '0';
48+
button.style.borderColor = border_color;
49+
button.style.borderStyle = border_style;
50+
button.style.borderWidth = border_width;
51+
button.style.color = border_color;
52+
button.style.fontSize = '75%';
53+
button.style.fontFamily = 'monospace';
54+
button.style.paddingLeft = '0.2em';
55+
button.style.paddingRight = '0.2em';
56+
button.style.borderRadius = '0 3px 0 0';
57+
}
58+
59+
function hide_elements(parent, selector) {
60+
const els = parent.querySelectorAll(selector);
61+
for (const el of els) {
62+
el.style.display = 'none';
63+
}
64+
}
65+
66+
function show_elements(parent, selector) {
67+
const els = parent.querySelectorAll(selector);
68+
for (const el of els) {
69+
el.style.display = '';
70+
}
71+
}
72+
73+
function set_traceback_visibility(pre, visible) {
74+
// Equivalent to: button.next('pre').find('.gt').nextUntil('.gp, .go')...
75+
const gts = pre.querySelectorAll('.gt');
76+
for (const gt of gts) {
77+
let n = gt.nextSibling;
78+
while (n) {
79+
if (n.nodeType === Node.ELEMENT_NODE) {
80+
const el = n;
81+
if (el.classList.contains('gp') || el.classList.contains('go')) {
82+
break;
83+
}
84+
el.style.visibility = visible ? 'visible' : 'hidden';
85+
}
86+
n = n.nextSibling;
87+
}
88+
}
89+
}
90+
91+
/**
92+
* find the next sibling that is a <pre> tag.
93+
*/
94+
function next_pre_sibling(startEl) {
95+
let next = startEl.nextElementSibling;
96+
while (next && next.tagName.toLowerCase() !== 'pre') {
97+
next = next.nextElementSibling;
98+
}
99+
return next;
100+
}
101+
102+
// create and add the button to all the code blocks that contain >>>
103+
for (const this_div of divs) {
104+
// get the styles from the current theme (per-block positioning like before)
105+
const pre = this_div.querySelector('pre');
106+
if (pre && pre.parentElement && pre.parentElement.parentElement) {
107+
pre.parentElement.parentElement.style.position = 'relative';
108+
}
109+
110+
if (this_div.querySelectorAll('.gp').length > 0) {
111+
const button = document.createElement('span');
112+
button.className = 'copy_button';
113+
button.textContent = '>>>';
114+
button.setAttribute('role', 'button');
115+
button.setAttribute('tabindex', '0');
116+
apply_button_styles(button);
117+
button.setAttribute('title', hide_text);
118+
button.setAttribute('aria-pressed', 'false');
119+
this_div.insertBefore(button, this_div.firstChild);
120+
}
121+
122+
// tracebacks (.gt) contain bare text elements that need to be
123+
// wrapped in a span to work with .nextUntil() (see later)
124+
const preWithGt = this_div.querySelectorAll('pre');
125+
for (const preNode of preWithGt) {
126+
if (preNode.querySelector('.gt')) {
127+
const contents = Array.from(preNode.childNodes);
128+
for (const node of contents) {
129+
if ((node.nodeType === Node.TEXT_NODE) && node.data.trim()) {
130+
const span = document.createElement('span');
131+
span.textContent = node.data;
132+
preNode.replaceChild(span, node);
133+
}
134+
}
135+
}
136+
}
137+
}
138+
139+
// define the behavior of the button when it's clicked
140+
const buttons = document.querySelectorAll('.copy_button');
141+
for (const button of buttons) {
142+
button.addEventListener('click', e => {
143+
e.preventDefault();
144+
const parent = button.parentNode;
145+
const pre = next_pre_sibling(button);
146+
if (button.getAttribute('aria-pressed') === 'false') {
147+
// hide the code output
148+
hide_elements(parent, '.go, .gp, .gt');
149+
if (pre) {
150+
set_traceback_visibility(pre, false);
151+
}
152+
button.style.textDecoration = 'line-through';
153+
button.setAttribute('title', show_text);
154+
button.setAttribute('aria-pressed', 'true');
155+
} else {
156+
// show the code output
157+
show_elements(parent, '.go, .gp, .gt');
158+
if (pre) {
159+
// pre is the same thing jQuery would return for .next('pre')
160+
set_traceback_visibility(pre, true);
161+
}
162+
button.style.textDecoration = 'none';
163+
button.setAttribute('title', hide_text);
164+
button.setAttribute('aria-pressed', 'false');
165+
}
166+
});
167+
button.addEventListener('keydown', e => {
168+
if (e.key === 'Enter' || e.key === ' ') {
169+
e.preventDefault();
170+
button.click();
171+
}
172+
});
173+
}
174+
});

0 commit comments

Comments
 (0)