Add copy and download actions to program source panels

This commit is contained in:
Christian Johannesen 2026-10-29 21:17:15 +01:00
parent 489918fbf8
commit 0d44c8a709
3 changed files with 56 additions and 1 deletions

View file

@ -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;

View file

@ -73,7 +73,14 @@
<div id="program-controls" class="control-list"></div>
</section>
<section class="program-source-panel" aria-labelledby="program-source-title">
<h2 id="program-source-title">Source code</h2>
<div class="source-panel-heading">
<h2 id="program-source-title">Source code</h2>
<div class="source-panel-actions">
<button id="copy-program-source" class="source-action" type="button" disabled>Copy</button>
<a id="download-program-source" class="source-action" hidden>Download</a>
</div>
</div>
<span id="source-copy-feedback" class="sr-only" role="status"></span>
<p id="program-source-status" role="status">Loading source…</p>
<pre id="program-source-code" tabindex="0" aria-label="Program source code" hidden><code></code></pre>
</section>

View file

@ -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; }