From 0d44c8a7094d0ae42d5b2715339bc5a14b4845a0 Mon Sep 17 00:00:00 2001 From: Christian Johannesen Date: Thu, 29 Oct 2026 21:17:15 +0100 Subject: [PATCH] Add copy and download actions to program source panels --- public/app.js | 41 +++++++++++++++++++++++++++++++++++++++++ public/index.html | 9 ++++++++- public/styles.css | 7 +++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/public/app.js b/public/app.js index a19e7dc..cc9621b 100644 --- a/public/app.js +++ b/public/app.js @@ -264,6 +264,10 @@ async function loadInlineSource(program) { const heading = document.querySelector('#program-source-title'); const message = document.querySelector('#program-source-status'); const listing = document.querySelector('#program-source-code'); + const copyButton = document.querySelector('#copy-program-source'); + const downloadLink = document.querySelector('#download-program-source'); + copyButton.disabled = true; + downloadLink.hidden = true; heading.textContent = program.language === 'machine-code' ? 'Assembly source' : 'BASIC V2 source'; message.hidden = false; message.className = ''; @@ -276,12 +280,49 @@ async function loadInlineSource(program) { highlightSource(listing.querySelector('code'), program, await response.text()); message.hidden = true; listing.hidden = false; + copyButton.disabled = false; + downloadLink.href = `/api/source?path=${encodeURIComponent(program.path)}`; + downloadLink.download = program.source.split('/').at(-1); + downloadLink.hidden = false; } catch (error) { message.className = 'error'; message.textContent = error.message; } } +let sourceCopyTimer; +document.querySelector('#copy-program-source').addEventListener('click', async event => { + const button = event.currentTarget; + const feedback = document.querySelector('#source-copy-feedback'); + const text = document.querySelector('#program-source-code code').textContent; + clearTimeout(sourceCopyTimer); + try { + try { + if (!navigator.clipboard?.writeText) throw new Error('Clipboard API unavailable'); + await navigator.clipboard.writeText(text); + } catch { + // Support plain HTTP deployments as well as HTTPS and localhost. + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.cssText = 'position:fixed;left:-9999px;top:0'; + document.body.append(textarea); + try { + textarea.select(); + if (!document.execCommand('copy')) throw new Error('Copy failed'); + } finally { + textarea.remove(); + button.focus({ preventScroll: true }); + } + } + button.textContent = 'Copied'; + feedback.textContent = 'Source code copied to clipboard.'; + } catch { + button.textContent = 'Try again'; + feedback.textContent = 'Could not copy the source. Select the code or use Download instead.'; + } + sourceCopyTimer = setTimeout(() => { button.textContent = 'Copy'; }, 2000); +}); + function startEmulator(program) { library.hidden = true; player.hidden = false; diff --git a/public/index.html b/public/index.html index b97c23a..6dfd1d2 100644 --- a/public/index.html +++ b/public/index.html @@ -73,7 +73,14 @@
-

Source code

+
+

Source code

+
+ + +
+
+

Loading source…

diff --git a/public/styles.css b/public/styles.css index 9276ce2..1339132 100644 --- a/public/styles.css +++ b/public/styles.css @@ -85,6 +85,13 @@ button:disabled { opacity: .5; cursor: wait; } .instructions-panel h2, .program-source-panel h2 { margin: 0 0 .85rem; font-size: 1rem; letter-spacing: .08em; text-transform: uppercase; } .program-source-panel pre { max-height: 600px; margin: 0; padding: 1rem; overflow: auto; border-radius: 6px; background: #0d0928; color: #d8d3f4; font: .85rem/1.55 ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre; tab-size: 4; } .program-source-panel pre:focus-visible { outline: 2px solid var(--c64-light-blue); outline-offset: 2px; } +.source-panel-heading { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .75rem; margin-bottom: .85rem; } +.source-panel-heading h2 { margin: 0; } +.source-panel-actions { display: flex; gap: .5rem; } +.source-action { display: inline-flex; align-items: center; justify-content: center; margin: 0; padding: .4rem .7rem; border: 1px solid #b8b1e755; border-radius: 6px; background: var(--c64-blue); color: #fff; font: inherit; font-size: .8rem; line-height: 1.25; text-decoration: none; cursor: pointer; } +.source-action:hover { background: var(--c64-light-blue); } +.source-action:focus-visible { outline: 2px solid var(--c64-lightest-blue); outline-offset: 2px; } +.source-action[hidden] { display: none; } .program-source-panel code, .source-modal code { font: inherit; } .token.comment { color: #aaa5b8; font-style: italic; } .token.keyword { color: #c6b4ff; }