imaging: copy button HTTP fallback (execCommand)

navigator.clipboard.writeText is gated on isSecureContext - HTTPS
or localhost only. PXE dashboard is served over plain HTTP
(10.9.100.1:9009) so the API was undefined and the chain threw
before .catch fired - user saw nothing. Wrap clipboard write in
copyText() that prefers the modern API and falls back to the
classic invisible-textarea + document.execCommand('copy') path
which works on HTTP. Visual flash logic moved into flashCopied()
for reuse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-05-13 18:30:16 -04:00
parent 6275a6a2b3
commit 0eb52c6a9e

View File

@@ -138,14 +138,36 @@ Content-Type: application/json
{% block extra_scripts %} {% block extra_scripts %}
<script> <script>
document.addEventListener('click', function(e) { function copyText(text) {
var btn = e.target.closest('.copy-btn'); // Modern path - only works over HTTPS or localhost
if (!btn) return; if (navigator.clipboard && window.isSecureContext) {
var text = btn.getAttribute('data-copy-text'); return navigator.clipboard.writeText(text);
if (!text) return; }
navigator.clipboard.writeText(text).then(function() { // Legacy fallback for plain HTTP
return new Promise(function(resolve, reject) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
ta.style.top = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
try {
var ok = document.execCommand('copy');
document.body.removeChild(ta);
if (ok) resolve(); else reject(new Error('execCommand returned false'));
} catch (err) {
document.body.removeChild(ta);
reject(err);
}
});
}
function flashCopied(btn, success) {
var origText = btn.dataset.origText || btn.textContent; var origText = btn.dataset.origText || btn.textContent;
btn.dataset.origText = origText; btn.dataset.origText = origText;
if (success) {
btn.textContent = 'copied!'; btn.textContent = 'copied!';
btn.classList.remove('btn-outline-secondary'); btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-success'); btn.classList.add('btn-success');
@@ -156,16 +178,25 @@ document.addEventListener('click', function(e) {
btn.classList.add('btn-outline-secondary'); btn.classList.add('btn-outline-secondary');
btn.style.transform = 'scale(1)'; btn.style.transform = 'scale(1)';
}, 1200); }, 1200);
}).catch(function(err) { } else {
btn.textContent = 'failed'; btn.textContent = 'failed';
btn.classList.remove('btn-outline-secondary'); btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-danger'); btn.classList.add('btn-danger');
setTimeout(function() { setTimeout(function() {
btn.textContent = btn.dataset.origText || 'copy'; btn.textContent = origText;
btn.classList.remove('btn-danger'); btn.classList.remove('btn-danger');
btn.classList.add('btn-outline-secondary'); btn.classList.add('btn-outline-secondary');
}, 1500); }, 1500);
}); }
}
document.addEventListener('click', function(e) {
var btn = e.target.closest('.copy-btn');
if (!btn) return;
var text = btn.getAttribute('data-copy-text');
if (!text) return;
copyText(text).then(function() { flashCopied(btn, true); })
.catch(function(err) { console.error('copy failed:', err); flashCopied(btn, false); });
}); });
</script> </script>
{% endblock %} {% endblock %}