|
221 | 221 | ? '../workshop/' |
222 | 222 | : 'https://raw.githubusercontent.com/copilot-dev-days/agent-lab-python/main/workshop/'; |
223 | 223 | const CHECKBOX_STORAGE_KEY = 'agent-lab-checkboxes-v1'; |
| 224 | + const COPY_FEEDBACK_DURATION_MS = 1600; |
224 | 225 | let checkboxSaveTimeout; |
225 | 226 |
|
226 | 227 | // Get current step from URL |
|
255 | 256 | checkboxSaveTimeout = setTimeout(() => writeCheckboxState(state), 100); |
256 | 257 | } |
257 | 258 |
|
| 259 | + function applyVisuallyHiddenStyles(element) { |
| 260 | + Object.assign(element.style, { |
| 261 | + position: 'absolute', |
| 262 | + width: '1px', |
| 263 | + height: '1px', |
| 264 | + padding: '0', |
| 265 | + margin: '-1px', |
| 266 | + border: '0', |
| 267 | + overflow: 'hidden', |
| 268 | + clip: 'rect(0 0 0 0)', |
| 269 | + clipPath: 'inset(50%)', |
| 270 | + whiteSpace: 'nowrap' |
| 271 | + }); |
| 272 | + } |
| 273 | + |
258 | 274 | function initializeTaskListCheckboxes(stepId) { |
259 | 275 | const container = document.getElementById('markdown-content'); |
260 | 276 | if (!container) return; |
|
282 | 298 | }); |
283 | 299 | } |
284 | 300 |
|
| 301 | + async function copyTextToClipboard(text) { |
| 302 | + if (navigator.clipboard?.writeText) { |
| 303 | + await navigator.clipboard.writeText(text); |
| 304 | + return; |
| 305 | + } |
| 306 | + |
| 307 | + const textArea = document.createElement('textarea'); |
| 308 | + textArea.value = text; |
| 309 | + textArea.setAttribute('readonly', ''); |
| 310 | + applyVisuallyHiddenStyles(textArea); |
| 311 | + document.body.appendChild(textArea); |
| 312 | + textArea.select(); |
| 313 | + |
| 314 | + const copySucceeded = document.execCommand('copy'); |
| 315 | + textArea.remove(); |
| 316 | + |
| 317 | + if (!copySucceeded) { |
| 318 | + throw new Error('Copy failed'); |
| 319 | + } |
| 320 | + } |
| 321 | + |
| 322 | + function initializeCodeBlockCopyButtons() { |
| 323 | + const container = document.getElementById('markdown-content'); |
| 324 | + if (!container) return; |
| 325 | + |
| 326 | + const codeBlocks = container.querySelectorAll('pre'); |
| 327 | + codeBlocks.forEach((block) => { |
| 328 | + const codeElement = block.querySelector('code'); |
| 329 | + if (!codeElement || block.querySelector('.code-copy-btn')) return; |
| 330 | + |
| 331 | + const button = document.createElement('button'); |
| 332 | + button.type = 'button'; |
| 333 | + button.className = 'code-copy-btn'; |
| 334 | + button.textContent = 'Copy'; |
| 335 | + button.setAttribute('aria-label', 'Copy code block'); |
| 336 | + |
| 337 | + let resetTimeoutId = null; |
| 338 | + button.addEventListener('click', async () => { |
| 339 | + try { |
| 340 | + await copyTextToClipboard(codeElement.textContent ?? ''); |
| 341 | + button.textContent = 'Copied!'; |
| 342 | + button.classList.add('copied'); |
| 343 | + } catch { |
| 344 | + button.textContent = 'Failed'; |
| 345 | + button.classList.add('failed'); |
| 346 | + } |
| 347 | + |
| 348 | + clearTimeout(resetTimeoutId); |
| 349 | + resetTimeoutId = setTimeout(() => { |
| 350 | + button.textContent = 'Copy'; |
| 351 | + button.classList.remove('copied', 'failed'); |
| 352 | + }, COPY_FEEDBACK_DURATION_MS); |
| 353 | + }); |
| 354 | + |
| 355 | + block.appendChild(button); |
| 356 | + }); |
| 357 | + } |
| 358 | + |
285 | 359 | // Build sidebar navigation |
286 | 360 | function buildSidebar() { |
287 | 361 | const nav = document.getElementById('stepNav'); |
|
390 | 464 |
|
391 | 465 | document.getElementById('markdown-content').innerHTML = marked.parse(md); |
392 | 466 | initializeTaskListCheckboxes(step.id); |
| 467 | + initializeCodeBlockCopyButtons(); |
393 | 468 |
|
394 | 469 | // If this is the completion page, add confetti! |
395 | 470 | if (step.id === '05-complete') { |
|
0 commit comments