Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6178cbbdd8 | ||
|
|
43bd405e36 | ||
|
|
b73f55ad0b | ||
|
|
475ecbe7ef | ||
|
|
159029675c | ||
|
|
b18d9a26e6 | ||
|
|
3768cf946e |
@@ -5102,6 +5102,85 @@ test('flere markerede afsnit bliver til én monotypeblok', async ({ page }) => {
|
|||||||
expect(trouble).toEqual([]);
|
expect(trouble).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blokke, som retur efterlod som <div>, bliver brødtekst — også bagefter.
|
||||||
|
*
|
||||||
|
* Chromium svarer på retur med en bar <div>. Den læses som brødtekst og er ikke
|
||||||
|
* formateret som en, så afstanden mellem afsnit forsvinder stille. Rettelsen er
|
||||||
|
* udskudt ét hak — før delingen er der ingen ny blok at rette — og den spørger,
|
||||||
|
* hvor markøren står. Skriver man hurtigt, når næste retur igennem først, og så
|
||||||
|
* retter den den næste blok to gange og lader denne stå.
|
||||||
|
*
|
||||||
|
* Det er målt frem for gættet: på en CI-runner faldt "flere markerede afsnit
|
||||||
|
* bliver til én monotypeblok" med tre linjer rigtigt skrevet, hvor "linje et" var
|
||||||
|
* en <div> og "linje to" og "linje tre" var <p>.
|
||||||
|
*
|
||||||
|
* Racet selv kan ikke skrives som en pålidelig prøve — det afhænger af, hvornår
|
||||||
|
* runneren når sine timere. Derfor planter denne den tilstand, racet efterlader,
|
||||||
|
* og holder på invarianten i stedet: ingen blok i en note er browserens egen
|
||||||
|
* beholder, uanset hvor markøren har været i mellemtiden.
|
||||||
|
*/
|
||||||
|
test('blokke retur efterlod som <div> bliver brødtekst alligevel', async ({ page }) => {
|
||||||
|
const trouble = watchForTrouble(page);
|
||||||
|
await page.goto('/noter');
|
||||||
|
|
||||||
|
const ed = await nyNote(page);
|
||||||
|
await page.keyboard.type('Overskrift');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.keyboard.type('første afsnit');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.keyboard.type('andet afsnit');
|
||||||
|
await expect(ed.locator('p')).toHaveCount(2);
|
||||||
|
|
||||||
|
// Tilstanden racet efterlader: rettelserne løb, mens markøren stod et andet sted,
|
||||||
|
// så ingen af blokkene blev lavet om. To af dem, fordi én er for let: en rettelse,
|
||||||
|
// der holder fast i et håndtag til blokken, kan nå den første og mister den næste,
|
||||||
|
// når den første ombytning tager dens node ud af dokumentet.
|
||||||
|
await ed.evaluate((el) => {
|
||||||
|
for (const para of [...el.querySelectorAll('p')]) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
while (para.firstChild) div.appendChild(para.firstChild);
|
||||||
|
para.replaceWith(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Markøren tilbage for enden af den sidste, hvor racet ville have efterladt den:
|
||||||
|
// brugeren skriver videre. At erstatte blokken kollapser markeringen til
|
||||||
|
// begyndelsen, og så bliver plantningen selv den fejl, prøven måler.
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(el.lastElementChild);
|
||||||
|
range.collapse(false);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
});
|
||||||
|
await expect(ed.locator('p')).toHaveCount(0);
|
||||||
|
await expect(ed.locator('> div')).toHaveCount(2);
|
||||||
|
|
||||||
|
// Næste retur henter dem begge ind igen, sammen med den nye linje.
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.keyboard.type('tredje afsnit');
|
||||||
|
|
||||||
|
await expect(ed.locator('> div')).toHaveCount(0);
|
||||||
|
await expect(ed.locator('p')).toHaveCount(3);
|
||||||
|
await expect(ed.locator('h1')).toHaveCount(1);
|
||||||
|
|
||||||
|
// Og teksten er den samme hele vejen — en rettelse, der taber et ord, er ingen
|
||||||
|
// rettelse. Den skal også overleve en gemning.
|
||||||
|
const gemt = noteGemt(page);
|
||||||
|
await ed.blur();
|
||||||
|
await gemt;
|
||||||
|
await page.reload();
|
||||||
|
await page.getByRole('button', { name: /Overskrift/ }).click();
|
||||||
|
|
||||||
|
await expect(ed.locator('> div')).toHaveCount(0);
|
||||||
|
await expect(ed.locator('p')).toHaveCount(3);
|
||||||
|
expect(await ed.locator('p').nth(0).textContent()).toBe('første afsnit');
|
||||||
|
expect(await ed.locator('p').nth(1).textContent()).toBe('andet afsnit');
|
||||||
|
expect(await ed.locator('p').nth(2).textContent()).toBe('tredje afsnit');
|
||||||
|
|
||||||
|
expect(trouble).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hele kontoens opgaver kan hentes i én omgang.
|
* Hele kontoens opgaver kan hentes i én omgang.
|
||||||
*
|
*
|
||||||
@@ -5621,6 +5700,100 @@ test('the verdande door is unaffected by the urd one', async ({ page }) => {
|
|||||||
expect(trouble).toEqual([]);
|
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.
|
* Tal en opgave ind.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -81,12 +81,55 @@
|
|||||||
// fired.
|
// fired.
|
||||||
editor.innerHTML = body.trim() ? markdownToHtml(body) : '<h1><br></h1>';
|
editor.innerHTML = body.trim() ? markdownToHtml(body) : '<h1><br></h1>';
|
||||||
colourCode();
|
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) {
|
} catch (e) {
|
||||||
editor.textContent = body;
|
editor.textContent = body;
|
||||||
console.error('verdande: noten kunne ikke tegnes', note.id, e);
|
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 = [
|
const SYNTAX = [
|
||||||
{ mark: '#', what: 'notes.syntaxProject' },
|
{ mark: '#', what: 'notes.syntaxProject' },
|
||||||
{ mark: '[[', what: 'notes.syntaxNote' },
|
{ mark: '[[', what: 'notes.syntaxNote' },
|
||||||
@@ -821,23 +864,54 @@
|
|||||||
* the caret in another heading in most browsers, and a note where every line is
|
* the caret in another heading in most browsers, and a note where every line is
|
||||||
* a title is a note with no title at all.
|
* a title is a note with no title at all.
|
||||||
*/
|
*/
|
||||||
function keepTitleFirst() {
|
function keepTitleFirst(split) {
|
||||||
const block = currentBlock();
|
if (!editor) return;
|
||||||
if (!block) return;
|
|
||||||
|
|
||||||
// A heading that is not the first line becomes body.
|
// Der hvor markøren står, gør execCommand det, for det er den, der lægger
|
||||||
if (block.tagName === 'H1' && block !== editor.firstElementChild) {
|
// ændringen i browserens egen fortryd-historik — ⌘Z skal tage afsnittet tilbage,
|
||||||
document.execCommand('formatBlock', false, 'p');
|
// ligesom den tager et ord tilbage.
|
||||||
return;
|
const caret = currentBlock();
|
||||||
}
|
if (caret && stray(caret)) document.execCommand('formatBlock', false, 'p');
|
||||||
|
|
||||||
// And the browser's own container becomes a paragraph. Chromium answers Enter
|
// Retur for enden af en overskrift efterlader endnu en overskrift i de fleste
|
||||||
// with a bare <div>, which reads as body but is not styled as one — so the
|
// browsere, og markøren kan være løbet videre, inden rettelsen når frem. Kun den
|
||||||
// spacing between paragraphs quietly disappears the moment somebody presses
|
// blok, delingen lige skabte: en <h1> andre steder i noten kan være en linje,
|
||||||
// return rather than choosing Brødtekst from the menu.
|
// nogen har valgt Titel til i menuen, og den er deres at beholde.
|
||||||
if (block.tagName === 'DIV') {
|
const lavet = split?.nextElementSibling;
|
||||||
document.execCommand('formatBlock', false, 'p');
|
if (lavet && lavet !== currentBlock() && stray(lavet)) swapTag(lavet, 'p');
|
||||||
}
|
|
||||||
|
// Og hver <div>, rettelsen ikke nåede.
|
||||||
|
//
|
||||||
|
// Den er udskudt ét hak, fordi der før delingen ikke er nogen ny blok at rette.
|
||||||
|
// Men et hak er nok: skriver man hurtigt, når næste retur igennem først, og så
|
||||||
|
// står markøren i den NÆSTE blok. Så rettede den den — to gange — og lod den
|
||||||
|
// blok, rettelsen var ment for, stå tilbage. Målt på en CI-runner, hvor "linje
|
||||||
|
// et" blev en <div> mens "linje to" og "linje tre" blev <p>: brødtekst uden
|
||||||
|
// afstand, hos netop de brugere der skriver hurtigst.
|
||||||
|
//
|
||||||
|
// Den spørger noten frem for et håndtag. Et håndtag holder ikke: når den første
|
||||||
|
// udskudte rettelse bytter tagget på en blok, peger den næstes håndtag på en
|
||||||
|
// node, der ikke længere sidder i dokumentet, og så bliver blokken midt i
|
||||||
|
// kæden aldrig rettet. En <div> er altid browserens egen beholder og aldrig
|
||||||
|
// nogens valg, så den kan fejes op hvor som helst uden at tage noget fra nogen.
|
||||||
|
const her = currentBlock();
|
||||||
|
for (const block of [...editor.children])
|
||||||
|
if (block !== her && block.tagName === 'DIV') swapTag(block, 'p');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* En blok, der ikke må stå, som den står.
|
||||||
|
*
|
||||||
|
* Browserens egen beholder: Chromium svarer på retur med en bar <div>, som læses
|
||||||
|
* som brødtekst og ikke er formateret som en, så afstanden mellem afsnit stille
|
||||||
|
* forsvinder. Eller en overskrift, der ikke er den første linje — en note, hvor
|
||||||
|
* hver linje er en titel, er en note uden titel.
|
||||||
|
*/
|
||||||
|
function stray(block) {
|
||||||
|
if (block.parentNode !== editor) return false;
|
||||||
|
return (
|
||||||
|
block.tagName === 'DIV' || (block.tagName === 'H1' && block !== editor.firstElementChild)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -912,9 +986,10 @@
|
|||||||
changed();
|
changed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// After the split, not before it.
|
// After the split, not before it — and with the block the split started from,
|
||||||
|
// because by the time this runs the caret may be two blocks further on.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
keepTitleFirst();
|
keepTitleFirst(block);
|
||||||
changed();
|
changed();
|
||||||
}, 0);
|
}, 0);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -730,8 +730,10 @@
|
|||||||
const note = await api.createNote({ body: '' });
|
const note = await api.createNote({ body: '' });
|
||||||
notes = [note, ...notes];
|
notes = [note, ...notes];
|
||||||
// Svaret er hele noten, så den skal ikke hentes igen — se open().
|
// 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 });
|
await open(note, { whole: true });
|
||||||
document.querySelector('.editor textarea')?.focus();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
app.toast(humanMessage(e));
|
app.toast(humanMessage(e));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user