|
81 | 81 |
|
82 | 82 | const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/copilot-dev-days/agent-lab-typescript/main/workshop/'; |
83 | 83 | const CHECKBOX_STATE_KEY_PREFIX = 'workshop-checkboxes:'; |
| 84 | + const COPY_STATUS_RESET_DELAY = 2000; |
| 85 | + const pendingCopyResetTimers = new Set(); |
84 | 86 |
|
85 | 87 | function getCurrentStepId() { |
86 | 88 | const params = new URLSearchParams(window.location.search); |
|
169 | 171 |
|
170 | 172 | function setupInteractiveCheckboxes(stepId) { |
171 | 173 | const container = document.getElementById('markdown-content'); |
| 174 | + if (!container) return; |
172 | 175 | const checkboxes = container.querySelectorAll('input[type="checkbox"]'); |
173 | 176 | if (!checkboxes.length) return; |
174 | 177 |
|
|
198 | 201 | }); |
199 | 202 | } |
200 | 203 |
|
| 204 | + async function copyCodeToClipboard(text) { |
| 205 | + if (navigator.clipboard?.writeText) { |
| 206 | + await navigator.clipboard.writeText(text); |
| 207 | + return; |
| 208 | + } |
| 209 | + |
| 210 | + const textarea = document.createElement('textarea'); |
| 211 | + textarea.value = text; |
| 212 | + textarea.setAttribute('readonly', ''); |
| 213 | + textarea.style.position = 'absolute'; |
| 214 | + textarea.style.left = '-9999px'; |
| 215 | + document.body.appendChild(textarea); |
| 216 | + textarea.select(); |
| 217 | + // Keep the deprecated fallback for older browsers that lack navigator.clipboard. |
| 218 | + const copied = document.execCommand('copy'); |
| 219 | + document.body.removeChild(textarea); |
| 220 | + if (!copied) throw new Error('Fallback copy failed'); |
| 221 | + } |
| 222 | + |
| 223 | + function setupCodeBlockCopyButtons() { |
| 224 | + const container = document.getElementById('markdown-content'); |
| 225 | + if (!container) return; |
| 226 | + const codeBlocks = container.querySelectorAll('pre'); |
| 227 | + |
| 228 | + codeBlocks.forEach((pre) => { |
| 229 | + const code = pre.querySelector('code'); |
| 230 | + const codeText = code?.textContent?.trim(); |
| 231 | + if (!codeText) return; |
| 232 | + |
| 233 | + const copyButton = document.createElement('button'); |
| 234 | + copyButton.type = 'button'; |
| 235 | + copyButton.className = 'code-copy-btn'; |
| 236 | + copyButton.textContent = 'Copy'; |
| 237 | + copyButton.setAttribute('aria-label', 'Copy code'); |
| 238 | + |
| 239 | + const liveRegion = document.createElement('span'); |
| 240 | + liveRegion.className = 'sr-only'; |
| 241 | + liveRegion.setAttribute('aria-live', 'polite'); |
| 242 | + let resetTimer = null; |
| 243 | + |
| 244 | + const resetButtonState = () => { |
| 245 | + copyButton.textContent = 'Copy'; |
| 246 | + copyButton.dataset.state = 'idle'; |
| 247 | + liveRegion.textContent = ''; |
| 248 | + }; |
| 249 | + |
| 250 | + copyButton.addEventListener('click', async () => { |
| 251 | + if (resetTimer) { |
| 252 | + clearTimeout(resetTimer); |
| 253 | + pendingCopyResetTimers.delete(resetTimer); |
| 254 | + } |
| 255 | + |
| 256 | + try { |
| 257 | + await copyCodeToClipboard(codeText); |
| 258 | + copyButton.textContent = 'Copied!'; |
| 259 | + copyButton.dataset.state = 'copied'; |
| 260 | + liveRegion.textContent = 'Code copied to clipboard'; |
| 261 | + } catch { |
| 262 | + copyButton.textContent = 'Failed'; |
| 263 | + copyButton.dataset.state = 'error'; |
| 264 | + liveRegion.textContent = 'Unable to copy code'; |
| 265 | + } |
| 266 | + |
| 267 | + resetTimer = setTimeout(() => { |
| 268 | + pendingCopyResetTimers.delete(resetTimer); |
| 269 | + resetTimer = null; |
| 270 | + resetButtonState(); |
| 271 | + }, COPY_STATUS_RESET_DELAY); |
| 272 | + pendingCopyResetTimers.add(resetTimer); |
| 273 | + }); |
| 274 | + |
| 275 | + pre.append(copyButton, liveRegion); |
| 276 | + }); |
| 277 | + } |
| 278 | + |
201 | 279 | async function loadContent() { |
202 | 280 | const idx = getCurrentStepIndex(); |
203 | 281 | if (idx === -1) { |
|
209 | 287 | document.title = `${step.title} | VS Code Copilot Agent Lab`; |
210 | 288 |
|
211 | 289 | try { |
| 290 | + pendingCopyResetTimers.forEach(timer => clearTimeout(timer)); |
| 291 | + pendingCopyResetTimers.clear(); |
| 292 | + |
212 | 293 | const response = await fetch(`${GITHUB_RAW_BASE}${step.file}`); |
213 | 294 | if (!response.ok) throw new Error('Failed to load'); |
214 | 295 |
|
|
225 | 306 | marked.setOptions({ breaks: true, gfm: true }); |
226 | 307 | document.getElementById('markdown-content').innerHTML = marked.parse(md); |
227 | 308 | setupInteractiveCheckboxes(step.id); |
| 309 | + setupCodeBlockCopyButtons(); |
228 | 310 |
|
229 | 311 | if (step.id === '05-complete') { |
230 | 312 | setTimeout(celebrate, 500); |
|
0 commit comments