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