|
1 | 1 | // 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 |
2 | 4 |
|
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 | + ); |
11 | 14 |
|
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 | + } |
26 | 25 |
|
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">>>></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'; |
43 | 32 |
|
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