onDragOverBlock?.(e, index)}
onDrop={() => onDropBlock?.(index)}
onClick={(e) => {
if ((e.target as HTMLElement).closest('button, a, input, select')) return;
if (!activeFocus && canFocus && textareaRef.current) {
textareaRef.current.focus();
const len = textareaRef.current.value.length;
textareaRef.current.setSelectionRange(len, len);
}
}}
>
{/* Bullet Controls: Collapse Toggle & Bullet (Drag-Handle & Zoom) / Checkbox */}
{hasChildren ? (
) : (
)}
{/* Bullet handle for dragging and zoom focus */}
{/* If To-Do block: checkbox alongside bullet */}
{block.type === 'todo' && (
)}
{/* Main Block Content */}
{previewUrl && (
{
const content = hidePreviewContent(localContent, previewUrl, title);
setLocalContent(content);
localContentRef.current = content;
flushDebounce(content);
}}
/>
)}
{mediaPicker === 'image' ? (
{
setMediaPicker(null);
const content = mediaContent('image', media.id);
setLocalContent(content);
localContentRef.current = content;
flushDebounce(content, { type: 'image' });
}}
onCancel={() => {
setMediaPicker(null);
requestAnimationFrame(() => textareaRef.current?.focus());
}}
/>
) : mediaPicker === 'dictation' ? (
{
setMediaPicker(null);
const b = dictationBeforeRef.current ?? '';
const a = dictationAfterRef.current ?? '';
const spaceBefore = b && !b.endsWith(' ') ? ' ' : '';
const spaceAfter = a && !a.startsWith(' ') && a.trim() !== '' ? ' ' : '';
const newContent = `${b}${spaceBefore}${text}${spaceAfter}${a}`.trim();
setLocalContent(newContent);
localContentRef.current = newContent;
flushDebounce(newContent);
requestAnimationFrame(() => {
const ta = textareaRef.current;
if (!ta) return;
ta.focus();
const pos = (b + spaceBefore + text).length;
ta.setSelectionRange(pos, pos);
});
}}
onCancel={() => {
setMediaPicker(null);
requestAnimationFrame(() => textareaRef.current?.focus());
}}
/>
) : mediaPicker === 'audio' ? (
{
setMediaPicker(null);
const content = mediaContent('audio', media.id);
setLocalContent(content);
localContentRef.current = content;
flushDebounce(content, { type: 'audio' });
}}
onCancel={() => {
setMediaPicker(null);
requestAnimationFrame(() => textareaRef.current?.focus());
}}
/>
) : block.type === 'audio' ? (
{
const ref = parseMediaContent(localContent);
if (!ref) return;
const content = mediaContent('audio', ref.id, caption);
setLocalContent(content);
localContentRef.current = content;
flushDebounce(content);
}}
/>
) : block.type === 'image' ? (
{
const ref = parseMediaContent(localContent);
if (!ref) return;
const content = mediaContent('image', ref.id, caption);
setLocalContent(content);
localContentRef.current = content;
flushDebounce(content);
}}
/>
) : block.type === 'divider' ? (
{
textareaRef.current?.focus();
}}
>
) : block.type === 'code' ? (
{
setLocalContent(newContent);
localContentRef.current = newContent;
flushDebounce(newContent);
}}
onChangeLang={(lang) => flushDebounce(localContent, { lang })}
onConvertToText={() => flushDebounce(localContent, { type: 'bullet' })}
handleFocus={handleFocus}
handleBlur={handleBlur}
handleContentChange={handleContentChange}
handleKeyDown={handleKeyDown}
/>
) : block.type === 'callout' ? (
) : (
{/* View Layer with Inline Markdown */}
{
if (!activeFocus && canFocus && textareaRef.current) {
// Keep the row's click handler from moving the caret back to the end
e.stopPropagation();
focusAtPoint(e.clientX, e.clientY);
}
}}
className={`w-full min-h-[1.5rem] leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] ${
activeFocus ? 'absolute inset-0 pointer-events-none' : 'relative cursor-text'
} ${
block.type === 'h1'
? 'text-xl sm:text-2xl font-bold text-main tracking-tight'
: block.type === 'h2'
? 'text-lg sm:text-xl font-semibold text-main'
: block.type === 'h3'
? 'text-base font-semibold text-main'
: block.type === 'todo' && block.checked
? 'text-sm text-faint line-through'
: 'text-sm text-main'
}`}
>
{/* When actively focused, ghost text is invisible so textarea renders text/caret directly */}
{localContent ? renderInlineMarkdown(localContent, { onTagClick, onWikiLinkClick }) : '\u200b'}
)}
{/* If To-Do is an enhanced task, render badge stably outside text layer */}
{block.type === 'todo' && block.taskId && (
)}
{/* Conflict copy: text that lost against someone else's change */}
{block.conflictOf && (
{block.conflictOf === 'title' ? 'Konflikt beim Titel' : 'Konflikt'}
{block.conflictAuthor ? ` – Version von ${block.conflictAuthor}` : ''}
{onResolveConflict && (
<>
>
)}
)}
{/* Rich Tokens Bar (Wiki-Links & #Tags) if present */}
{hasSpecialTokens && !isFocused && (
{tokens.map((part, idx) => {
if (part.startsWith('[[')) return null;
const mdMatch = part.match(/^\[(.*?)\]\((https?:\/\/.*?)\)$/);
if (mdMatch && safeHref(mdMatch[2])) {
const label = mdMatch[1];
const url = safeHref(mdMatch[2]) as string;
return (
{label || url}
);
}
// The preview card above already shows this address
if (/^https?:\/\//.test(part) && safeHref(part) && part !== previewUrl) {
return (
{part}
);
}
return null;
})}
)}
{/* Floating Slash Commands Menu */}
{slashSearch !== null && canEdit && (
Befehl auswählen
{filteredSlashCommands.map((cmd, i) => (
))}
{filteredSlashCommands.length === 0 && (
Kein Befehl gefunden
)}
)}
{/* Someone else is editing this block */}
{lockedBy && (
{lockedBy}
)}
{/* Floating Actions Overlay Bubble (Zoom, Auslagern, Löschen, TODO-Menü, Text-Umwandlung) */}
{/* Convert back to bullet for headings, callouts, dividers, code and numbered items */}
{canEdit && ['callout', 'h1', 'h2', 'h3', 'divider', 'code', 'numbered'].includes(block.type) && (
)}
{/* To-Do 3-dots Menu */}
{block.type === 'todo' && canEdit && (
{showTodoMenu && (
e.stopPropagation()}
>
{!block.taskId ? (
) : (
)}
)}
)}
{/* Link to this bullet */}
{onCopyBlockLink && localContent.trim() !== '' && !mediaPicker && (
)}
{/* Branch into its own note */}
{canEdit && onExtractBlock && localContent.trim() !== '' && !mediaPicker && (
)}
{/* Zoom / Focus button */}
{onZoomBlock && (
)}
{/* Delete button */}
{canEdit && (
)}
{/* Link Insertion Modal */}
{showLinkModal && (
setShowLinkModal(false)}
onInsertLink={handleInsertLink}
/>
)}
);
};