Skip to content

Commit d859e1a

Browse files
feat: add Copy to Clipboard button to Scratchpad
Add a "Copy" button to the Scratchpad webview to allow users to easily copy the content to the system clipboard. - Add Copy button to `scratchpad.html` with visual feedback ("Copied!") - Refactor button CSS to use `btn`, `btn-primary`, `btn-secondary` classes - Handle `copyToClipboard` message in `ScratchpadViewProvider.ts` - Use `vscode.env.clipboard` for reliable clipboard access
1 parent e7ea972 commit d859e1a

3 files changed

Lines changed: 67 additions & 10 deletions

File tree

.Jules/palette.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@
55
## 2024-06-15 - Lightweight Confirmation in Webviews
66
**Learning:** Standard modal dialogs are too heavy for micro-interactions within Webviews. A two-step button confirmation (Click -> Confirm? -> Click) provides safety without context switching.
77
**Action:** Implement inline state-based confirmation for destructive actions in Webviews, using `setTimeout` to auto-reset.
8+
9+
## 2024-05-27 - Clipboard Reliability in Webviews
10+
**Learning:** `navigator.clipboard.writeText` in VS Code Webviews is flaky because it requires the document to be focused, which isn't guaranteed if the user just clicked a button.
11+
**Action:** Always delegate clipboard operations to the extension host via `postMessage` and `vscode.env.clipboard.writeText` for robust behavior.

src/features/scratchpad/ScratchpadViewProvider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export class ScratchpadViewProvider implements vscode.WebviewViewProvider {
3737
this._content = message.content;
3838
// 不记录频繁的内容变更日志,以免刷屏
3939
break;
40+
case "copyToClipboard": {
41+
vscode.env.clipboard.writeText(message.content);
42+
break;
43+
}
4044
}
4145
}, undefined);
4246

src/features/scratchpad/scratchpad.html

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,33 +56,48 @@
5656
display: flex;
5757
gap: 8px;
5858
}
59-
.btn-clear {
59+
.btn {
6060
background: none;
6161
border: none;
6262
cursor: pointer;
63-
color: var(--vscode-button-foreground);
64-
background-color: var(--vscode-button-secondaryBackground);
6563
padding: 2px 6px;
6664
border-radius: 2px;
6765
font-size: 11px;
6866
display: flex;
6967
align-items: center;
7068
gap: 4px;
69+
font-family: var(--vscode-font-family);
7170
}
72-
.btn-clear:hover {
73-
background-color: var(--vscode-button-secondaryHoverBackground);
74-
}
75-
.btn-clear:focus {
71+
.btn:focus {
7672
outline: 1px solid var(--vscode-focusBorder);
7773
outline-offset: 1px;
7874
}
79-
.btn-clear.confirm {
75+
76+
/* Secondary Button (Standard) */
77+
.btn-secondary {
78+
color: var(--vscode-button-secondaryForeground);
79+
background-color: var(--vscode-button-secondaryBackground);
80+
}
81+
.btn-secondary:hover {
82+
background-color: var(--vscode-button-secondaryHoverBackground);
83+
}
84+
/* Destructive State for Secondary Button */
85+
.btn-secondary.confirm {
8086
background-color: var(--vscode-button-destructiveBackground);
8187
color: var(--vscode-button-destructiveForeground);
8288
}
83-
.btn-clear.confirm:hover {
89+
.btn-secondary.confirm:hover {
8490
background-color: var(--vscode-button-destructiveHoverBackground);
8591
}
92+
93+
/* Primary Button */
94+
.btn-primary {
95+
color: var(--vscode-button-foreground);
96+
background-color: var(--vscode-button-background);
97+
}
98+
.btn-primary:hover {
99+
background-color: var(--vscode-button-hoverBackground);
100+
}
86101
</style>
87102
</head>
88103
<body>
@@ -95,7 +110,10 @@
95110
<div class="footer">
96111
<span id="char-count" class="char-count" aria-live="polite">0 chars</span>
97112
<div class="actions">
98-
<button id="btn-clear" class="btn-clear" aria-label="Clear Scratchpad" title="Clear all content">
113+
<button id="btn-copy" class="btn btn-primary" aria-label="Copy to Clipboard" title="Copy content to clipboard">
114+
Copy
115+
</button>
116+
<button id="btn-clear" class="btn btn-secondary" aria-label="Clear Scratchpad" title="Clear all content">
99117
Clear
100118
</button>
101119
</div>
@@ -105,7 +123,9 @@
105123
const textarea = document.getElementById('scratchpad');
106124
const charCount = document.getElementById('char-count');
107125
const btnClear = document.getElementById('btn-clear');
126+
const btnCopy = document.getElementById('btn-copy');
108127
let clearTimeoutId;
128+
let copyTimeoutId;
109129

110130
// Debounce function
111131
function debounce(func, delay) {
@@ -132,6 +152,15 @@
132152
btnClear.dataset.state = 'idle';
133153
}
134154

155+
function resetCopyButton() {
156+
if (copyTimeoutId) {
157+
clearTimeout(copyTimeoutId);
158+
copyTimeoutId = null;
159+
}
160+
btnCopy.textContent = 'Copy';
161+
btnCopy.setAttribute('aria-label', 'Copy to Clipboard');
162+
}
163+
135164
// 监听内容变化
136165
const onInput = debounce(() => {
137166
vscode.postMessage({
@@ -145,6 +174,26 @@
145174
onInput();
146175
});
147176

177+
// Copy Button Logic
178+
btnCopy.addEventListener('click', () => {
179+
const content = textarea.value;
180+
if (!content) return;
181+
182+
vscode.postMessage({
183+
type: 'copyToClipboard',
184+
content: content
185+
});
186+
187+
btnCopy.textContent = 'Copied!';
188+
btnCopy.setAttribute('aria-label', 'Copied to Clipboard');
189+
190+
if (copyTimeoutId) clearTimeout(copyTimeoutId);
191+
copyTimeoutId = setTimeout(resetCopyButton, 2000);
192+
193+
textarea.focus();
194+
});
195+
196+
// Clear Button Logic
148197
btnClear.addEventListener('click', () => {
149198
if (textarea.value.length === 0) return;
150199

0 commit comments

Comments
 (0)