|
304 | 304 | return md; |
305 | 305 | } |
306 | 306 |
|
| 307 | + const resetTimers = new WeakMap(); |
| 308 | + |
| 309 | + function resetCopyButton(button) { |
| 310 | + const existingTimerId = resetTimers.get(button); |
| 311 | + if (existingTimerId) { |
| 312 | + window.clearTimeout(existingTimerId); |
| 313 | + } |
| 314 | + |
| 315 | + const timerId = window.setTimeout(() => { |
| 316 | + button.textContent = 'Copy'; |
| 317 | + button.setAttribute('aria-label', 'Copy code block'); |
| 318 | + button.dataset.copyState = 'idle'; |
| 319 | + }, 2000); |
| 320 | + resetTimers.set(button, timerId); |
| 321 | + } |
| 322 | + |
| 323 | + async function copyText(text) { |
| 324 | + if (navigator.clipboard?.writeText) { |
| 325 | + try { |
| 326 | + await navigator.clipboard.writeText(text); |
| 327 | + return; |
| 328 | + } catch (error) { |
| 329 | + // Fall back below when the Clipboard API is unavailable or denied. |
| 330 | + } |
| 331 | + } |
| 332 | + |
| 333 | + const textArea = document.createElement('textarea'); |
| 334 | + textArea.value = text; |
| 335 | + textArea.setAttribute('readonly', ''); |
| 336 | + textArea.style.position = 'absolute'; |
| 337 | + textArea.style.left = '-9999px'; |
| 338 | + document.body.appendChild(textArea); |
| 339 | + textArea.select(); |
| 340 | + |
| 341 | + try { |
| 342 | + if (!document.execCommand('copy')) { |
| 343 | + throw new Error('Failed to copy code to clipboard'); |
| 344 | + } |
| 345 | + } finally { |
| 346 | + document.body.removeChild(textArea); |
| 347 | + } |
| 348 | + } |
| 349 | + |
| 350 | + function enhanceCodeBlocks() { |
| 351 | + document.querySelectorAll('#markdown-content pre code').forEach((code) => { |
| 352 | + const pre = code.parentElement; |
| 353 | + if (!pre || pre.querySelector('.code-copy-button')) { |
| 354 | + return; |
| 355 | + } |
| 356 | + |
| 357 | + const text = code?.textContent?.trimEnd(); |
| 358 | + if (!text) { |
| 359 | + return; |
| 360 | + } |
| 361 | + |
| 362 | + const button = document.createElement('button'); |
| 363 | + button.type = 'button'; |
| 364 | + button.className = 'code-copy-button'; |
| 365 | + button.textContent = 'Copy'; |
| 366 | + button.dataset.copyState = 'idle'; |
| 367 | + button.setAttribute('aria-label', 'Copy code block'); |
| 368 | + button.setAttribute('aria-live', 'polite'); |
| 369 | + |
| 370 | + button.addEventListener('click', async () => { |
| 371 | + try { |
| 372 | + await copyText(text); |
| 373 | + button.textContent = 'Copied!'; |
| 374 | + button.setAttribute('aria-label', 'Code copied'); |
| 375 | + button.dataset.copyState = 'copied'; |
| 376 | + } catch (error) { |
| 377 | + button.textContent = 'Failed'; |
| 378 | + button.setAttribute('aria-label', 'Copy failed'); |
| 379 | + button.dataset.copyState = 'error'; |
| 380 | + } |
| 381 | + |
| 382 | + resetCopyButton(button); |
| 383 | + }); |
| 384 | + |
| 385 | + pre.appendChild(button); |
| 386 | + }); |
| 387 | + } |
| 388 | + |
307 | 389 | // Load and render markdown |
308 | 390 | async function loadContent() { |
309 | 391 | const idx = getCurrentStepIndex(); |
|
342 | 424 | checkbox.disabled = false; |
343 | 425 | }); |
344 | 426 |
|
| 427 | + enhanceCodeBlocks(); |
| 428 | + |
345 | 429 | // If this is the completion page, add confetti! |
346 | 430 | if (step.id === '05-complete') { |
347 | 431 | // Trigger celebration on load |
|
0 commit comments