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 ? (
flushDebounce(localContent, { collapsed: !block.collapsed })
}
className="p-0.5 text-muted hover:text-accent transition-[color,opacity] opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
title={block.collapsed ? 'Unterpunkte ausklappen' : 'Unterpunkte einklappen'}
>
{block.collapsed ? (
) : (
)}
) : (
)}
{/* Bullet handle for dragging and zoom focus */}
canEdit && onDragStartBlock?.(index)}
onClick={() => onZoomBlock?.(block.id)}
className={`${
block.type === 'numbered' && listMarker ? 'w-4 h-4 justify-end overflow-visible' : 'w-4 h-4 justify-center'
} flex items-center rounded-btn group/bullet transition-transform ${
canEdit ? 'cursor-grab active:cursor-grabbing' : 'cursor-pointer'
}`}
title={
canEdit
? 'Ziehen zum Verschieben • Klick: Ast hineinzoomen (Fokus-Modus)'
: 'Klick: In diesen Ast hineinzoomen (Fokus-Modus)'
}
>
{block.type === 'numbered' && listMarker ? (
{listMarker}
) : (
)}
{/* If To-Do block: checkbox alongside bullet */}
{block.type === 'todo' && (
flushDebounce(localContent, { checked: !block.checked })
}
className={`w-4 h-4 checkbox-square flex items-center justify-center transition-colors border ${
block.checked
? 'bg-status-ok border-status-ok text-on-accent'
: 'border-strong hover:border-accent text-transparent'
}`}
title={block.checked ? 'Als offen markieren' : 'Als erledigt markieren'}
>
)}
{/* 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 */}
{previewUrl && !activeFocus ? (
// The card above shows the page; the address itself becomes a small link marker
e.stopPropagation()}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium text-accent bg-accent-dim hover:bg-accent/20 no-underline transition-colors"
title={previewUrl}
>
{linkHost(previewUrl)}
) : localContent ? (
renderInlineMarkdown(localContent, { onTagClick, onWikiLinkClick })
) : (
'\u200b'
)}
)}
{/* If To-Do is an enhanced task, render badge stably outside text layer */}
{block.type === 'todo' && block.taskId && (
e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
onOpenTask?.(block.taskId as string);
}}
className="badge-accent text-[9px] inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded font-medium shrink-0 cursor-pointer hover:opacity-80"
title="Erweiterte Aufgabe öffnen"
>
Erweitert
)}
{/* Checkbox tied to a subtask of a linked task: tick and text follow the subtask */}
{block.type === 'todo' && !block.taskId && block.subtaskOf && (
e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
onOpenTask?.(block.subtaskOf as string);
}}
className="badge-neutral text-[9px] inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded font-medium shrink-0 cursor-pointer hover:opacity-80"
title="Unteraufgabe einer verknüpften Aufgabe – Aufgabe öffnen"
>
Unteraufgabe
)}
{/* 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 && (
<>
onResolveConflict(block.id, true)}
className="px-1.5 py-0.5 rounded-btn border border-status-warn/40 hover:bg-status-warn/15 cursor-pointer"
title={block.conflictOf === 'title' ? 'Diesen Text als Titel übernehmen' : 'Diesen Text in den ursprünglichen Punkt übernehmen'}
>
Übernehmen
onResolveConflict(block.id, false)}
className="px-1.5 py-0.5 rounded-btn border border-border text-muted hover:text-main hover:bg-panel cursor-pointer"
title="Diese Version verwerfen"
>
Verwerfen
>
)}
)}
{/* Floating Slash Commands Menu */}
{slashSearch !== null && canEdit && (
Befehl auswählen
{filteredSlashCommands.map((cmd, i) => (
{
slashItemRefs.current[i] = el;
}}
type="button"
onMouseDown={(e) => {
e.preventDefault();
applySlashCommand(cmd);
}}
className={`w-full text-left px-2 py-1 rounded-btn text-xs flex items-center justify-between transition-colors ${
i === slashIndex
? 'bg-accent-dim text-accent font-semibold'
: 'text-main hover:bg-panel-elevated'
}`}
>
{cmd.icon}
{cmd.label}
{cmd.desc}
))}
{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) && (
{
refocusAfterTypeChangeRef.current = true;
flushDebounce(localContent, { type: 'bullet', checked: false });
}}
className="p-1 sm:p-0.5 min-w-[28px] min-h-[28px] sm:min-w-0 sm:min-h-0 flex items-center justify-center text-faint hover:text-accent rounded-btn transition-colors cursor-pointer"
title="In normalen Text umwandeln"
>
)}
{/* To-Do 3-dots Menu */}
{block.type === 'todo' && canEdit && (
{
e.stopPropagation();
setShowTodoMenu((prev) => !prev);
}}
className="p-1 sm:p-0.5 min-w-[28px] min-h-[28px] sm:min-w-0 sm:min-h-0 flex items-center justify-center text-faint hover:text-accent rounded-btn transition-colors cursor-pointer"
title="Aufgaben-Optionen"
>
{showTodoMenu && (
e.stopPropagation()}
>
{block.subtaskOf && !block.taskId ? (
Unteraufgabe einer verknüpften Aufgabe
) : !block.taskId ? (
{
setShowTodoMenu(false);
try {
const targetDocId = docId || block.documentId || block.docId || '';
const thought = await api.createEnhancedTask(targetDocId, block.id, {
content: localContent || 'Aufgabe',
checked: Boolean(block.checked),
});
const updatedBlock: DocumentBlock = {
...latestBlockRef.current,
content: localContent,
taskId: thought.id,
type: 'todo',
};
onUpdateBlockRef.current(updatedBlock, true);
onOpenTask?.(thought.id);
} catch (err: any) {
alert(err.message || 'Fehler beim Erstellen der erweiterten Aufgabe');
}
}}
className="w-full text-left px-2 py-1.5 rounded-btn hover:bg-panel-elevated hover:text-accent flex items-center gap-1.5 text-main font-medium cursor-pointer"
>
In erweiterte Aufgabe umwandeln
) : (
{
setShowTodoMenu(false);
try {
if (block.taskId) {
await api.unlinkEnhancedTask(block.taskId).catch(() => {});
}
const updatedBlock: DocumentBlock = {
...latestBlockRef.current,
content: localContent,
taskId: '',
type: 'todo',
};
onUpdateBlockRef.current(updatedBlock, true);
} catch (err: any) {
alert(err.message || 'Fehler beim Aufheben der Verknüpfung');
}
}}
className="w-full text-left px-2 py-1.5 rounded-btn hover:bg-panel-elevated hover:text-status-warn flex items-center gap-1.5 text-main font-medium cursor-pointer"
>
Verknüpfung aufheben
)}
{
setShowTodoMenu(false);
refocusAfterTypeChangeRef.current = true;
flushDebounce(localContent, { type: 'bullet', checked: false, taskId: '' });
}}
className="w-full text-left px-2 py-1.5 rounded-btn hover:bg-panel-elevated hover:text-accent flex items-center gap-1.5 text-main font-medium cursor-pointer border-t border-border mt-0.5 pt-1.5"
>
In normalen Text umwandeln
)}
)}
{/* Link to this bullet */}
{onCopyBlockLink && localContent.trim() !== '' && !mediaPicker && (
onCopyBlockLink(block.id)}
className="p-1 sm:p-0.5 min-w-[28px] min-h-[28px] sm:min-w-0 sm:min-h-0 flex items-center justify-center text-faint hover:text-accent rounded-btn transition-colors cursor-pointer"
title="Link zu diesem Punkt kopieren"
aria-label="Link zu diesem Punkt kopieren"
>
)}
{/* Branch into its own note */}
{canEdit && onExtractBlock && localContent.trim() !== '' && !isNoteLinkOnly(localContent) && !mediaPicker && (
onExtractBlock(block.id)}
className="p-1 sm:p-0.5 min-w-[28px] min-h-[28px] sm:min-w-0 sm:min-h-0 flex items-center justify-center text-faint hover:text-accent rounded-btn transition-colors cursor-pointer"
title="In eigene Notiz umwandeln (mit allen Unterpunkten)"
>
)}
{/* Zoom / Focus button */}
{onZoomBlock && (
onZoomBlock(block.id)}
className="p-1 sm:p-0.5 min-w-[28px] min-h-[28px] sm:min-w-0 sm:min-h-0 flex items-center justify-center text-faint hover:text-accent rounded-btn transition-colors cursor-pointer"
title="In diesen Ast hineinzoomen (Fokus-Modus)"
>
)}
{/* Delete button */}
{canEdit && (
{
if (hasChildren) {
if (window.confirm('Bist du sicher? Alle Unterpunkte dieses Asts werden ebenfalls gelöscht.')) {
onDeleteBlockRef.current(block.id, true);
}
} else {
onDeleteBlockRef.current(block.id, false);
}
}}
className="p-1 sm:p-0.5 min-w-[28px] min-h-[28px] sm:min-w-0 sm:min-h-0 flex items-center justify-center text-faint hover:text-status-err rounded-btn transition-colors cursor-pointer"
title="Block löschen"
>
)}
{/* Link Insertion Modal */}
{showLinkModal && (
setShowLinkModal(false)}
onInsertLink={handleInsertLink}
/>
)}
);
};