feat: decouple person-mention display text from person name (#372) #373

Merged
marcel merged 17 commits from feat/person-mentions-issue-362-frontend-b2 into main 2026-04-29 16:55:53 +02:00
2 changed files with 27 additions and 48 deletions
Showing only changes of commit 7a25feb04e - Show all commits

View File

@@ -79,17 +79,6 @@ let leftBorderClass = $derived(
saveState === 'error' ? 'border-l-2 border-error' : active ? 'border-l-2 border-turquoise' : '' saveState === 'error' ? 'border-l-2 border-error' : active ? 'border-l-2 border-turquoise' : ''
); );
// Single source of truth for the editor's textarea — stored on attach so
// we can read selection bounds for quote selection without re-querying the DOM.
let textareaEl: HTMLTextAreaElement | null = null;
function captureTextarea(node: HTMLTextAreaElement) {
textareaEl = node;
return () => {
textareaEl = null;
};
}
function emitChange() { function emitChange() {
onTextChange(localText, localMentions); onTextChange(localText, localMentions);
} }
@@ -101,17 +90,6 @@ async function handleDelete() {
}); });
if (confirmed) onDeleteClick(); if (confirmed) onDeleteClick();
} }
function handleTextareaMouseUp() {
if (!textareaEl) return;
const start = textareaEl.selectionStart;
const end = textareaEl.selectionEnd;
if (start !== end) {
selectedQuote = localText.substring(start, end);
} else {
selectedQuote = null;
}
}
</script> </script>
<div <div
@@ -176,24 +154,22 @@ function handleTextareaMouseUp() {
{/if} {/if}
</div> </div>
<!-- Textarea (now powered by PersonMentionEditor for @-mention typeahead) --> <!-- Editor powered by PersonMentionEditor (Tiptap) for @-mention typeahead -->
<div onmouseup={handleTextareaMouseUp} role="presentation"> <PersonMentionEditor
<PersonMentionEditor bind:value={() => localText,
bind:value={() => localText, (v) => {
(v) => { localText = v;
localText = v; emitChange();
emitChange(); }}
}} bind:mentionedPersons={() => localMentions,
bind:mentionedPersons={() => localMentions, (next) => {
(next) => { localMentions = next;
localMentions = next; emitChange();
emitChange(); }}
}} placeholder={m.transcription_block_placeholder()}
placeholder={m.transcription_block_placeholder()} onfocus={onFocus}
onfocus={onFocus} onSelectionChange={(text) => (selectedQuote = text)}
captureTextarea={captureTextarea} />
/>
</div>
{#if selectedQuote} {#if selectedQuote}
<p class="mt-1 text-xs text-ink-3">{m.transcription_block_quote_hint()}</p> <p class="mt-1 text-xs text-ink-3">{m.transcription_block_quote_hint()}</p>

View File

@@ -41,8 +41,7 @@ describe('TranscriptionBlock — rendering', () => {
it('renders text in textarea', async () => { it('renders text in textarea', async () => {
renderBlock(); renderBlock();
const textarea = page.getByRole('textbox'); await expect.element(page.getByText('Liebe Mutter,')).toBeInTheDocument();
await expect.element(textarea).toHaveValue('Liebe Mutter,');
}); });
it('renders optional label when provided', async () => { it('renders optional label when provided', async () => {
@@ -226,14 +225,18 @@ describe('TranscriptionBlock — delete confirmation', () => {
// ─── Quote selection ───────────────────────────────────────────────────────── // ─── Quote selection ─────────────────────────────────────────────────────────
describe('TranscriptionBlock — quote selection', () => { describe('TranscriptionBlock — quote selection', () => {
it('shows quote hint after text is selected in textarea', async () => { it('shows quote hint after text is selected in the editor', async () => {
renderBlock({ text: 'Breslau, den 12. August' }); renderBlock({ text: 'Breslau, den 12. August' });
await page.getByRole('textbox').click(); await page.getByRole('textbox').click();
// Select text and fire mouseup via native DOM — locator.selectText/dispatchEvent not available // Select all text in the contenteditable via the native Selection API.
const el = document.querySelector('textarea') as HTMLTextAreaElement; // Tiptap fires selectionUpdate which the block forwards as onSelectionChange.
el.focus(); const editorEl = document.querySelector('[role="textbox"]') as HTMLElement;
el.setSelectionRange(0, el.value.length); const range = document.createRange();
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); range.selectNodeContents(editorEl);
const selection = window.getSelection()!;
selection.removeAllRanges();
selection.addRange(range);
editorEl.dispatchEvent(new Event('input', { bubbles: true }));
await expect.element(page.getByText(/Zitat/)).toBeInTheDocument(); await expect.element(page.getByText(/Zitat/)).toBeInTheDocument();
}); });
}); });