Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0d18bb800 |
@@ -3049,6 +3049,68 @@ test('en note kan trækkes hen på et projekt i sidebjælken', async ({ page })
|
||||
expect(trouble).toEqual([]);
|
||||
});
|
||||
|
||||
test('en monospace-linje kan forlades — på retur og gennem menuen', async ({ page }) => {
|
||||
const trouble = watchForTrouble(page);
|
||||
await page.goto('/noter');
|
||||
|
||||
await page.getByRole('button', { name: 'Ny note' }).click();
|
||||
const ed = page.getByRole('textbox', { name: 'Notens tekst' });
|
||||
await ed.click();
|
||||
await page.keyboard.type('Titel');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// En monospace-linje, og så retur: linjen efter skal være brødtekst, ikke mere
|
||||
// monospace. Før rettelsen holdt en <pre> på markøren, og alt derefter blev ved
|
||||
// med at være kode.
|
||||
await page.getByRole('button', { name: 'Formatér' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Monotype' }).click();
|
||||
await page.keyboard.type('kode linje');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.type('almindelig tekst');
|
||||
|
||||
// Koden står i sin blok; teksten efter står uden for den.
|
||||
await expect(ed.locator('pre')).toHaveText('kode linje');
|
||||
await expect(ed.locator('p').filter({ hasText: 'almindelig tekst' })).toBeVisible();
|
||||
await expect(ed.locator('pre').filter({ hasText: 'almindelig tekst' })).toHaveCount(0);
|
||||
|
||||
// Shift+retur bliver derimod i blokken, så flerlinjet kode stadig kan skrives.
|
||||
await page.getByRole('button', { name: 'Formatér' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Monotype' }).click();
|
||||
await page.keyboard.type('linje et');
|
||||
await page.keyboard.press('Shift+Enter');
|
||||
await page.keyboard.type('linje to');
|
||||
await expect(ed.locator('pre').filter({ hasText: 'linje et' })).toContainText('linje to');
|
||||
|
||||
// Og menuen kan føre en monospace-linje tilbage til brødtekst — dét, der før
|
||||
// lagde et <p> inde i <pre> og ikke kom nogen vegne.
|
||||
await page.getByRole('button', { name: 'Formatér' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Brødtekst' }).click();
|
||||
// Den blok er nu brødtekst, ikke kode: den står som <p>, ikke i en <pre>.
|
||||
await expect(ed.locator('p').filter({ hasText: 'linje et' })).toBeVisible();
|
||||
await expect(ed.locator('pre').filter({ hasText: 'linje et' })).toHaveCount(0);
|
||||
|
||||
// Det overlever turen gennem Markdown. En ren note for sig, så prøven ikke
|
||||
// afhænger af alt det ovenfor: kode, retur, brødtekst — gem, hent igen.
|
||||
await page.getByRole('button', { name: 'Ny note' }).click();
|
||||
await ed.click();
|
||||
await page.keyboard.type('Rundtur');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.getByRole('button', { name: 'Formatér' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Monotype' }).click();
|
||||
await page.keyboard.type('docker pull');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.type('og så videre i almindelig skrift');
|
||||
await ed.blur();
|
||||
await page.waitForTimeout(1200);
|
||||
await page.reload();
|
||||
await page.getByRole('button', { name: /Rundtur/ }).click();
|
||||
await expect(ed.locator('pre')).toContainText('docker pull');
|
||||
await expect(ed.locator('p').filter({ hasText: 'almindelig skrift' })).toBeVisible();
|
||||
await expect(ed.locator('pre').filter({ hasText: 'almindelig skrift' })).toHaveCount(0);
|
||||
|
||||
expect(trouble).toEqual([]);
|
||||
});
|
||||
|
||||
test('kodeblokken har en kopiér-knap, og knappen havner ikke i filen', async ({ page }) => {
|
||||
const trouble = watchForTrouble(page);
|
||||
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||
|
||||
@@ -120,11 +120,99 @@
|
||||
|
||||
function setStyle(style) {
|
||||
stylesOpen = false;
|
||||
editor?.focus();
|
||||
if (style.key === 'bullet') return apply('insertUnorderedList');
|
||||
if (style.key === 'numbered') return apply('insertOrderedList');
|
||||
|
||||
// formatBlock is unreliable around <pre>. Asked to turn a monospace line back
|
||||
// into body, Chromium and WebKit nest a <p> *inside* the <pre> —
|
||||
// `<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);
|
||||
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.
|
||||
*/
|
||||
function replaceBlock(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.
|
||||
block.querySelectorAll?.('.copy').forEach((b) => b.remove());
|
||||
|
||||
const el = document.createElement(tag);
|
||||
while (block.firstChild) el.appendChild(block.firstChild);
|
||||
if (!el.firstChild) el.appendChild(document.createElement('br'));
|
||||
block.replaceWith(el);
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
range.collapse(false);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
readState();
|
||||
changed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps out of a code block on Enter.
|
||||
*
|
||||
* A <pre> is preformatted, so the browser answers Enter by inserting a <br> and
|
||||
* staying inside it — which means a monospace last line makes every line after
|
||||
* it monospace too, with no way back to body by typing. Plain Enter now leaves
|
||||
* the block: the tail after the caret becomes a paragraph below the <pre>, and
|
||||
* that is where the caret lands. Shift+Enter is left untouched, so a soft line
|
||||
* break still writes multi-line code inside the block.
|
||||
*/
|
||||
function exitPre(pre) {
|
||||
pre.querySelectorAll('.copy').forEach((b) => b.remove());
|
||||
|
||||
const selection = window.getSelection();
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
// Everything from the caret to the end of the block leaves with the new line.
|
||||
const tail = document.createRange();
|
||||
tail.setStart(range.endContainer, range.endOffset);
|
||||
tail.setEnd(pre, pre.childNodes.length);
|
||||
const frag = tail.extractContents();
|
||||
|
||||
// A break at the head of what we carried is the empty line return was pressed
|
||||
// on; it should not open the paragraph with a blank row.
|
||||
while (frag.firstChild && frag.firstChild.nodeName === 'BR') frag.removeChild(frag.firstChild);
|
||||
|
||||
const p = document.createElement('p');
|
||||
if (frag.childNodes.length) p.appendChild(frag);
|
||||
// The split can leave an empty text node behind, and a block with no element
|
||||
// and no text is not a caret host in Chromium — the next keystroke falls back
|
||||
// into the block above. An empty paragraph needs a <br> to be stood in.
|
||||
if (!p.textContent && !p.querySelector('br, img')) {
|
||||
p.replaceChildren(document.createElement('br'));
|
||||
}
|
||||
|
||||
// And the same break seen from the other side, left dangling in the <pre>.
|
||||
while (pre.lastChild && pre.lastChild.nodeName === 'BR') pre.removeChild(pre.lastChild);
|
||||
|
||||
// Leaving an empty block behind is leaving nothing: the <pre> itself goes.
|
||||
if (!pre.textContent.trim()) pre.replaceWith(p);
|
||||
else pre.after(p);
|
||||
|
||||
const caret = document.createRange();
|
||||
caret.setStart(p, 0);
|
||||
caret.collapse(true);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(caret);
|
||||
}
|
||||
|
||||
/** What the caret is standing in, for the toolbar's pressed states. */
|
||||
function readState() {
|
||||
if (!editor) return;
|
||||
@@ -470,6 +558,16 @@
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
// 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
|
||||
// monospace last line traps everything typed after it in monospace.
|
||||
const block = currentBlock();
|
||||
if (block && block.tagName === 'PRE') {
|
||||
event.preventDefault();
|
||||
exitPre(block);
|
||||
changed();
|
||||
return;
|
||||
}
|
||||
// After the split, not before it.
|
||||
setTimeout(() => {
|
||||
keepTitleFirst();
|
||||
|
||||
Reference in New Issue
Block a user