|
313 | 313 | }); |
314 | 314 | } |
315 | 315 |
|
| 316 | + async function copyText(text) { |
| 317 | + if (navigator.clipboard?.writeText) { |
| 318 | + try { |
| 319 | + await navigator.clipboard.writeText(text); |
| 320 | + return; |
| 321 | + } catch (error) { |
| 322 | + // Fall back to document.execCommand when clipboard permissions are unavailable. |
| 323 | + } |
| 324 | + } |
| 325 | + |
| 326 | + const textarea = document.createElement('textarea'); |
| 327 | + textarea.value = text; |
| 328 | + textarea.setAttribute('readonly', ''); |
| 329 | + textarea.style.position = 'absolute'; |
| 330 | + textarea.style.left = '-9999px'; |
| 331 | + document.body.appendChild(textarea); |
| 332 | + textarea.select(); |
| 333 | + |
| 334 | + try { |
| 335 | + document.execCommand('copy'); |
| 336 | + } finally { |
| 337 | + textarea.remove(); |
| 338 | + } |
| 339 | + } |
| 340 | + |
| 341 | + const copyButtonResetTimers = new WeakMap(); |
| 342 | + |
| 343 | + function showCopyState(button, state) { |
| 344 | + const labels = { |
| 345 | + idle: 'Copy', |
| 346 | + success: 'Copied!', |
| 347 | + error: 'Try again' |
| 348 | + }; |
| 349 | + |
| 350 | + button.textContent = labels[state]; |
| 351 | + button.dataset.state = state; |
| 352 | + |
| 353 | + const existingTimer = copyButtonResetTimers.get(button); |
| 354 | + if (existingTimer) { |
| 355 | + clearTimeout(existingTimer); |
| 356 | + } |
| 357 | + |
| 358 | + if (state !== 'idle') { |
| 359 | + const resetTimer = setTimeout(() => { |
| 360 | + button.textContent = labels.idle; |
| 361 | + button.dataset.state = 'idle'; |
| 362 | + }, 2000); |
| 363 | + copyButtonResetTimers.set(button, resetTimer); |
| 364 | + } |
| 365 | + } |
| 366 | + |
| 367 | + function enhanceCodeBlocks() { |
| 368 | + const blocks = document.querySelectorAll('#markdown-content pre'); |
| 369 | + |
| 370 | + blocks.forEach((pre) => { |
| 371 | + if (pre.parentElement?.classList.contains('code-block-wrapper')) { |
| 372 | + return; |
| 373 | + } |
| 374 | + |
| 375 | + const code = pre.querySelector('code'); |
| 376 | + const wrapper = document.createElement('div'); |
| 377 | + const button = document.createElement('button'); |
| 378 | + |
| 379 | + wrapper.className = 'code-block-wrapper'; |
| 380 | + button.className = 'copy-code-btn'; |
| 381 | + button.type = 'button'; |
| 382 | + button.textContent = 'Copy'; |
| 383 | + button.setAttribute('aria-label', 'Copy code block to clipboard'); |
| 384 | + |
| 385 | + button.addEventListener('click', async () => { |
| 386 | + const codeText = code?.textContent ?? pre.textContent ?? ''; |
| 387 | + |
| 388 | + try { |
| 389 | + await copyText(codeText.trimEnd()); |
| 390 | + showCopyState(button, 'success'); |
| 391 | + } catch (error) { |
| 392 | + showCopyState(button, 'error'); |
| 393 | + } |
| 394 | + }); |
| 395 | + |
| 396 | + pre.parentNode.insertBefore(wrapper, pre); |
| 397 | + wrapper.appendChild(button); |
| 398 | + wrapper.appendChild(pre); |
| 399 | + }); |
| 400 | + } |
| 401 | + |
316 | 402 | // Process markdown - clean up navigation links and fix paths |
317 | 403 | function processMarkdown(md) { |
318 | 404 | // Remove the header navigation bar (we have our own sidebar) |
|
373 | 459 |
|
374 | 460 | document.getElementById('markdown-content').innerHTML = marked.parse(md); |
375 | 461 | enableTaskListCheckboxes(step.id); |
| 462 | + enhanceCodeBlocks(); |
376 | 463 |
|
377 | 464 | // If this is the completion page, add confetti! |
378 | 465 | if (step.id === '05-complete') { |
|
0 commit comments