feat: decouple person-mention display text from person name (#372) #373
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user