From f37c272ff976fe84636e7a0dcbe9a429cb097c86 Mon Sep 17 00:00:00 2001 From: Yuriy Andamasov Date: Wed, 6 May 2026 01:59:34 +0300 Subject: fix(codecopier): exclude Copy label from clipboard and stop showing false success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two real bugs in the docs site copy-to-clipboard handler, both flagged by copilot review on PR #1886 (deferred there because that PR is scoped to typos). Bug 1: Copy label leaked into copied snippets on narrow screens The handler read `currentTarget.offsetParent.innerText`. The same handler appends a visible `.copyDiv` (with a 'Copy'

) into that container. Below the 992px breakpoint the label is visible (see code-snippets.css .copyDiv > p) so users got 'Copy' appended to every copied snippet. Fix: extract text from the

 element inside the container instead,
which excludes the injected button. Also switched the lookup root from
`offsetParent` to `parentElement` so the source-of-truth is the DOM
relationship (the .copyDiv is inserted as a beforeend child of the inner
.highlight div via insertAdjacentHTML), not CSS positioning.

Bug 2: Failed clipboard writes still showed 'Copied!'
The try/catch only logged on failure but the surrounding code still flipped
the button into the copiedNotifier success state. Users got false success
when writeText rejected (insecure context, permission denied, etc.).

Fix: move the success UI flip inside the try, add an explicit failure UI
flip ('Failed' text + new `.copyFailedNotifier` class with red background)
in the catch. setTimeout still reverts both classes after 2s.

Verified in browser with a sphinx-rendered fixture (jQuery 3.7 + Pygments
output): both snippets copy their own text without the Copy label, success
flow shows Copied!, simulated writeText rejection shows Failed, both states
revert after 2s.

