Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d67b61d3c6 | ||
|
|
f0d18bb800 | ||
|
|
7f8d925299 |
@@ -3049,6 +3049,68 @@ test('en note kan trækkes hen på et projekt i sidebjælken', async ({ page })
|
|||||||
expect(trouble).toEqual([]);
|
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 }) => {
|
test('kodeblokken har en kopiér-knap, og knappen havner ikke i filen', async ({ page }) => {
|
||||||
const trouble = watchForTrouble(page);
|
const trouble = watchForTrouble(page);
|
||||||
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
|
await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||||
|
|||||||
@@ -120,11 +120,99 @@
|
|||||||
|
|
||||||
function setStyle(style) {
|
function setStyle(style) {
|
||||||
stylesOpen = false;
|
stylesOpen = false;
|
||||||
|
editor?.focus();
|
||||||
if (style.key === 'bullet') return apply('insertUnorderedList');
|
if (style.key === 'bullet') return apply('insertUnorderedList');
|
||||||
if (style.key === 'numbered') return apply('insertOrderedList');
|
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);
|
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. */
|
/** What the caret is standing in, for the toolbar's pressed states. */
|
||||||
function readState() {
|
function readState() {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
@@ -470,6 +558,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
// 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.
|
// After the split, not before it.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
keepTitleFirst();
|
keepTitleFirst();
|
||||||
|
|||||||
Reference in New Issue
Block a user