diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index 314d794..9496cba 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -5440,6 +5440,84 @@ test('et indsat billede kan åbnes stort og kopieres', async ({ page, context }) expect(trouble).toEqual([]); }); +/** + * Et billede bliver større af at blive åbnet — ikke mindre. + * + * Rapporteret som "billederne er meget små, når man klikker på dem". Ruden havde + * kun `max-width` og `max-height`, så den kunne gøre et billede mindre og aldrig + * større. Målt i den rigtige app: et lille billede (600×400) stod 600 pixels bredt + * i noten og 600 i ruden, og et højt skærmbillede fra en telefon stod 701 bredt i + * noten og 357 i ruden. At klikke for at se billedet bedre gav et dårligere billede. + * + * Målt på kasserne, med tal: "billedet er synligt" var sandt hele tiden. + */ +test('et åbnet billede fylder skærmen og kan ses i fuld størrelse', async ({ page }) => { + const trouble = watchForTrouble(page); + await page.goto('/noter'); + await page.getByRole('button', { name: 'Ny note' }).click(); + const body = page.getByLabel('Notens tekst'); + await body.fill('Billednote med mål'); + + const paste = (w, h) => + body.evaluate(async (el, [w, h]) => { + const c = document.createElement('canvas'); + c.width = w; + c.height = h; + const g = c.getContext('2d'); + g.fillStyle = '#c33'; + g.fillRect(0, 0, w, h); + const blob = await new Promise((r) => c.toBlob(r, 'image/png')); + const data = new DataTransfer(); + data.items.add(new File([blob], 'skud.png', { type: 'image/png' })); + el.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true })); + }, [w, h]); + + const viewport = page.viewportSize(); + const shot = page.getByRole('dialog', { name: 'Billede' }); + const big = shot.locator('img'); + + // Et lille billede: ruden forstørrer det, til det fylder skærmen i den ene led. + await paste(600, 400); + const small = page.locator('[aria-label="Notens tekst"] img').last(); + await expect(small).toHaveJSProperty('complete', true); + const inNote = await small.boundingBox(); + await small.click(); + await expect(big).toHaveClass(/sized/); + const fit = await big.boundingBox(); + expect(fit.width).toBeGreaterThan(inNote.width * 1.5); + expect(fit.width).toBeLessThanOrEqual(viewport.width); + expect(fit.y + fit.height).toBeLessThanOrEqual(viewport.height); + + // Fuld størrelse er aldrig mindre end det, man kom fra. + await shot.getByRole('button', { name: 'Fuld størrelse' }).click(); + const full = await big.boundingBox(); + expect(full.width).toBeGreaterThanOrEqual(fit.width); + await shot.getByRole('button', { name: 'Tilpas skærmen' }).click(); + await page.keyboard.press('Escape'); + await expect(shot).toHaveCount(0); + + // Et højt skærmbillede fra en telefon: i fuld størrelse er det mindst lige så + // bredt som i noten, og ruden ruller, så hele billedet kan nås. + await paste(1170, 2532); + const tall = page.locator('[aria-label="Notens tekst"] img').last(); + await expect(tall).toHaveJSProperty('complete', true); + const tallInNote = await tall.boundingBox(); + await tall.click(); + await expect(big).toHaveClass(/sized/); + await big.click(); + const tallFull = await big.boundingBox(); + expect(tallFull.width).toBeGreaterThanOrEqual(tallInNote.width); + expect(tallFull.width).toBeGreaterThanOrEqual(1170); + const scroll = await shot.evaluate((d) => d.scrollHeight - d.clientHeight); + expect(scroll).toBeGreaterThan(1000); + // Et klik på billedet igen bringer det tilbage til skærmens mål. + await big.click(); + const back = await big.boundingBox(); + expect(back.y + back.height).toBeLessThanOrEqual(viewport.height); + + expect(trouble).toEqual([]); +}); + /** * En opgaves tekst kan læses færdig, også på en telefon. * diff --git a/web/src/lib/components/NoteEditor.svelte b/web/src/lib/components/NoteEditor.svelte index 86e97f6..302885d 100644 --- a/web/src/lib/components/NoteEditor.svelte +++ b/web/src/lib/components/NoteEditor.svelte @@ -13,6 +13,7 @@ * unforgivable. When the replacement (the EditContext API) is everywhere, this * is the file to change. */ + import { tick } from 'svelte'; import { markdownToHtml, htmlToMarkdown, blockText } from '$lib/richtext.js'; import { highlight, guessLanguage } from '$lib/highlight.js'; import { t } from '$lib/i18n.svelte.js'; @@ -756,6 +757,8 @@ if (image) { event.preventDefault(); showing = { src: image.currentSrc || image.src, alt: image.alt ?? '' }; + zoomed = false; + natural = null; copied = false; copyError = ''; return; @@ -769,6 +772,49 @@ let copied = $state(false); let copyError = $state(''); + // Ruden viste billedet med `max-width`/`max-height` og intet andet, så det kunne + // blive mindre, men aldrig større. Det var mindre end i noten, det kom fra: et + // skærmbillede fra en telefon på en skærm i bredformat stod 701 pixels bredt i + // arket og 357 i ruden, og et lille billede blev slet ikke forstørret. Nu fylder + // det skærmen — også når det skal forstørres for at gøre det — og et klik på + // billedet viser det i sin egen størrelse, til at rulle rundt i. + let zoomed = $state(false); + /** @type {{ w: number, h: number } | null} */ + let natural = $state(null); + /** @type {HTMLDivElement | null} */ + let lightbox = $state(null); + + /** @param {Event & { currentTarget: HTMLImageElement }} event */ + function measured(event) { + const img = event.currentTarget; + natural = { w: img.naturalWidth, h: img.naturalHeight }; + } + + /** + * Skift mellem "hele skærmen" og "fuld størrelse". + * + * Fra et klik på billedet rulles der hen til det sted, man klikkede, så man + * ikke lander i øverste venstre hjørne og skal lede efter det, man ville se. + * + * @param {MouseEvent} [event] + */ + async function toggleZoom(event) { + const img = lightbox?.querySelector('.shot'); + let fx = 0.5; + let fy = 0.5; + if (event?.clientX !== undefined && event.detail > 0 && img) { + const r = img.getBoundingClientRect(); + fx = (event.clientX - r.left) / r.width; + fy = (event.clientY - r.top) / r.height; + } + zoomed = !zoomed; + await tick(); + if (!zoomed || !img || !lightbox) return; + const r = img.getBoundingClientRect(); + lightbox.scrollLeft += r.left + r.width * fx - lightbox.clientWidth / 2; + lightbox.scrollTop += r.top + r.height * fy - lightbox.clientHeight / 2; + } + function closeImage() { showing = null; // Tilbage i teksten, hvor man var. Uden det står fokus på en knap, der ikke @@ -1293,15 +1339,35 @@ af netop det ark, det skal lægge sig over. --> {#if showing} -