Merge pull request #92 from kristianwind/monotype-over-hele-markeringen
fix(noter): monotype tager hele markeringen, og farvelægningen taber ingen linjer
This commit is contained in:
@@ -4992,6 +4992,108 @@ test('en punktliste kan have niveauer, der overlever gemningen', async ({ page }
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* unforgivable. When the replacement (the EditContext API) is everywhere, this
|
||||
* 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 { t } from '$lib/i18n.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
|
||||
// out of the block from the menu. Do any conversion that touches a <pre> by
|
||||
// hand, replacing the element rather than reformatting inside it.
|
||||
const block = currentBlock();
|
||||
if (block && (block.tagName === 'PRE' || style.tag === 'pre')) {
|
||||
replaceBlock(block, style.tag);
|
||||
//
|
||||
// Hele markeringen, ikke blokken markøren står i. Den her vej spurgte efter
|
||||
// `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;
|
||||
}
|
||||
apply('formatBlock', style.tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Alle de blokke, markeringen rører — ikke kun den, markøren står i.
|
||||
*
|
||||
* 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
|
||||
// not text and must not travel into a paragraph. addCopyButtons puts it back
|
||||
// on the next render for any <pre> that still needs one.
|
||||
@@ -156,6 +214,16 @@
|
||||
while (block.firstChild) el.appendChild(block.firstChild);
|
||||
if (!el.firstChild) el.appendChild(document.createElement('br'));
|
||||
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();
|
||||
range.selectNodeContents(el);
|
||||
@@ -380,7 +448,7 @@
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const text = codeTextOf(pre);
|
||||
const text = blockText(pre);
|
||||
try {
|
||||
await navigator.clipboard.writeText(text.replace(/\n$/, ''));
|
||||
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
|
||||
* skriver resultatet tilbage i blokken, og oversættelsen til Markdown, som
|
||||
* skriver det i filen. Var de to uenige, ville forskellen ende i noten.
|
||||
* De to skal bruge det samme svar: farvelægningen, som skriver resultatet
|
||||
* tilbage i blokken, og oversættelsen til Markdown, som skriver det i filen. De
|
||||
* 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() {
|
||||
if (!editor) return;
|
||||
@@ -419,7 +484,7 @@
|
||||
// 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
|
||||
// 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);
|
||||
if (lang) pre.setAttribute('data-lang', lang);
|
||||
const painted = highlight(code, lang);
|
||||
|
||||
@@ -315,7 +315,7 @@ const BLOCK_TAGS = /^(P|H[1-6]|UL|OL|LI|PRE|BLOCKQUOTE|DIV)$/;
|
||||
const holdsBlocks = (el) => [...el.children].some((c) => BLOCK_TAGS.test(c.tagName));
|
||||
|
||||
/**
|
||||
* Teksten i en kodeblok, med linjeskiftene i behold.
|
||||
* 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
|
||||
@@ -332,7 +332,7 @@ const holdsBlocks = (el) => [...el.children].some((c) => BLOCK_TAGS.test(c.tagNa
|
||||
* <pre>'ens egne børn ser kun det ene element og spørger det om en textContent,
|
||||
* der allerede har tabt linjeskiftet.
|
||||
*/
|
||||
function codeText(node) {
|
||||
export function blockText(node) {
|
||||
let out = '';
|
||||
for (const n of node.childNodes) {
|
||||
if (n.nodeType !== Node.ELEMENT_NODE) {
|
||||
@@ -344,7 +344,7 @@ function codeText(node) {
|
||||
out += '\n';
|
||||
continue;
|
||||
}
|
||||
out += codeText(n);
|
||||
out += blockText(n);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -401,7 +401,7 @@ export function htmlToMarkdown(root) {
|
||||
// else and an indent does not.
|
||||
const lang = child.getAttribute('data-lang') ?? '';
|
||||
emit('```' + lang);
|
||||
const code = codeText(child);
|
||||
const code = blockText(child);
|
||||
for (const l of code.replace(/\n$/, '').split('\n')) {
|
||||
emit(l);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user