Files
dmtools/assets/controllers/clipboard_controller.js
T
FrankandClaude Sonnet 5 ed22b81e97 Art: full-image thumbnails, detail page with copy-to-clipboard
- gallery thumbnails now object-fit: contain (whole image, letterboxed),
  and a card links to the new detail page instead of the raw file
- /art/{id} detail page: large uncropped image, "Open full size",
  "Delete" (moved here from the gallery), and "Copy image"
- clipboard Stimulus controller copies the image to the OS clipboard
  (converts non-PNG via canvas) for pasting into WhatsApp etc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:53:49 +02:00

71 lines
2.4 KiB
JavaScript

import { Controller } from '@hotwired/stimulus';
/*
* Copies an image to the OS clipboard so it can be pasted into WhatsApp etc.
*
* <button data-controller="clipboard"
* data-clipboard-url-value="/art/5/file"
* data-action="clipboard#copy">Copy image</button>
*
* The Clipboard API only reliably accepts image/png, so non-PNG sources are
* redrawn through a canvas first. Needs a secure context (https or localhost).
*/
export default class extends Controller {
static values = { url: String };
async copy(event) {
const btn = event.currentTarget;
if (!navigator.clipboard || !window.ClipboardItem) {
this.flash(btn, 'Not supported');
return;
}
this.flash(btn, 'Copying…', 0);
try {
const res = await fetch(this.urlValue, { credentials: 'same-origin' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
let blob = await res.blob();
if (blob.type !== 'image/png') {
blob = await this.toPng(blob);
}
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
this.flash(btn, 'Copied!');
} catch (e) {
console.error('clipboard copy failed', e);
this.flash(btn, 'Copy failed');
}
}
toPng(blob) {
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(blob);
img.onload = () => {
URL.revokeObjectURL(url);
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
canvas.getContext('2d').drawImage(img, 0, 0);
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('image decode failed'));
};
img.src = url;
});
}
flash(btn, msg, revertAfter = 1800) {
if (!btn.dataset.label) btn.dataset.label = btn.textContent;
btn.textContent = msg;
clearTimeout(this._t);
if (revertAfter > 0) {
this._t = setTimeout(() => {
btn.textContent = btn.dataset.label;
}, revertAfter);
}
}
}