From 3768cf946ee3e590cab36521484a09638ba42493 Mon Sep 17 00:00:00 2001 From: kristianwind Date: Thu, 8 Oct 2026 07:16:22 +0200 Subject: [PATCH] =?UTF-8?q?fix(noter):=20en=20ny=20note=20=C3=A5bner=20med?= =?UTF-8?q?=20mark=C3=B8ren=20i=20overskriften?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oprettelse af en note satte ingen markør. Siden kaldte `document.querySelector('.editor textarea')?.focus()` — og der er ingen textarea; editoren blev lavet om til en contenteditable, så kaldet har ramt ingenting lige siden, og `?.` sørgede for, at det aldrig larmede. Rettelsen ligger i NoteEditor frem for på siden, så begge veje er dækket: knappen på /noter og den på et projekt, der navigerer til /noter?note=. Reglen er, at en TOM note sætter markøren i sin overskrift — en note med indhold rører den ikke, for der er markøren brugerens eget valg. Fire prøver, og de er kontrolleret i begge retninger: uden kaldet giver den nye note `Expected: "h1", Received: null`, og med kaldet på enhver note giver kontrollen `Expected: null, Received: "h1"`. Co-Authored-By: Claude Opus 5 --- web/e2e/smoke.spec.js | 94 ++++++++++++++++++++++++ web/src/lib/components/NoteEditor.svelte | 43 +++++++++++ web/src/routes/noter/+page.svelte | 4 +- 3 files changed, 140 insertions(+), 1 deletion(-) diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index a967fbc..2c272b8 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -5620,3 +5620,97 @@ test('the verdande door is unaffected by the urd one', async ({ page }) => { expect(trouble).toEqual([]); }); + +/** + * En ny note åbner med markøren i sin titel. + * + * Titlen er første linje, som i Apple Notes — der er ikke et selvstændigt felt, + * så "sæt markøren i overskriften" betyder et område placeret inde i

i et + * contenteditable. Derfor måles der på markeringen og ikke på hvad der har fokus: + * editoren kan have fokus med markøren stående et helt andet sted. + */ +async function caretBlock(page) { + return page.evaluate(() => { + const s = window.getSelection(); + if (!s || !s.anchorNode) return null; + const node = s.anchorNode.nodeType === 1 ? s.anchorNode : s.anchorNode.parentElement; + const block = node?.closest('h1, h2, h3, p, li, pre'); + return block ? block.tagName.toLowerCase() : null; + }); +} + +test('en ny note åbner med markøren i overskriften', async ({ page }) => { + const trouble = watchForTrouble(page); + await page.goto('/noter'); + await page.getByRole('button', { name: 'Ny note' }).click(); + + expect(await caretBlock(page), 'markøren skal stå i h1').toBe('h1'); + + // Og den står et sted, man kan skrive: det, der tastes, bliver titlen, og + // titlen er det, listen kalder noten. + await page.keyboard.type('Pakkeliste til Island'); + await expect(page.locator('.editor h1').first()).toHaveText('Pakkeliste til Island'); + await expect(page.locator('.notes button.row strong').first()).toHaveText( + /^Pakkeliste til Island$/ + ); + + expect(trouble).toEqual([]); +}); + +test('en note med indhold stjæler ikke markøren', async ({ page }) => { + // Kontrollen, og den halvdel der gør det til en regel frem for en vane: åbner + // man en note, man har skrevet i forvejen, skal markøren IKKE hoppe op i + // overskriften og skubbe det, man var i gang med. + const trouble = watchForTrouble(page); + await page.goto('/noter'); + await page.getByRole('button', { name: 'Ny note' }).click(); + await page.keyboard.type('Allerede skrevet'); + // Vent til den er gemt, så den er en note med indhold næste gang den åbnes. + await page.waitForTimeout(1200); + + await page.reload(); + await page.getByRole('button', { name: /Allerede skrevet/ }).first().click(); + await expect(page.locator('.editor h1').first()).toHaveText('Allerede skrevet'); + expect(await caretBlock(page), 'ingen markør skal være sat').toBe(null); + + expect(trouble).toEqual([]); +}); + +test('urd-døren opfører sig ens: ny note, markør i overskriften', async ({ page }) => { + // Han bad om "Urd og noter". Det er samme side, men målt gennem døren, fordi + // det er den vej en bruger af urd-appen kommer — og fordi en regel, der kun er + // prøvet ad én vej, er prøvet ét sted. + const trouble = watchForTrouble(page); + await page.goto('/urd'); + await expect(page).toHaveURL(/\/noter$/); + await page.getByRole('button', { name: 'Ny note' }).click(); + + expect(await caretBlock(page), 'markøren skal stå i h1 også i urd').toBe('h1'); + expect(trouble).toEqual([]); +}); + +test('en note oprettet fra et projekt åbner også med markøren i overskriften', async ({ page }) => { + // Den anden vej ind, og den blev påstået dækket før den blev målt: projektets + // "Ny note" opretter noten og sender videre med ?note=, så markøren + // placeres af editoren på den anden side af en navigation frem for i samme + // hug. Reglen sidder i editoren netop for at dække begge — det er værdiløst + // uden en prøve, der går den anden vej. + const trouble = watchForTrouble(page); + await page.goto('/'); + + const sidebar = page.getByRole('navigation', { name: 'Hovedmenu' }); + await sidebar.getByLabel('Nyt projekt').click(); + await sidebar.getByLabel('Projektnavn').fill('Islandsturen'); + await sidebar.getByLabel('Projektnavn').press('Enter'); + await sidebar.getByRole('link', { name: 'Islandsturen' }).click(); + await expect(page.getByRole('heading', { name: 'Islandsturen' })).toBeVisible(); + + await page.getByRole('button', { name: 'Ny note', exact: true }).click(); + await expect(page).toHaveURL(/\/noter\?note=/); + + expect(await caretBlock(page), 'markøren skal stå i h1 efter navigationen').toBe('h1'); + await page.keyboard.type('Dæk og kort'); + await expect(page.locator('.editor h1').first()).toHaveText('Dæk og kort'); + + expect(trouble).toEqual([]); +}); diff --git a/web/src/lib/components/NoteEditor.svelte b/web/src/lib/components/NoteEditor.svelte index 47e6c29..9647b0e 100644 --- a/web/src/lib/components/NoteEditor.svelte +++ b/web/src/lib/components/NoteEditor.svelte @@ -81,12 +81,55 @@ // fired. editor.innerHTML = body.trim() ? markdownToHtml(body) : '


'; colourCode(); + // Og markøren sættes DER. + // + // Indtil nu lavede linjen ovenfor kun titelblokken, så sætningen i + // kommentaren — "opens on its title" — handlede om, hvad der blev tegnet, + // ikke om hvor man kunne skrive. Noter-siden prøvede at råde bod på det + // med `document.querySelector('.editor textarea')?.focus()`, og der er + // ingen textarea: editoren er et contenteditable. Det kald har ramt + // ingenting, stille, siden editoren blev lavet om, og `?.` sørgede for, + // at det aldrig kastede. + // + // Her frem for på siden, så begge veje ind er dækket: noter-sidens egen + // opret-knap, og projektsidens, der sender videre med ?note=. + if (!body.trim()) placeCaretInTitle(); } catch (e) { editor.textContent = body; console.error('verdande: noten kunne ikke tegnes', note.id, e); } }); + /** + * Sætter markøren i titlen på en tom note. + * + * En contenteditable har ikke et felt at fokusere — den har en markør, og den + * skal placeres. `focus()` alene giver elementet tastaturfokus og lader + * markøren stå, hvor browseren synes, hvilket for et tomt element er starten + * på dokumentet frem for inde i overskriften. + * + * Fokus FØRST og så området: at fokusere et contenteditable kan nulstille + * markeringen, så den omvendte rækkefølge sætter et område, der straks bliver + * kasseret igen. + * + * Hvad den IKKE kan, sagt her frem for opdaget på en telefon: åbne tastaturet + * på iOS. Det sker kun, når focus() kaldes inde i den brugerhandling, der bad + * om det — og en ny note skal først oprettes på serveren, så der ligger et + * `await` imellem. Markøren står rigtigt; på en telefon skal man stadig trykke + * én gang for at få tastaturet frem. + */ + function placeCaretInTitle() { + const heading = editor?.querySelector('h1'); + if (!heading) return; + editor.focus(); + const range = document.createRange(); + range.selectNodeContents(heading); + range.collapse(true); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); + } + const SYNTAX = [ { mark: '#', what: 'notes.syntaxProject' }, { mark: '[[', what: 'notes.syntaxNote' }, diff --git a/web/src/routes/noter/+page.svelte b/web/src/routes/noter/+page.svelte index 78be862..198ddf3 100644 --- a/web/src/routes/noter/+page.svelte +++ b/web/src/routes/noter/+page.svelte @@ -730,8 +730,10 @@ const note = await api.createNote({ body: '' }); notes = [note, ...notes]; // Svaret er hele noten, så den skal ikke hentes igen — se open(). + // Markøren sættes af editoren selv, når noten er tom — se + // placeCaretInTitle i NoteEditor. Her stod en focus() på + // `.editor textarea`, og editoren har ingen textarea. await open(note, { whole: true }); - document.querySelector('.editor textarea')?.focus(); } catch (e) { app.toast(humanMessage(e)); }