🤖 Generated by [robots](https://vyos.io)
---
 docs/_static/js/codecopier.js | 46 ++++++++++++++++++++++++++++++++-----------
 1 file changed, 34 insertions(+), 12 deletions(-)

(limited to 'docs/_static/js/codecopier.js')

diff --git a/docs/_static/js/codecopier.js b/docs/_static/js/codecopier.js
index bf0b3b4d..ae069ba0 100644
--- a/docs/_static/js/codecopier.js
+++ b/docs/_static/js/codecopier.js
@@ -20,6 +20,24 @@ function formDiv(id) {
 `
 }
 
+// Extract the snippet text from a code-block container while excluding the
+// injected .copyDiv button. On viewports below the breakpoint the visible
+// "Copy" label otherwise leaks into the clipboard via innerText.
+function extractSnippetText(container) {
+  if (!container) {
+    return ''
+  }
+  const preElement = container.querySelector('pre')
+  if (preElement) {
+    return preElement.innerText
+  }
+  // Fallback for any structure without a 
: clone the container,
+  // remove every .copyDiv, then read innerText off the clone.
+  const clone = container.cloneNode(true)
+  clone.querySelectorAll('.copyDiv').forEach((node) => node.remove())
+  return clone.innerText
+}
+
 $(document).ready(async function () {
   const codeSnippets = $(
     '.rst-content div[class^=highlight] div[class^=highlight], .rst-content pre.literal-block div[class^=highlight], .rst-content pre.literal-block div[class^=highlight]'
@@ -34,26 +52,31 @@ $(document).ready(async function () {
   copyButton.click(async ({
     currentTarget
   }) => {
-    // we obtain text and copy it
     const id = currentTarget.dataset.identifier
+    const divWithNeededId = $(`div[data-identifier='${id}']`)
+    // Read from the .copyDiv's parentElement (the inner .highlight div it was
+    // injected into via insertAdjacentHTML('beforeend', ...)) rather than
+    // currentTarget.offsetParent — offsetParent depends on CSS positioning
+    // and would walk past .highlight if its `position: relative` is ever
+    // dropped, picking up the wrong snippet.
+    const textToCopy = extractSnippetText(currentTarget.parentElement)
 
     try {
-      await navigator.clipboard.writeText(currentTarget.offsetParent.innerText)
+      await navigator.clipboard.writeText(textToCopy)
+      // Only flip the button into the success state when the write actually
+      // resolves — earlier versions showed "Copied!" even on failure.
+      divWithNeededId.addClass('copiedNotifier')
+      divWithNeededId.html('Copied!')
     } catch (error) {
-      console.log('Copiing text failed, please try again', {
-        error
-      })
+      console.error('Copying text failed, please try again', error)
+      divWithNeededId.addClass('copyFailedNotifier')
+      divWithNeededId.html('Failed')
     }
 
-    // we edit the copyDiv connected to copied text
-    const divWithNeededId = $(`div[data-identifier='${id}']`)
-    divWithNeededId.addClass('copiedNotifier')
-    divWithNeededId.html('Copied!')
-
     setTimeout(() => {
       divWithNeededId.html(innersOfCopyDiv)
       divWithNeededId.removeClass('copiedNotifier')
-
+      divWithNeededId.removeClass('copyFailedNotifier')
     }, 2000)
   })
 
@@ -64,4 +87,3 @@ $(document).ready(async function () {
   navbar.append(readTheDocsButton)
 
 });
-
-- 
cgit v1.2.3


From 4cd5e2cc3bb38b75d4e47aacd6768218daf8762a Mon Sep 17 00:00:00 2001
From: Yuriy Andamasov 
Date: Wed, 6 May 2026 22:50:11 +0300
Subject: fix(codecopier): prevent state class crossover on rapid re-clicks
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reset both notifier classes and cancel any pending revert timeout at the
start of each click so a fast failure-then-success (or vice versa) can't
leave `copiedNotifier` and `copyFailedNotifier` applied simultaneously,
and so the 2-second auto-revert window restarts cleanly per click.

Addresses Copilot review feedback on PR #1890.

🤖 Generated by [robots](https://vyos.io)
---
 docs/_static/js/codecopier.js | 19 ++++++++++++++++---
 1 file changed, 16 insertions(+), 3 deletions(-)

(limited to 'docs/_static/js/codecopier.js')

diff --git a/docs/_static/js/codecopier.js b/docs/_static/js/codecopier.js
index ae069ba0..02143b92 100644
--- a/docs/_static/js/codecopier.js
+++ b/docs/_static/js/codecopier.js
@@ -61,6 +61,18 @@ $(document).ready(async function () {
     // dropped, picking up the wrong snippet.
     const textToCopy = extractSnippetText(currentTarget.parentElement)
 
+    // Clear any state class left over from a previous click so a rapid
+    // success-after-failure (or vice versa) doesn't end up with both
+    // classes applied at once.
+    divWithNeededId.removeClass('copiedNotifier copyFailedNotifier')
+
+    // Cancel a pending revert timeout from a previous click so the new
+    // click's 2-second window starts clean instead of being ended early.
+    const previousTimeout = divWithNeededId.data('revertTimeout')
+    if (previousTimeout) {
+      clearTimeout(previousTimeout)
+    }
+
     try {
       await navigator.clipboard.writeText(textToCopy)
       // Only flip the button into the success state when the write actually
@@ -73,11 +85,12 @@ $(document).ready(async function () {
       divWithNeededId.html('Failed')
     }
 
-    setTimeout(() => {
+    const revertTimeout = setTimeout(() => {
       divWithNeededId.html(innersOfCopyDiv)
-      divWithNeededId.removeClass('copiedNotifier')
-      divWithNeededId.removeClass('copyFailedNotifier')
+      divWithNeededId.removeClass('copiedNotifier copyFailedNotifier')
+      divWithNeededId.removeData('revertTimeout')
     }, 2000)
+    divWithNeededId.data('revertTimeout', revertTimeout)
   })
 
   // we edit the button that is added by readthedocs portal
-- 
cgit v1.2.3


From 57e2d2be666f4daca0b19e4f7230d9130713a198 Mon Sep 17 00:00:00 2001
From: Yuriy Andamasov 
Date: Wed, 6 May 2026 23:46:53 +0300
Subject: fix(codecopier): guard UI updates against out-of-order async
 completions

Rapid re-clicks queue multiple `navigator.clipboard.writeText`
promises; without a request token, an older promise resolving later
can overwrite the newer click's UI (or have its 2-second timeout fire
on the new click's state). Add a per-button `copyRequestId` that
each click increments and captures locally; the success/failure UI
flip and the revert-timeout body all bail out early if the captured
token no longer matches the current one.

Addresses CodeRabbit review feedback on PR #1890.

\xf0\x9f\xa4\x96 Generated by [robots](https://vyos.io)
---
 docs/_static/js/codecopier.js | 13 +++++++++++++
 1 file changed, 13 insertions(+)

(limited to 'docs/_static/js/codecopier.js')

diff --git a/docs/_static/js/codecopier.js b/docs/_static/js/codecopier.js
index 02143b92..4b0941c9 100644
--- a/docs/_static/js/codecopier.js
+++ b/docs/_static/js/codecopier.js
@@ -54,6 +54,16 @@ $(document).ready(async function () {
   }) => {
     const id = currentTarget.dataset.identifier
     const divWithNeededId = $(`div[data-identifier='${id}']`)
+
+    // Per-click request token. Rapid re-clicks queue multiple writeText
+    // promises; without this guard, an older promise resolving later
+    // can overwrite the newer click's UI (or have its timeout fire on
+    // the new state). Each click increments the token, captures the
+    // local copy, and bails out of any UI/timeout work if the captured
+    // token no longer matches the current one.
+    const requestId = (divWithNeededId.data('copyRequestId') || 0) + 1
+    divWithNeededId.data('copyRequestId', requestId)
+
     // Read from the .copyDiv's parentElement (the inner .highlight div it was
     // injected into via insertAdjacentHTML('beforeend', ...)) rather than
     // currentTarget.offsetParent — offsetParent depends on CSS positioning
@@ -75,17 +85,20 @@ $(document).ready(async function () {
 
     try {
       await navigator.clipboard.writeText(textToCopy)
+      if (divWithNeededId.data('copyRequestId') !== requestId) return
       // Only flip the button into the success state when the write actually
       // resolves — earlier versions showed "Copied!" even on failure.
       divWithNeededId.addClass('copiedNotifier')
       divWithNeededId.html('Copied!')
     } catch (error) {
+      if (divWithNeededId.data('copyRequestId') !== requestId) return
       console.error('Copying text failed, please try again', error)
       divWithNeededId.addClass('copyFailedNotifier')
       divWithNeededId.html('Failed')
     }
 
     const revertTimeout = setTimeout(() => {
+      if (divWithNeededId.data('copyRequestId') !== requestId) return
       divWithNeededId.html(innersOfCopyDiv)
       divWithNeededId.removeClass('copiedNotifier copyFailedNotifier')
       divWithNeededId.removeData('revertTimeout')
-- 
cgit v1.2.3