diff --git a/documentation/source/_themes/m21/static/copybutton.js b/documentation/source/_themes/m21/static/copybutton.js index c9f49770b1..71d8e99147 100644 --- a/documentation/source/_themes/m21/static/copybutton.js +++ b/documentation/source/_themes/m21/static/copybutton.js @@ -1,64 +1,174 @@ // found in the _themes/m21/static folder +// MSAC: I can't remember where this came from, but in 2026 rewritten +// to use modern JS and no jQuery -$(document).ready(function() { - /* Add a [>>>] button on the top-right corner of code samples to hide - * the >>> and ... prompts and the output and thus make the code - * copyable. */ - var div = $('.highlight-python .highlight,' + - '.highlight-python3 .highlight,' + - '.highlight-default .highlight'); - var pre = div.find('pre'); +/** Add a [>>>] button on the top-right corner of code samples to hide + * the >>> and ... prompts and the output and thus make the code + * copyable. */ +document.addEventListener('DOMContentLoaded', () => { + const divs = document.querySelectorAll( + '.highlight-python .highlight,' + + '.highlight-python3 .highlight,' + + '.highlight-default .highlight' + ); - // get the styles from the current theme - pre.parent().parent().css('position', 'relative'); - var hide_text = 'Hide the prompts and output'; - var show_text = 'Show the prompts and output'; - var border_width = pre.css('border-top-width'); - var border_style = pre.css('border-top-style'); - var border_color = pre.css('border-top-color'); - var button_styles = { - 'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0', - 'border-color': border_color, 'border-style': border_style, - 'border-width': border_width, 'color': border_color, 'text-size': '75%', - 'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em', - 'border-radius': '0 3px 0 0' - } + // We take the first
we find (if any) to read theme styles
+ // and apply them to the buttons
+ let firstPre = null;
+ for (const this_div of divs) {
+ const maybePre = this_div.querySelector('pre');
+ if (maybePre) {
+ firstPre = maybePre;
+ break;
+ }
+ }
- // create and add the button to all the code blocks that contain >>>
- div.each(function(index) {
- var jthis = $(this);
- if (jthis.find('.gp').length > 0) {
- var button = $('');
- button.css(button_styles)
- button.attr('title', hide_text);
- button.data('hidden', 'false');
- jthis.prepend(button);
- }
- // tracebacks (.gt) contain bare text elements that need to be
- // wrapped in a span to work with .nextUntil() (see later)
- jthis.find('pre:has(.gt)').contents().filter(function() {
- return ((this.nodeType == 3) && (this.data.trim().length > 0));
- }).wrap('');
- });
+ // get the styles from the current theme
+ if (firstPre && firstPre.parentElement && firstPre.parentElement.parentElement) {
+ firstPre.parentElement.parentElement.style.position = 'relative';
+ }
+ const hide_text = 'Hide the prompts and output';
+ const show_text = 'Show the prompts and output';
- // define the behavior of the button when it's clicked
- $('.copybutton').click(function(e){
- e.preventDefault();
- var button = $(this);
- if (button.data('hidden') === 'false') {
- // hide the code output
- button.parent().find('.go, .gp, .gt').hide();
- button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden');
- button.css('text-decoration', 'line-through');
- button.attr('title', show_text);
- button.data('hidden', 'true');
- } else {
- // show the code output
- button.parent().find('.go, .gp, .gt').show();
- button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible');
- button.css('text-decoration', 'none');
- button.attr('title', hide_text);
- button.data('hidden', 'false');
- }
- });
- });
+ let border_width = '';
+ let border_style = '';
+ let border_color = '';
+ if (firstPre) {
+ const cs = window.getComputedStyle(firstPre);
+ border_width = cs.borderTopWidth;
+ border_style = cs.borderTopStyle;
+ border_color = cs.borderTopColor;
+ }
+
+ function apply_button_styles(button) {
+ button.style.cursor = 'pointer';
+ button.style.position = 'absolute';
+ button.style.top = '0';
+ button.style.right = '0';
+ button.style.borderColor = border_color;
+ button.style.borderStyle = border_style;
+ button.style.borderWidth = border_width;
+ button.style.color = border_color;
+ button.style.fontSize = '75%';
+ button.style.fontFamily = 'monospace';
+ button.style.paddingLeft = '0.2em';
+ button.style.paddingRight = '0.2em';
+ button.style.borderRadius = '0 3px 0 0';
+ }
+
+ function hide_elements(parent, selector) {
+ const els = parent.querySelectorAll(selector);
+ for (const el of els) {
+ el.style.display = 'none';
+ }
+ }
+
+ function show_elements(parent, selector) {
+ const els = parent.querySelectorAll(selector);
+ for (const el of els) {
+ el.style.display = '';
+ }
+ }
+
+ function set_traceback_visibility(pre, visible) {
+ // Equivalent to: button.next('pre').find('.gt').nextUntil('.gp, .go')...
+ const gts = pre.querySelectorAll('.gt');
+ for (const gt of gts) {
+ let n = gt.nextSibling;
+ while (n) {
+ if (n.nodeType === Node.ELEMENT_NODE) {
+ const el = n;
+ if (el.classList.contains('gp') || el.classList.contains('go')) {
+ break;
+ }
+ el.style.visibility = visible ? 'visible' : 'hidden';
+ }
+ n = n.nextSibling;
+ }
+ }
+ }
+
+ /**
+ * find the next sibling that is a tag.
+ */
+ function next_pre_sibling(startEl) {
+ let next = startEl.nextElementSibling;
+ while (next && next.tagName.toLowerCase() !== 'pre') {
+ next = next.nextElementSibling;
+ }
+ return next;
+ }
+
+ // create and add the button to all the code blocks that contain >>>
+ for (const this_div of divs) {
+ // get the styles from the current theme (per-block positioning like before)
+ const pre = this_div.querySelector('pre');
+ if (pre && pre.parentElement && pre.parentElement.parentElement) {
+ pre.parentElement.parentElement.style.position = 'relative';
+ }
+
+ if (this_div.querySelectorAll('.gp').length > 0) {
+ const button = document.createElement('span');
+ button.className = 'copy_button';
+ button.textContent = '>>>';
+ button.setAttribute('role', 'button');
+ button.setAttribute('tabindex', '0');
+ apply_button_styles(button);
+ button.setAttribute('title', hide_text);
+ button.setAttribute('aria-pressed', 'false');
+ this_div.insertBefore(button, this_div.firstChild);
+ }
+
+ // tracebacks (.gt) contain bare text elements that need to be
+ // wrapped in a span to work with .nextUntil() (see later)
+ const preWithGt = this_div.querySelectorAll('pre');
+ for (const preNode of preWithGt) {
+ if (preNode.querySelector('.gt')) {
+ const contents = Array.from(preNode.childNodes);
+ for (const node of contents) {
+ if ((node.nodeType === Node.TEXT_NODE) && node.data.trim()) {
+ const span = document.createElement('span');
+ span.textContent = node.data;
+ preNode.replaceChild(span, node);
+ }
+ }
+ }
+ }
+ }
+
+ // define the behavior of the button when it's clicked
+ const buttons = document.querySelectorAll('.copy_button');
+ for (const button of buttons) {
+ button.addEventListener('click', e => {
+ e.preventDefault();
+ const parent = button.parentNode;
+ const pre = next_pre_sibling(button);
+ if (button.getAttribute('aria-pressed') === 'false') {
+ // hide the code output
+ hide_elements(parent, '.go, .gp, .gt');
+ if (pre) {
+ set_traceback_visibility(pre, false);
+ }
+ button.style.textDecoration = 'line-through';
+ button.setAttribute('title', show_text);
+ button.setAttribute('aria-pressed', 'true');
+ } else {
+ // show the code output
+ show_elements(parent, '.go, .gp, .gt');
+ if (pre) {
+ // pre is the same thing jQuery would return for .next('pre')
+ set_traceback_visibility(pre, true);
+ }
+ button.style.textDecoration = 'none';
+ button.setAttribute('title', hide_text);
+ button.setAttribute('aria-pressed', 'false');
+ }
+ });
+ button.addEventListener('keydown', e => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ button.click();
+ }
+ });
+ }
+});