From 1fac4f18e195736c19cc766a35d7a96f9844830f Mon Sep 17 00:00:00 2001 From: ChisakaKanako Date: Tue, 22 Sep 2026 14:39:02 +0800 Subject: [PATCH] fix(theme): keep the copy button out of code text selection close #5120 --- .../node/markdown/plugins/containers.test.ts | 14 ++++----- src/client/app/composables/copyCode.ts | 24 ++++++++++++++- .../styles/components/vp-doc.css | 29 ++++++++++++++++++- src/node/markdown/plugins/preWrapper.ts | 2 +- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/__tests__/unit/node/markdown/plugins/containers.test.ts b/__tests__/unit/node/markdown/plugins/containers.test.ts index ffee6c777..21c2781ee 100644 --- a/__tests__/unit/node/markdown/plugins/containers.test.ts +++ b/__tests__/unit/node/markdown/plugins/containers.test.ts @@ -96,9 +96,9 @@ describe('node/markdown/plugins/containers', () => {

content

Click me to toggle the code -
js
console.log('hi')
+      
console.log('hi')
       
-
+ js " `) }) @@ -335,11 +335,11 @@ describe('node/markdown/plugins/containers', () => { ].join('\n') expect(await render(src)).toMatchInlineSnapshot(` "
-
js
const a = 1
+      
const a = 1
       
-
ts
const a: number = 1
+      js
const a: number = 1
       
-
+ ts
" `) }) @@ -497,9 +497,9 @@ describe('node/markdown/plugins/containers (github alerts)', () => {
  • list item
-
js
const a = 1
+      
const a = 1
       
-
+ js " `) }) diff --git a/src/client/app/composables/copyCode.ts b/src/client/app/composables/copyCode.ts index d5f088482..2008040a8 100644 --- a/src/client/app/composables/copyCode.ts +++ b/src/client/app/composables/copyCode.ts @@ -7,11 +7,33 @@ const ignoredNodes = ['.vp-copy-ignore', '.diff.remove'].join(', ') export function useCopyCode() { if (inBrowser) { const timeoutIdMap: WeakMap = new WeakMap() + let selectingBlock: HTMLElement | null = null + + const blockOf = (node: Node | null) => + node?.parentElement?.closest('div[class*="language-"]') ?? + null + + document.addEventListener('selectionchange', () => { + const selection = document.getSelection() + const block = + selection && !selection.isCollapsed + ? blockOf(selection.anchorNode) || blockOf(selection.focusNode) + : null + + if (selectingBlock && selectingBlock !== block) { + selectingBlock.classList.remove('selecting') + } + if (block) { + block.classList.add('selecting') + } + selectingBlock = block + }) + window.addEventListener('click', (e) => { const el = e.target as HTMLElement if (el.matches('div[class*="language-"] > button.copy')) { const parent = el.parentElement - const sibling = el.nextElementSibling?.nextElementSibling //
 tag
+        const sibling = parent?.querySelector('pre')
         if (!parent || !sibling) {
           return
         }
diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css
index a5ba52f7c..e710cec5e 100644
--- a/src/client/theme-default/styles/components/vp-doc.css
+++ b/src/client/theme-default/styles/components/vp-doc.css
@@ -506,13 +506,16 @@
   top: 0.75rem;
   inset-inline-end: 0.75rem;
   z-index: 3;
+  pointer-events: none;
   border: 1px solid var(--vp-code-copy-code-border-color);
   border-radius: 0.25rem;
   width: 2.5rem;
   height: 2.5rem;
   background-color: var(--vp-code-copy-code-bg);
   opacity: 0;
+  visibility: hidden;
   cursor: pointer;
+  user-select: none;
   background-image: var(--vp-icon-copy);
   background-position: 50%;
   background-size: 1.25rem;
@@ -520,12 +523,36 @@
   transition:
     border-color 0.25s,
     background-color 0.25s,
-    opacity 0.25s;
+    opacity 0.25s,
+    visibility 0s 0.25s;
 }
 
 .vp-doc [class*='language-']:hover > button.copy,
+.vp-doc [class*='language-']:focus-within > button.copy,
 .vp-doc [class*='language-'] > button.copy:focus {
   opacity: 1;
+  visibility: visible;
+  pointer-events: auto;
+  transition:
+    border-color 0.25s,
+    background-color 0.25s,
+    opacity 0.25s,
+    visibility 0s;
+}
+
+.vp-doc [class*='language-'].selecting > button.copy:not(:focus),
+.vp-doc [class*='language-'].selecting > span.lang {
+  opacity: 0;
+  visibility: hidden;
+  pointer-events: none;
+}
+
+.vp-doc [class*='language-'].selecting > button.copy:not(:focus) {
+  transition:
+    border-color 0.25s,
+    background-color 0.25s,
+    opacity 0.25s,
+    visibility 0s 0.25s;
 }
 
 .vp-doc [class*='language-'] > button.copy:hover,
diff --git a/src/node/markdown/plugins/preWrapper.ts b/src/node/markdown/plugins/preWrapper.ts
index 23e2b601a..948671ab3 100644
--- a/src/node/markdown/plugins/preWrapper.ts
+++ b/src/node/markdown/plugins/preWrapper.ts
@@ -43,9 +43,9 @@ export function preWrapperPlugin(md: MarkdownItAsync, options: Options) {
 
     return (
       `
` + + fence(...args) + `` + `${label}` + - fence(...args) + '
' ) }