feat(workspace): add formatting shortcuts for bold, italic, and links in outliner
This commit is contained in:
1 parent
1d6863824b
commit
a317ce0fee
4 files changed
+285
No files matched your search
@@ -255,3 +255,123 @@ describe('slash command menu and outliner fixes', () => {
|
||||
expect(updates[updates.length - 1]?.content).toBe('Siehe [Doku](https://example.com/doku)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatting shortcuts (Teil C)', () => {
|
||||
it('toggles bold with and without selection (Ctrl+B and Cmd+B)', () => {
|
||||
const block = { id: 'b1', type: 'bullet', content: 'hallo welt', level: 0 } as DocumentBlock;
|
||||
const { container } = renderBlock(block);
|
||||
const ta = container.querySelector('textarea') as HTMLTextAreaElement;
|
||||
ta.focus();
|
||||
|
||||
// 1. With selection: wraps in **
|
||||
ta.setSelectionRange(6, 10); // "welt"
|
||||
const event1 = new KeyboardEvent('keydown', { key: 'b', ctrlKey: true, bubbles: true, cancelable: true });
|
||||
ta.dispatchEvent(event1);
|
||||
expect(event1.defaultPrevented).toBe(true);
|
||||
expect(ta.value).toBe('hallo **welt**');
|
||||
expect(ta.selectionStart).toBe(8);
|
||||
expect(ta.selectionEnd).toBe(12);
|
||||
|
||||
// 2. With selection where delimiters are directly outside: unwraps **
|
||||
const event2 = new KeyboardEvent('keydown', { key: 'b', metaKey: true, bubbles: true, cancelable: true });
|
||||
ta.dispatchEvent(event2);
|
||||
expect(event2.defaultPrevented).toBe(true);
|
||||
expect(ta.value).toBe('hallo welt');
|
||||
expect(ta.selectionStart).toBe(6);
|
||||
expect(ta.selectionEnd).toBe(10);
|
||||
|
||||
// 3. With selection including delimiters: unwraps **
|
||||
ta.value = 'hallo **welt**';
|
||||
ta.setSelectionRange(6, 14); // "**welt**"
|
||||
const event3 = new KeyboardEvent('keydown', { key: 'b', ctrlKey: true, bubbles: true, cancelable: true });
|
||||
ta.dispatchEvent(event3);
|
||||
expect(ta.value).toBe('hallo welt');
|
||||
expect(ta.selectionStart).toBe(6);
|
||||
expect(ta.selectionEnd).toBe(10);
|
||||
|
||||
// 4. Without selection: inserts **** and places cursor between
|
||||
ta.value = 'start';
|
||||
ta.setSelectionRange(5, 5);
|
||||
const event4 = new KeyboardEvent('keydown', { key: 'b', ctrlKey: true, bubbles: true, cancelable: true });
|
||||
ta.dispatchEvent(event4);
|
||||
expect(ta.value).toBe('start****');
|
||||
expect(ta.selectionStart).toBe(7);
|
||||
expect(ta.selectionEnd).toBe(7);
|
||||
});
|
||||
|
||||
it('toggles italic with and without selection (Ctrl+I)', () => {
|
||||
const block = { id: 'b1', type: 'bullet', content: 'hallo welt', level: 0 } as DocumentBlock;
|
||||
const { container } = renderBlock(block);
|
||||
const ta = container.querySelector('textarea') as HTMLTextAreaElement;
|
||||
ta.focus();
|
||||
|
||||
// 1. With selection: wraps in *
|
||||
ta.setSelectionRange(6, 10); // "welt"
|
||||
fireEvent.keyDown(ta, { key: 'i', ctrlKey: true });
|
||||
expect(ta.value).toBe('hallo *welt*');
|
||||
expect(ta.selectionStart).toBe(7);
|
||||
expect(ta.selectionEnd).toBe(11);
|
||||
|
||||
// 2. Toggles off when already italic
|
||||
fireEvent.keyDown(ta, { key: 'i', ctrlKey: true });
|
||||
expect(ta.value).toBe('hallo welt');
|
||||
expect(ta.selectionStart).toBe(6);
|
||||
expect(ta.selectionEnd).toBe(10);
|
||||
|
||||
// 3. Without selection: inserts ** and places cursor between
|
||||
fireEvent.change(ta, { target: { value: 'a' } });
|
||||
ta.setSelectionRange(1, 1);
|
||||
fireEvent.keyDown(ta, { key: 'i', ctrlKey: true });
|
||||
expect(ta.value).toBe('a**');
|
||||
expect(ta.selectionStart).toBe(2);
|
||||
expect(ta.selectionEnd).toBe(2);
|
||||
});
|
||||
|
||||
it('handles Ctrl+K: with selection wraps in [Text](…) and selects placeholder, without selection opens modal', () => {
|
||||
const block = { id: 'b1', type: 'bullet', content: 'besuche mindsift heute', level: 0 } as DocumentBlock;
|
||||
const { container, queryByText } = renderBlock(block);
|
||||
const ta = container.querySelector('textarea') as HTMLTextAreaElement;
|
||||
ta.focus();
|
||||
|
||||
// 1. With selection
|
||||
ta.setSelectionRange(8, 16); // "mindsift"
|
||||
const eventWithSel = new KeyboardEvent('keydown', { key: 'k', ctrlKey: true, bubbles: true, cancelable: true });
|
||||
ta.dispatchEvent(eventWithSel);
|
||||
expect(eventWithSel.defaultPrevented).toBe(true);
|
||||
expect(ta.value).toBe('besuche [mindsift](…) heute');
|
||||
// Placeholder "…" is selected
|
||||
expect(ta.selectionStart).toBe(19);
|
||||
expect(ta.selectionEnd).toBe(20);
|
||||
expect(ta.value.slice(ta.selectionStart, ta.selectionEnd)).toBe('…');
|
||||
|
||||
// 2. Without selection: opens LinkModal
|
||||
ta.setSelectionRange(0, 0);
|
||||
expect(queryByText('Link einfügen')).toBeNull();
|
||||
fireEvent.keyDown(ta, { key: 'k', ctrlKey: true });
|
||||
expect(queryByText('Link einfügen')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('does nothing in code blocks or for read-only readers', () => {
|
||||
// In code block
|
||||
const codeBlock = { id: 'c1', type: 'code', content: 'const x = 1;', level: 0 } as DocumentBlock;
|
||||
const codeView = renderBlock(codeBlock);
|
||||
const codeTa = codeView.container.querySelector('textarea') as HTMLTextAreaElement;
|
||||
codeTa.focus();
|
||||
codeTa.setSelectionRange(6, 7); // "x"
|
||||
const codeEvent = new KeyboardEvent('keydown', { key: 'b', ctrlKey: true, bubbles: true, cancelable: true });
|
||||
codeTa.dispatchEvent(codeEvent);
|
||||
expect(codeEvent.defaultPrevented).toBe(false);
|
||||
expect(codeTa.value).toBe('const x = 1;');
|
||||
|
||||
// For readers (!canEdit)
|
||||
const bulletBlock = { id: 'b1', type: 'bullet', content: 'text', level: 0 } as DocumentBlock;
|
||||
const readerView = renderBlock(bulletBlock, { canEdit: false });
|
||||
const readerTa = readerView.container.querySelector('textarea') as HTMLTextAreaElement;
|
||||
readerTa.focus();
|
||||
readerTa.setSelectionRange(0, 4);
|
||||
const readerEvent = new KeyboardEvent('keydown', { key: 'b', ctrlKey: true, bubbles: true, cancelable: true });
|
||||
readerTa.dispatchEvent(readerEvent);
|
||||
expect(readerEvent.defaultPrevented).toBe(false);
|
||||
expect(readerTa.value).toBe('text');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user