Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44b28f1cd9 | ||
|
|
b497b76be4 | ||
|
|
dba99b7ecc | ||
|
|
f34690f41a |
@@ -4797,6 +4797,24 @@ test('en søgning i ⌘K efterlader de træffere, den fandt', async ({ page }) =
|
|||||||
await expect(list.getByText('Fyrreskov syd')).toBeVisible();
|
await expect(list.getByText('Fyrreskov syd')).toBeVisible();
|
||||||
await expect(list.getByText('Egeskov')).toHaveCount(0);
|
await expect(list.getByText('Egeskov')).toHaveCount(0);
|
||||||
|
|
||||||
|
// Og de træffere kan ÅBNES. Adressen bærer stadig `?note=` på den, ⌘K åbnede,
|
||||||
|
// og den blev læst igen hver gang noget ændrede sig — så et klik på en anden
|
||||||
|
// række satte `selectedId`, effekten så den uenig med adressen, og trak én
|
||||||
|
// tilbage til den første. Man kunne søge og ikke vælge.
|
||||||
|
await list.getByText('Fyrreskov syd').click();
|
||||||
|
await expect(page.getByLabel('Notens tekst')).toContainText('Fyrreskov syd');
|
||||||
|
await expect(page.getByLabel('Notens tekst')).not.toContainText('Fyrreskov nord');
|
||||||
|
|
||||||
|
// Og tilbage til den, adressen stadig nævner. Det tilfælde er grunden til, at
|
||||||
|
// åbningen hænger på navigationen og ikke på adressens værdi: `?note=` peger
|
||||||
|
// stadig på "Fyrreskov nord", så en rettelse, der blot læste den én gang og
|
||||||
|
// huskede værdien, ville se den uændret og lade være med at åbne noget. Man
|
||||||
|
// kunne så ikke komme tilbage til den note, man kom fra.
|
||||||
|
await page.getByRole('button', { name: /Søg/ }).click();
|
||||||
|
await palette.getByRole('textbox').fill('Fyrreskov nord');
|
||||||
|
await palette.locator('li button', { hasText: 'Fyrreskov nord' }).click();
|
||||||
|
await expect(page.getByLabel('Notens tekst')).toContainText('Fyrreskov nord');
|
||||||
|
|
||||||
// Feltet kan tømmes, og listen kommer hel igen. Adressen beholder sit `q` —
|
// Feltet kan tømmes, og listen kommer hel igen. Adressen beholder sit `q` —
|
||||||
// den skrives kun af navigation. Den første udgave skrev den ved hvert
|
// den skrives kun af navigation. Den første udgave skrev den ved hvert
|
||||||
// tastetryk, så den kunne deles, og det kostede bogstaver ud af feltet; se
|
// tastetryk, så den kunne deles, og det kostede bogstaver ud af feltet; se
|
||||||
@@ -4923,6 +4941,207 @@ test('klokkens panel bliver inden for skærmen på en telefon', async ({ page })
|
|||||||
expect(trouble).toEqual([]);
|
expect(trouble).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* En punktliste kan have niveauer, og de overlever gemningen.
|
||||||
|
*
|
||||||
|
* Tab rykker ind og shift+Tab ud, som i ethvert andet program med lister. Det var
|
||||||
|
* ikke bundet til noget: Tab uden for forslagslisten gjorde det, browseren gør med
|
||||||
|
* Tab, altså flyttede fokus ud af noten. Der var ingen vej til et niveau to.
|
||||||
|
*
|
||||||
|
* Omformeren kunne begge veje i forvejen — indrykning bærer niveauet i Markdown —
|
||||||
|
* men skriveren læste kun en underliste, der ligger INDE i sit punkt. Lægger
|
||||||
|
* browseren den som søskende, og det gør de uens, blev grenen sprunget over uden
|
||||||
|
* lyd: punktet forsvandt ved gemningen. Begge former læses nu.
|
||||||
|
*
|
||||||
|
* Målt efter en genindlæsning. Før den står indrykningen på skærmen uanset hvad —
|
||||||
|
* det er turen gennem Markdown, der afgør, om den blev skrevet ned.
|
||||||
|
*/
|
||||||
|
test('en punktliste kan have niveauer, der overlever gemningen', async ({ page }) => {
|
||||||
|
const trouble = watchForTrouble(page);
|
||||||
|
await page.goto('/noter');
|
||||||
|
|
||||||
|
await nyNote(page);
|
||||||
|
await page.keyboard.type('Indkøb');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.getByRole('button', { name: 'Formatér' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Punktliste' }).click();
|
||||||
|
await page.keyboard.type('Frugt');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.keyboard.type('Aebler');
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
|
||||||
|
const ed = page.getByRole('textbox', { name: 'Notens tekst' });
|
||||||
|
// Niveauet er der med det samme: en liste inde i en liste.
|
||||||
|
await expect(ed.locator('ul ul li')).toHaveText(['Aebler']);
|
||||||
|
|
||||||
|
const gemt = noteGemt(page);
|
||||||
|
await ed.blur();
|
||||||
|
await gemt;
|
||||||
|
await page.reload();
|
||||||
|
await page.getByRole('button', { name: /Indkøb/ }).click();
|
||||||
|
|
||||||
|
// Og efter turen gennem Markdown står det der stadig.
|
||||||
|
await expect(ed.locator('ul ul li')).toHaveText(['Aebler']);
|
||||||
|
await expect(ed.locator('ul > li').first()).toContainText('Frugt');
|
||||||
|
|
||||||
|
// Og tilbage igen: shift+Tab er vejen ud, ellers er niveauet en envejsdør.
|
||||||
|
await ed.locator('ul ul li').click();
|
||||||
|
await page.keyboard.press('Shift+Tab');
|
||||||
|
await expect(ed.locator('ul ul li')).toHaveCount(0);
|
||||||
|
|
||||||
|
expect(trouble).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Farvelægningen skriver ikke linjerne sammen i en blok, man lige har forladt.
|
||||||
|
*
|
||||||
|
* Blokken farves, når markøren forlader den, og resultatet males TILBAGE ind i
|
||||||
|
* den. Den læsning havde sin egen kopi af "hvad er teksten i en blok", og kopien
|
||||||
|
* brugte `textContent` — hvor et <br> er den tomme streng. Så en blok skrevet med
|
||||||
|
* shift+retur fik sine linjer skrevet sammen i det øjeblik, man klikkede væk. På
|
||||||
|
* skærmen, ikke kun i filen, og uden at man havde rørt teksten.
|
||||||
|
*
|
||||||
|
* Kommentaren over kopien sagde "ét sted, fordi to steder skal bruge det samme
|
||||||
|
* svar". Der var to, og de var uenige om netop det tegn.
|
||||||
|
*/
|
||||||
|
test('et klik væk fra en kodeblok skriver ikke dens linjer sammen', async ({ page }) => {
|
||||||
|
const trouble = watchForTrouble(page);
|
||||||
|
await page.goto('/noter');
|
||||||
|
|
||||||
|
await nyNote(page);
|
||||||
|
await page.keyboard.type('Farvelagt');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.getByRole('button', { name: 'Formatér' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Monotype' }).click();
|
||||||
|
await page.keyboard.type('echo en');
|
||||||
|
await page.keyboard.press('Shift+Enter');
|
||||||
|
await page.keyboard.type('echo to');
|
||||||
|
|
||||||
|
const ed = page.getByRole('textbox', { name: 'Notens tekst' });
|
||||||
|
// Ud af blokken, så farvelægningen tager fat i den.
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
await page.keyboard.press('End');
|
||||||
|
await ed.getByText('Farvelagt', { exact: true }).click();
|
||||||
|
|
||||||
|
// Kopiér-knappen ligger inde i blokken for at kunne sidde i hjørnet af den, så
|
||||||
|
// den skal ud, før teksten læses — præcis det, koden selv gør.
|
||||||
|
const tekst = await ed.locator('pre').evaluate((el) => {
|
||||||
|
const kopi = el.cloneNode(true);
|
||||||
|
kopi.querySelectorAll('.copy').forEach((b) => b.remove());
|
||||||
|
return kopi.innerText.trim();
|
||||||
|
});
|
||||||
|
expect(tekst).toBe('echo en\necho to');
|
||||||
|
|
||||||
|
expect(trouble).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flere markerede afsnit bliver til ÉN monotypeblok — ikke kun det øverste.
|
||||||
|
*
|
||||||
|
* Monotype går uden om `execCommand`, fordi formatBlock pakker et <p> ind i
|
||||||
|
* <pre>'en i stedet for at lave den om. Den vej spurgte efter `currentBlock()`,
|
||||||
|
* altså det ene blok markøren står i — så af en markering over ti linjers
|
||||||
|
* terminaludskrift blev den første linje til kode og resten stod tilbage som
|
||||||
|
* brødtekst.
|
||||||
|
*
|
||||||
|
* Og det skal være én blok, ikke tre. En udskrift, man indsætter, er ét stykke
|
||||||
|
* kode; tre blokke ved siden af hinanden ser næsten ens ud på skærmen og er tre
|
||||||
|
* hegn i filen.
|
||||||
|
*/
|
||||||
|
test('flere markerede afsnit bliver til én monotypeblok', async ({ page }) => {
|
||||||
|
const trouble = watchForTrouble(page);
|
||||||
|
await page.goto('/noter');
|
||||||
|
|
||||||
|
await nyNote(page);
|
||||||
|
await page.keyboard.type('Udskrift');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.keyboard.type('linje et');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.keyboard.type('linje to');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.keyboard.type('linje tre');
|
||||||
|
|
||||||
|
const ed = page.getByRole('textbox', { name: 'Notens tekst' });
|
||||||
|
await expect(ed.locator('p')).toHaveCount(3);
|
||||||
|
|
||||||
|
// Markér alle tre, som man gør det med musen eller tastaturet: fra begyndelsen
|
||||||
|
// af den første til enden af den sidste.
|
||||||
|
await ed.getByText('linje et', { exact: true }).click();
|
||||||
|
await page.keyboard.press('Home');
|
||||||
|
await page.keyboard.press('Shift+ArrowDown');
|
||||||
|
await page.keyboard.press('Shift+ArrowDown');
|
||||||
|
await page.keyboard.press('Shift+End');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Formatér' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Monotype' }).click();
|
||||||
|
|
||||||
|
// Én blok med alle tre linjer, og ingen brødtekst tilbage.
|
||||||
|
const blok = ed.locator('pre');
|
||||||
|
await expect(blok).toHaveCount(1);
|
||||||
|
await expect(ed.locator('p')).toHaveCount(0);
|
||||||
|
|
||||||
|
const gemt = noteGemt(page);
|
||||||
|
await ed.blur();
|
||||||
|
await gemt;
|
||||||
|
await page.reload();
|
||||||
|
await page.getByRole('button', { name: /Udskrift/ }).click();
|
||||||
|
|
||||||
|
await expect(ed.locator('pre')).toHaveCount(1);
|
||||||
|
expect(await ed.locator('pre code').evaluate((el) => el.textContent.trim())).toBe(
|
||||||
|
'linje et\nlinje to\nlinje tre'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(trouble).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* En kodeblok med flere linjer kommer tilbage med dem alle.
|
||||||
|
*
|
||||||
|
* Shift+retur bliver i blokken, som prøven ovenfor viser — men det, browseren
|
||||||
|
* lægger ind, er et <br>, og `textContent` på et <br> er den tomme streng. Blokken
|
||||||
|
* blev derfor skrevet til Markdown som én linje: "linje etlinje to". Man kunne
|
||||||
|
* skrive flere linjer og ikke gemme dem.
|
||||||
|
*
|
||||||
|
* Målt efter en genindlæsning og ikke før. Før gemningen står begge linjer der —
|
||||||
|
* det er turen gennem Markdown, der taber dem, og det er præcis den, prøven
|
||||||
|
* ovenfor stopper lige før.
|
||||||
|
*/
|
||||||
|
test('en kodeblok med flere linjer overlever gemningen', async ({ page }) => {
|
||||||
|
const trouble = watchForTrouble(page);
|
||||||
|
await page.goto('/noter');
|
||||||
|
|
||||||
|
await nyNote(page);
|
||||||
|
await page.keyboard.type('Opskrift');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await page.getByRole('button', { name: 'Formatér' }).click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Monotype' }).click();
|
||||||
|
await page.keyboard.type('first line');
|
||||||
|
await page.keyboard.press('Shift+Enter');
|
||||||
|
await page.keyboard.type('second line');
|
||||||
|
|
||||||
|
const ed = page.getByRole('textbox', { name: 'Notens tekst' });
|
||||||
|
const gemt = noteGemt(page);
|
||||||
|
await ed.blur();
|
||||||
|
await gemt;
|
||||||
|
await page.reload();
|
||||||
|
await page.getByRole('button', { name: /Opskrift/ }).click();
|
||||||
|
|
||||||
|
// Én blok, to linjer. Teksten læses rå frem for gennem `toHaveText`, som
|
||||||
|
// normaliserer mellemrum væk — og linjeskiftet ER det, der måles.
|
||||||
|
//
|
||||||
|
// Og på <code>, ikke på <pre>: kopiér-knappen ligger inde i blokken for at
|
||||||
|
// kunne sidde i hjørnet af den, så `textContent` på <pre> har ordet "Kopiér"
|
||||||
|
// med. Det er samme fælde, som selve fejlen handler om — hvad der tæller med i
|
||||||
|
// en blocks tekst — bare fra den anden side.
|
||||||
|
const blok = ed.locator('pre');
|
||||||
|
await expect(blok).toHaveCount(1);
|
||||||
|
expect(await blok.locator('code').evaluate((el) => el.textContent.trim())).toBe(
|
||||||
|
'first line\nsecond line'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(trouble).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Et indsat billede kan ses i fuld størrelse og kopieres videre.
|
* Et indsat billede kan ses i fuld størrelse og kopieres videre.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* unforgivable. When the replacement (the EditContext API) is everywhere, this
|
* unforgivable. When the replacement (the EditContext API) is everywhere, this
|
||||||
* is the file to change.
|
* is the file to change.
|
||||||
*/
|
*/
|
||||||
import { markdownToHtml, htmlToMarkdown } from '$lib/richtext.js';
|
import { markdownToHtml, htmlToMarkdown, blockText } from '$lib/richtext.js';
|
||||||
import { highlight, guessLanguage } from '$lib/highlight.js';
|
import { highlight, guessLanguage } from '$lib/highlight.js';
|
||||||
import { t } from '$lib/i18n.svelte.js';
|
import { t } from '$lib/i18n.svelte.js';
|
||||||
import { app } from '$lib/stores.svelte.js';
|
import { app } from '$lib/stores.svelte.js';
|
||||||
@@ -133,20 +133,78 @@
|
|||||||
// `<pre>…<p>body</p></pre>` — so the text stays monospace and there is no way
|
// `<pre>…<p>body</p></pre>` — so the text stays monospace and there is no way
|
||||||
// out of the block from the menu. Do any conversion that touches a <pre> by
|
// out of the block from the menu. Do any conversion that touches a <pre> by
|
||||||
// hand, replacing the element rather than reformatting inside it.
|
// hand, replacing the element rather than reformatting inside it.
|
||||||
const block = currentBlock();
|
//
|
||||||
if (block && (block.tagName === 'PRE' || style.tag === 'pre')) {
|
// Hele markeringen, ikke blokken markøren står i. Den her vej spurgte efter
|
||||||
replaceBlock(block, style.tag);
|
// `currentBlock()`, og det er ét blok — så en markering over ti linjers
|
||||||
|
// terminaludskrift gjorde den første linje til kode og lod resten stå. Det
|
||||||
|
// er den slags, man ikke opdager med to linjer og ikke kan overse med ti.
|
||||||
|
const blocks = selectedBlocks();
|
||||||
|
if (!blocks.length) return apply('formatBlock', style.tag);
|
||||||
|
|
||||||
|
if (style.tag === 'pre') {
|
||||||
|
mergeToMono(blocks);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ud af monotype igen: hver blok for sig, uden at røre markeringen —
|
||||||
|
// tekstknuderne flytter med over i de nye elementer, så den range, browseren
|
||||||
|
// holder på, stadig peger på noget, der er i dokumentet. Resten klarer
|
||||||
|
// formatBlock bagefter, nu hvor der ikke er en <pre> i vejen.
|
||||||
|
const pres = blocks.filter((b) => b.tagName === 'PRE');
|
||||||
|
for (const b of pres) swapTag(b, style.tag);
|
||||||
|
if (pres.length === blocks.length) {
|
||||||
|
readState();
|
||||||
|
changed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
apply('formatBlock', style.tag);
|
apply('formatBlock', style.tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces a block element with one of another tag, keeping its contents and the
|
* Alle de blokke, markeringen rører — ikke kun den, markøren står i.
|
||||||
* caret. Used where execCommand cannot be trusted — converting to or from a
|
*
|
||||||
* <pre>, which it either nests or ignores.
|
* En sammenfaldet markering rører præcis ét blok, så den enkle sag opfører sig
|
||||||
|
* som før.
|
||||||
*/
|
*/
|
||||||
function replaceBlock(block, tag) {
|
function selectedBlocks() {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (!selection || !selection.rangeCount || !editor) return [];
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
return [...editor.children].filter((b) => range.intersectsNode(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lægger flere blokke sammen til én monotypeblok.
|
||||||
|
*
|
||||||
|
* Én blok og ikke én pr. linje: en udskrift, man indsætter, er ét stykke kode.
|
||||||
|
* Tre blokke ved siden af hinanden ser næsten ens ud på skærmen og bliver til
|
||||||
|
* tre hegn i filen — og så kan de ikke rulles eller kopieres som det ene,
|
||||||
|
* de er.
|
||||||
|
*/
|
||||||
|
function mergeToMono(blocks) {
|
||||||
|
const pre = document.createElement('pre');
|
||||||
|
// blockText og ikke textContent: et <br> har den tomme streng som
|
||||||
|
// textContent, og det er netop <br>, der står for et linjeskift inde i en
|
||||||
|
// blok. Samme grund som da en kodeblok ikke kunne gemme mere end én linje.
|
||||||
|
pre.textContent = blocks.map((b) => blockText(b)).join('\n');
|
||||||
|
if (!pre.textContent) pre.appendChild(document.createElement('br'));
|
||||||
|
|
||||||
|
blocks[0].replaceWith(pre);
|
||||||
|
for (const b of blocks.slice(1)) b.remove();
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(pre);
|
||||||
|
range.collapse(false);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
|
||||||
|
readState();
|
||||||
|
changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bytter et bloks tag ud og flytter indholdet med, uden at røre markøren. */
|
||||||
|
function swapTag(block, tag) {
|
||||||
// The copy button lives inside the <pre> so it can sit in its corner; it is
|
// The copy button lives inside the <pre> so it can sit in its corner; it is
|
||||||
// not text and must not travel into a paragraph. addCopyButtons puts it back
|
// not text and must not travel into a paragraph. addCopyButtons puts it back
|
||||||
// on the next render for any <pre> that still needs one.
|
// on the next render for any <pre> that still needs one.
|
||||||
@@ -156,6 +214,16 @@
|
|||||||
while (block.firstChild) el.appendChild(block.firstChild);
|
while (block.firstChild) el.appendChild(block.firstChild);
|
||||||
if (!el.firstChild) el.appendChild(document.createElement('br'));
|
if (!el.firstChild) el.appendChild(document.createElement('br'));
|
||||||
block.replaceWith(el);
|
block.replaceWith(el);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces a block element with one of another tag, keeping its contents and the
|
||||||
|
* caret. Used where execCommand cannot be trusted — converting to or from a
|
||||||
|
* <pre>, which it either nests or ignores.
|
||||||
|
*/
|
||||||
|
function replaceBlock(block, tag) {
|
||||||
|
const el = swapTag(block, tag);
|
||||||
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
range.selectNodeContents(el);
|
range.selectNodeContents(el);
|
||||||
@@ -380,7 +448,7 @@
|
|||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const text = codeTextOf(pre);
|
const text = blockText(pre);
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text.replace(/\n$/, ''));
|
await navigator.clipboard.writeText(text.replace(/\n$/, ''));
|
||||||
btn.textContent = t('notes.copied');
|
btn.textContent = t('notes.copied');
|
||||||
@@ -394,19 +462,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Teksten i en kodeblok, uden knapper og andet, editoren har lagt oven på den.
|
* Teksten i en kodeblok kommer fra `blockText` i richtext.js — ÉT sted, hvilket
|
||||||
|
* den her kommentar påstod i forvejen, mens der stod en kopi lige her.
|
||||||
*
|
*
|
||||||
* Ét sted, fordi to steder skal bruge det samme svar: farvelægningen, som
|
* De to skal bruge det samme svar: farvelægningen, som skriver resultatet
|
||||||
* skriver resultatet tilbage i blokken, og oversættelsen til Markdown, som
|
* tilbage i blokken, og oversættelsen til Markdown, som skriver det i filen. De
|
||||||
* skriver det i filen. Var de to uenige, ville forskellen ende i noten.
|
* var uenige om ét tegn — kopien her læste `textContent`, og et <br> har den
|
||||||
|
* tomme streng som textContent. Så en blok, man havde skrevet med shift+retur,
|
||||||
|
* fik sine linjer skrevet sammen af farvelægningen, i det øjeblik man klikkede
|
||||||
|
* væk fra den. På skærmen, ikke kun i filen.
|
||||||
*/
|
*/
|
||||||
function codeTextOf(pre) {
|
|
||||||
const from = pre.querySelector('code') ?? pre;
|
|
||||||
return [...from.childNodes]
|
|
||||||
.filter((n) => n.nodeType !== Node.ELEMENT_NODE || !n.classList?.contains('copy'))
|
|
||||||
.map((n) => n.textContent ?? '')
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function colourCode() {
|
function colourCode() {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
@@ -419,7 +484,7 @@
|
|||||||
// maler resultatet tilbage ind i blokken — så et klik på "Kopiér" skrev
|
// maler resultatet tilbage ind i blokken — så et klik på "Kopiér" skrev
|
||||||
// "Kopieret" ind i koden, og den næste gemning lagde det i filen. Knappen
|
// "Kopieret" ind i koden, og den næste gemning lagde det i filen. Knappen
|
||||||
// spiste den kode, den var sat der for at kopiere.
|
// spiste den kode, den var sat der for at kopiere.
|
||||||
const code = codeTextOf(pre);
|
const code = blockText(pre);
|
||||||
const lang = pre.getAttribute('data-lang') || guessLanguage(code);
|
const lang = pre.getAttribute('data-lang') || guessLanguage(code);
|
||||||
if (lang) pre.setAttribute('data-lang', lang);
|
if (lang) pre.setAttribute('data-lang', lang);
|
||||||
const painted = highlight(code, lang);
|
const painted = highlight(code, lang);
|
||||||
@@ -788,6 +853,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Står markøren i en punktliste eller en nummereret liste?
|
||||||
|
*
|
||||||
|
* Spurgt gennem kommandotilstanden frem for ved at gå op gennem træet: det er
|
||||||
|
* den samme kilde, værktøjslinjen allerede markerer knappen efter, så de to kan
|
||||||
|
* ikke blive uenige om, hvad der er en liste.
|
||||||
|
*/
|
||||||
|
const inList = () =>
|
||||||
|
document.queryCommandState('insertUnorderedList') ||
|
||||||
|
document.queryCommandState('insertOrderedList');
|
||||||
|
|
||||||
function onkeydown(event) {
|
function onkeydown(event) {
|
||||||
// The suggestion list owns the arrows and return while it is open, the way
|
// The suggestion list owns the arrows and return while it is open, the way
|
||||||
// every other completion does. Escape closes it without choosing.
|
// every other completion does. Escape closes it without choosing.
|
||||||
@@ -816,6 +892,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tab rykker et punkt ind og ud, som i enhver anden liste. Kun i en liste:
|
||||||
|
// alle andre steder er Tab den tast, der flytter fokus videre, og en editor,
|
||||||
|
// der stjæler den, er en editor, man ikke kan komme ud af med tastaturet.
|
||||||
|
if (event.key === 'Tab' && inList()) {
|
||||||
|
event.preventDefault();
|
||||||
|
apply(event.shiftKey ? 'outdent' : 'indent');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === 'Enter' && !event.shiftKey) {
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
// A code block is the one place the browser keeps the caret inside on
|
// A code block is the one place the browser keeps the caret inside on
|
||||||
// Enter rather than starting a new block. Step out of it ourselves, or a
|
// Enter rather than starting a new block. Step out of it ourselves, or a
|
||||||
|
|||||||
+44
-8
@@ -314,6 +314,41 @@ const BLOCK_TAGS = /^(P|H[1-6]|UL|OL|LI|PRE|BLOCKQUOTE|DIV)$/;
|
|||||||
/** Whether an element holds blocks, in which case reading it as one line loses them. */
|
/** Whether an element holds blocks, in which case reading it as one line loses them. */
|
||||||
const holdsBlocks = (el) => [...el.children].some((c) => BLOCK_TAGS.test(c.tagName));
|
const holdsBlocks = (el) => [...el.children].some((c) => BLOCK_TAGS.test(c.tagName));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teksten i et blok, med linjeskiftene i behold.
|
||||||
|
*
|
||||||
|
* `textContent` er ikke nok, og det er ikke en detalje: et <br> har den tomme
|
||||||
|
* streng som textContent, og <br> er præcis det, browseren lægger ind, når man
|
||||||
|
* trykker shift+retur i en blok. Blokken blev derfor skrevet ned som én linje —
|
||||||
|
* "first linesecond line" — så man kunne skrive flere linjer og ikke gemme dem.
|
||||||
|
* Det er usynligt indtil en genindlæsning, for på skærmen står de rigtigt hele
|
||||||
|
* tiden.
|
||||||
|
*
|
||||||
|
* Kopiér-knappen springes over. Den ligger inde i <pre> for at kunne sidde i
|
||||||
|
* hjørnet af den, og ellers stod ordet "Kopiér" i hver eneste kodeblok efter hver
|
||||||
|
* gemning.
|
||||||
|
*
|
||||||
|
* Rekursivt, fordi indholdet ligger i et <code> inde i blokken: en filtrering over
|
||||||
|
* <pre>'ens egne børn ser kun det ene element og spørger det om en textContent,
|
||||||
|
* der allerede har tabt linjeskiftet.
|
||||||
|
*/
|
||||||
|
export function blockText(node) {
|
||||||
|
let out = '';
|
||||||
|
for (const n of node.childNodes) {
|
||||||
|
if (n.nodeType !== Node.ELEMENT_NODE) {
|
||||||
|
out += n.textContent ?? '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (n.classList?.contains('copy')) continue;
|
||||||
|
if (n.tagName === 'BR') {
|
||||||
|
out += '\n';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out += blockText(n);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
export function htmlToMarkdown(root) {
|
export function htmlToMarkdown(root) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
|
|
||||||
@@ -366,14 +401,7 @@ export function htmlToMarkdown(root) {
|
|||||||
// else and an indent does not.
|
// else and an indent does not.
|
||||||
const lang = child.getAttribute('data-lang') ?? '';
|
const lang = child.getAttribute('data-lang') ?? '';
|
||||||
emit('```' + lang);
|
emit('```' + lang);
|
||||||
// Knapper og andet, editoren har lagt oven på blokken, hører ikke til
|
const code = blockText(child);
|
||||||
// i filen. Kopiér-knappen ligger inde i <pre> for at kunne placeres i
|
|
||||||
// hjørnet af den, og `textContent` ville ellers skrive ordet "Kopiér"
|
|
||||||
// ind i hver eneste kodeblok, hver gang noten blev gemt.
|
|
||||||
const code = [...child.childNodes]
|
|
||||||
.filter((n) => n.nodeType !== Node.ELEMENT_NODE || !n.classList?.contains('copy'))
|
|
||||||
.map((n) => n.textContent ?? '')
|
|
||||||
.join('');
|
|
||||||
for (const l of code.replace(/\n$/, '').split('\n')) {
|
for (const l of code.replace(/\n$/, '').split('\n')) {
|
||||||
emit(l);
|
emit(l);
|
||||||
}
|
}
|
||||||
@@ -408,6 +436,14 @@ export function htmlToMarkdown(root) {
|
|||||||
const pad = ' '.repeat(depth);
|
const pad = ' '.repeat(depth);
|
||||||
|
|
||||||
for (const li of el.children) {
|
for (const li of el.children) {
|
||||||
|
// En underliste, der ligger som SØSKENDE til punktet frem for inde i det.
|
||||||
|
// Browserne er uenige om, hvilken af de to former en indrykning laver, og
|
||||||
|
// de to ser ens ud på skærmen — men den her løkke sprang søskendeformen
|
||||||
|
// over uden lyd, så et indrykket punkt ville forsvinde ved gemningen.
|
||||||
|
if (li.tagName === 'UL' || li.tagName === 'OL') {
|
||||||
|
list(li, depth + 1, emit);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (li.tagName !== 'LI') continue;
|
if (li.tagName !== 'LI') continue;
|
||||||
|
|
||||||
// Punktets egen tekst er alt undtagen de lister, der ligger under det.
|
// Punktets egen tekst er alt undtagen de lister, der ligger under det.
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
import { api, humanMessage } from '$lib/api.js';
|
import { api, humanMessage } from '$lib/api.js';
|
||||||
import { app } from '$lib/stores.svelte.js';
|
import { app } from '$lib/stores.svelte.js';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { untrack } from 'svelte';
|
import { onMount, untrack } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { afterNavigate, goto } from '$app/navigation';
|
||||||
import { t, tag } from '$lib/i18n.svelte.js';
|
import { t, tag } from '$lib/i18n.svelte.js';
|
||||||
import { NOTE, startDrag } from '$lib/dnd.js';
|
import { NOTE, startDrag } from '$lib/dnd.js';
|
||||||
import { colorVar } from '$lib/colors.js';
|
import { colorVar } from '$lib/colors.js';
|
||||||
@@ -511,15 +511,43 @@
|
|||||||
query = inURL;
|
query = inURL;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Arriving from somewhere that names a note — a task's panel, a project's page —
|
/**
|
||||||
// opens it rather than dropping the person on a list to find it again.
|
* En note, man kommer MED — en besked fra klokken, et link fra en opgave, en
|
||||||
let asked = $derived($page.url.searchParams.get('note'));
|
* træffer i ⌘K. Den åbnes, så man ikke bliver sat af på en liste og skal finde
|
||||||
$effect(() => {
|
* den igen.
|
||||||
if (!asked || selectedId === asked) return;
|
*
|
||||||
const found = notes.find((n) => n.id === asked);
|
* Bundet til selve navigationen og ikke til adressens indhold, og det er hele
|
||||||
|
* pointen. Siden skriver aldrig adressen — det blev fjernet, fordi det kostede
|
||||||
|
* bogstaver ud af søgefeltet — så `?note=` bliver stående på den, man kom med,
|
||||||
|
* uanset hvad man klikker bagefter. En effekt, der læste den igen, hver gang
|
||||||
|
* noget ændrede sig, så derfor `selectedId` uenig med adressen og åbnede den
|
||||||
|
* gamle igen: man kunne søge og ikke vælge, for hvert klik på en anden række
|
||||||
|
* sprang tilbage til den første.
|
||||||
|
*
|
||||||
|
* En adresse, der ikke følger med, er ikke en tilstand at holde sig enig med.
|
||||||
|
* Den er en besked, der kommer én gang — så den læses ved ankomsten frem for
|
||||||
|
* ved hver ændring.
|
||||||
|
*/
|
||||||
|
function openFromURL(url) {
|
||||||
|
const id = url?.searchParams.get('note');
|
||||||
|
if (!id || id === selectedId) return;
|
||||||
|
const found = notes.find((n) => n.id === id);
|
||||||
if (found) open(found);
|
if (found) open(found);
|
||||||
else api.note(asked).then(open).catch(() => {});
|
else api.note(id).then(open).catch(() => {});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// To veje ind, fordi der er to måder at ankomme på, og hver af dem er blind for
|
||||||
|
// den anden.
|
||||||
|
//
|
||||||
|
// `afterNavigate` alene rækker ikke: layoutet tegner ingenting, mens kontoen
|
||||||
|
// hentes, så den her side bliver monteret EFTER den første navigation er forbi.
|
||||||
|
// En lytter, der registreres der, hører den aldrig — `/noter?note=<id>` tastet
|
||||||
|
// eller fulgt udefra åbnede så ingenting.
|
||||||
|
//
|
||||||
|
// Og `onMount` alene rækker heller ikke: går man til en anden note inde i
|
||||||
|
// programmet, monteres siden ikke om.
|
||||||
|
onMount(() => openFromURL($page.url));
|
||||||
|
afterNavigate((nav) => openFromURL(nav?.to?.url ?? $page.url));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hvilken hentning der gælder.
|
* Hvilken hentning der gælder.
|
||||||
|
|||||||
Reference in New Issue
Block a user