Compare commits

...
4 Commits
Author SHA1 Message Date
Kristian Wind 661e47ebdf Merge pull request #102 from kristianwind/markoer-i-titlen
CI / OpenAPI is valid (push) Successful in 11s
CI / End-to-end (push) Successful in 15m22s
CI / Go (push) Failing after 26m42s
Release / Build and publish image (push) Failing after 9s
CI / Docker image builds (push) Successful in 2m36s
fix(noter): en ny note åbner med markøren i overskriften
2026-10-08 10:17:18 +02:00
kristianwindandClaude Opus 5 6178cbbdd8 Merge branch 'main' into markoer-i-titlen
Begge grene føjede prøver til i enden af smoke.spec.js, og begge beholdes:
fire markør-prøver og fire fra tal-en-opgave-ind. Den fælles `/**` ovenfor
dækkede kun den første, så den anden blok har fået sin egen.

Suiten på den løste fletning: 129 bestod, 0 netafbrydelser.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-08 10:06:42 +02:00
kristianwind 475ecbe7ef Merge branch 'afsnit-efter-retur' into markoer-i-titlen 2026-10-08 07:55:04 +02:00
kristianwindandClaude Opus 5 3768cf946e fix(noter): en ny note åbner med markøren i overskriften
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=<id>.
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 <[email protected]>
2026-10-08 07:16:22 +02:00
3 changed files with 140 additions and 1 deletions
+94
View File
@@ -5700,6 +5700,100 @@ 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 <h1> 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=<id>, 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([]);
});
/**
* Tal en opgave ind.
*
+43
View File
@@ -81,12 +81,55 @@
// fired.
editor.innerHTML = body.trim() ? markdownToHtml(body) : '<h1><br></h1>';
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=<id>.
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' },
+3 -1
View File
@@ -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));
}