Dev #3
@@ -153,6 +153,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const [linkSearchQuery, setLinkSearchQuery] = useState('');
|
||||
const [linkWebTitle, setLinkWebTitle] = useState('');
|
||||
const [linkWebUrl, setLinkWebUrl] = useState('');
|
||||
const [activeEditingBlockId, setActiveEditingBlockId] = useState<string | null>(null);
|
||||
const [editingLinkOriginalRaw, setEditingLinkOriginalRaw] = useState<string | null>(null);
|
||||
|
||||
// Drag and Drop reordering state for blocks
|
||||
const [draggingBlockId, setDraggingBlockId] = useState<string | null>(null);
|
||||
@@ -1245,6 +1247,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
setSlashBlockId(null);
|
||||
deleteBlockAndFocusPrev(index);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
setActiveEditingBlockId(null);
|
||||
(e.target as HTMLElement)?.blur();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1327,6 +1332,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
const focusBlock = (blockId: string) => {
|
||||
setLastFocusedBlockId(blockId);
|
||||
setActiveEditingBlockId(blockId);
|
||||
const tryFocus = () => {
|
||||
const el = blockInputRefs.current[blockId];
|
||||
if (el) {
|
||||
@@ -1373,6 +1379,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
if (cmd.action) {
|
||||
updateBlock(blockId, { content: cleanContent });
|
||||
setEditingLinkOriginalRaw(null);
|
||||
setLinkModalBlockId(blockId);
|
||||
setLinkModalTab(cmd.action === 'insert_weblink' ? 'web' : 'note');
|
||||
setLinkSearchQuery('');
|
||||
@@ -1419,13 +1426,18 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
if (!linkModalBlockId || !selectedDoc) return;
|
||||
const block = selectedDoc.blocks.find((b) => b.id === linkModalBlockId);
|
||||
if (block) {
|
||||
let newContent: string;
|
||||
if (editingLinkOriginalRaw && block.content.includes(editingLinkOriginalRaw)) {
|
||||
newContent = block.content.replace(editingLinkOriginalRaw, `[[${docTitle}]]`);
|
||||
} else {
|
||||
const space = block.content && !block.content.endsWith(' ') ? ' ' : '';
|
||||
const newContent = `${block.content}${space}[[${docTitle}]] `;
|
||||
newContent = `${block.content}${space}[[${docTitle}]] `;
|
||||
}
|
||||
updateBlock(linkModalBlockId, { content: newContent });
|
||||
}
|
||||
const targetBlockId = linkModalBlockId;
|
||||
setLinkModalBlockId(null);
|
||||
focusBlock(targetBlockId);
|
||||
setEditingLinkOriginalRaw(null);
|
||||
setActiveEditingBlockId(null);
|
||||
};
|
||||
|
||||
const handleInsertWebLink = () => {
|
||||
@@ -1445,15 +1457,20 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
}
|
||||
}
|
||||
if (block) {
|
||||
let newContent: string;
|
||||
if (editingLinkOriginalRaw && block.content.includes(editingLinkOriginalRaw)) {
|
||||
newContent = block.content.replace(editingLinkOriginalRaw, `[${title}](${url})`);
|
||||
} else {
|
||||
const space = block.content && !block.content.endsWith(' ') ? ' ' : '';
|
||||
const newContent = `${block.content}${space}[${title}](${url}) `;
|
||||
newContent = `${block.content}${space}[${title}](${url}) `;
|
||||
}
|
||||
updateBlock(linkModalBlockId, { content: newContent });
|
||||
}
|
||||
const targetBlockId = linkModalBlockId;
|
||||
setLinkModalBlockId(null);
|
||||
setEditingLinkOriginalRaw(null);
|
||||
setLinkWebTitle('');
|
||||
setLinkWebUrl('');
|
||||
focusBlock(targetBlockId);
|
||||
setActiveEditingBlockId(null);
|
||||
};
|
||||
|
||||
const getDirectChildTodos = (blocks: DocumentBlock[], parentIndex: number): DocumentBlock[] => {
|
||||
@@ -1612,8 +1629,11 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const hasRichTokens = (text: string): boolean =>
|
||||
Boolean(text && (text.includes('[[') || text.includes('#') || (text.includes('[') && text.includes(']('))));
|
||||
|
||||
// Helper to render text with clickable Wiki-Links, Web-Links and Hashtags
|
||||
const renderFormattedText = (text: string) => {
|
||||
const renderFormattedText = (text: string, blockId?: string) => {
|
||||
const parts = text.split(/(\[\[.*?\]\]|\[.*?\]\(https?:\/\/[^\s)]+\)|#[a-zA-Z0-9äöüÄÖÜß_-]+)/g);
|
||||
return parts.map((part, i) => {
|
||||
if (part.startsWith('[[') && part.endsWith(']]')) {
|
||||
@@ -1622,6 +1642,13 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className={`inline-flex items-center gap-1 px-1.5 py-0.5 mx-0.5 border text-xs font-medium rounded-btn transition-colors align-baseline ${
|
||||
target
|
||||
? 'bg-accent/20 border-accent/40 text-accent hover:bg-accent/30'
|
||||
: 'badge-neutral hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (target) {
|
||||
@@ -1630,17 +1657,30 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
setWorkspaceMode('editor');
|
||||
}
|
||||
}}
|
||||
className={`inline-flex items-center gap-1 px-1.5 py-0.2 mx-0.5 border text-xs font-medium cursor-pointer rounded-btn transition-colors ${
|
||||
target
|
||||
? 'bg-accent/20 border-accent/40 text-accent hover:bg-accent/30'
|
||||
: 'badge-neutral hover:text-main'
|
||||
}`}
|
||||
className="inline-flex items-center gap-1 cursor-pointer"
|
||||
title={target ? `Öffne: ${target.title}` : `Dokument "${linkTitle}" existiert noch nicht`}
|
||||
>
|
||||
<span>📄</span>
|
||||
<span>{linkTitle}</span>
|
||||
<ExternalLink className="w-2.5 h-2.5 opacity-60" />
|
||||
</span>
|
||||
{!isReadOnly && blockId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingLinkOriginalRaw(part);
|
||||
setLinkModalBlockId(blockId);
|
||||
setLinkModalTab('note');
|
||||
setLinkSearchQuery(linkTitle);
|
||||
}}
|
||||
className="ml-0.5 p-0.5 rounded hover:bg-black/10 dark:hover:bg-white/10 opacity-60 hover:opacity-100 transition-opacity cursor-pointer"
|
||||
title="Notiz-Link bearbeiten"
|
||||
>
|
||||
<Edit3 className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const webMatch = part.match(/^\[(.*?)\]\((https?:\/\/[^\s)]+)\)$/);
|
||||
@@ -1648,19 +1688,40 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const title = webMatch[1] || webMatch[2];
|
||||
const url = webMatch[2];
|
||||
return (
|
||||
<a
|
||||
<span
|
||||
key={i}
|
||||
className="inline-flex items-center gap-1 px-1.5 py-0.5 mx-0.5 bg-sky-500/10 border border-sky-500/30 text-sky-500 dark:text-sky-400 hover:bg-sky-500/20 text-xs font-medium rounded-btn transition-colors align-baseline"
|
||||
>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-1 px-1.5 py-0.2 mx-0.5 bg-sky-500/10 border border-sky-500/30 text-sky-500 dark:text-sky-400 hover:bg-sky-500/20 text-xs font-medium rounded-btn transition-colors"
|
||||
className="inline-flex items-center gap-1 cursor-pointer"
|
||||
title={`Öffne: ${url}`}
|
||||
>
|
||||
<span>🌐</span>
|
||||
<span>{title}</span>
|
||||
<ExternalLink className="w-2.5 h-2.5 opacity-60" />
|
||||
</a>
|
||||
{!isReadOnly && blockId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingLinkOriginalRaw(part);
|
||||
setLinkModalBlockId(blockId);
|
||||
setLinkModalTab('web');
|
||||
setLinkWebTitle(webMatch[1] || '');
|
||||
setLinkWebUrl(url);
|
||||
}}
|
||||
className="ml-0.5 p-0.5 rounded hover:bg-sky-500/20 opacity-60 hover:opacity-100 transition-opacity cursor-pointer"
|
||||
title="Link bearbeiten"
|
||||
>
|
||||
<Edit3 className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (part.startsWith('#') && part.length > 1) {
|
||||
@@ -1672,7 +1733,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
e.stopPropagation();
|
||||
openTagPage(tagWord);
|
||||
}}
|
||||
className="inline-flex items-center gap-0.5 px-1.5 py-0.2 mx-0.5 bg-blue-500/10 border border-blue-500/30 text-blue-500 dark:text-blue-400 hover:bg-blue-500/20 text-xs font-medium cursor-pointer rounded-btn transition-colors"
|
||||
className="inline-flex items-center gap-0.5 px-1.5 py-0.5 mx-0.5 bg-blue-500/10 border border-blue-500/30 text-blue-500 dark:text-blue-400 hover:bg-blue-500/20 text-xs font-medium cursor-pointer rounded-btn transition-colors align-baseline"
|
||||
title={`Öffne Hashtag-Seite: #${tagWord}`}
|
||||
>
|
||||
<Hash className="w-2.5 h-2.5 opacity-80" />
|
||||
@@ -1680,7 +1741,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span key={i}>{part}</span>;
|
||||
return <span key={i} className="whitespace-pre-wrap">{part}</span>;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2754,19 +2815,40 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
isReadOnly ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
|
||||
}`}
|
||||
/>
|
||||
{(!hasRichTokens(block.content) || activeEditingBlockId === block.id) && !isReadOnly ? (
|
||||
<input
|
||||
ref={(el) => (blockInputRefs.current[block.id] = el)}
|
||||
type="text"
|
||||
readOnly={isReadOnly}
|
||||
value={block.content}
|
||||
onFocus={handleBlockFocus}
|
||||
onBlur={handleBlockBlur}
|
||||
onFocus={() => {
|
||||
setActiveEditingBlockId(block.id);
|
||||
handleBlockFocus();
|
||||
}}
|
||||
onBlur={() => {
|
||||
setActiveEditingBlockId((prev) => (prev === block.id ? null : prev));
|
||||
handleBlockBlur();
|
||||
}}
|
||||
onChange={(e) => handleContentChange(block, e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, block, originalIndex)}
|
||||
className={`w-full bg-transparent text-sm focus:outline-none ${
|
||||
block.checked ? 'line-through text-faint' : 'text-main'
|
||||
} ${isReadOnly ? 'cursor-default' : ''}`}
|
||||
}`}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!isReadOnly) {
|
||||
focusBlock(block.id);
|
||||
}
|
||||
}}
|
||||
className={`w-full text-sm leading-relaxed break-words min-h-[20px] ${
|
||||
block.checked ? 'line-through text-faint' : 'text-main'
|
||||
} ${isReadOnly ? 'cursor-default' : 'cursor-text'}`}
|
||||
>
|
||||
{renderFormattedText(block.content, block.id)}
|
||||
</div>
|
||||
)}
|
||||
{!isReadOnly && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -2778,11 +2860,6 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{(block.content.includes('[[') || block.content.includes('#') || (block.content.includes('[') && block.content.includes(']('))) && (
|
||||
<div className="text-xs text-muted flex flex-wrap items-center gap-1 pt-0.5 pl-6">
|
||||
{renderFormattedText(block.content)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress bar for child todos if any exist */}
|
||||
{(() => {
|
||||
@@ -2996,22 +3073,36 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
{/* TYPE: DEFAULT BULLET / PARAGRAPH */}
|
||||
{(block.type === 'bullet' || block.type === 'paragraph') && (
|
||||
<div className="space-y-0.5">
|
||||
{(!hasRichTokens(block.content) || activeEditingBlockId === block.id) && !isReadOnly ? (
|
||||
<textarea
|
||||
ref={(el) => (blockInputRefs.current[block.id] = el)}
|
||||
readOnly={isReadOnly}
|
||||
rows={Math.max(1, Math.ceil((block.content || '').length / 80))}
|
||||
value={block.content}
|
||||
onFocus={handleBlockFocus}
|
||||
onBlur={handleBlockBlur}
|
||||
onFocus={() => {
|
||||
setActiveEditingBlockId(block.id);
|
||||
handleBlockFocus();
|
||||
}}
|
||||
onBlur={() => {
|
||||
setActiveEditingBlockId((prev) => (prev === block.id ? null : prev));
|
||||
handleBlockBlur();
|
||||
}}
|
||||
onChange={(e) => handleContentChange(block, e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, block, originalIndex)}
|
||||
className={`w-full bg-transparent text-sm text-main leading-relaxed focus:outline-none resize-none placeholder:text-faint ${
|
||||
isReadOnly ? 'cursor-default' : ''
|
||||
}`}
|
||||
className="w-full bg-transparent text-sm text-main leading-relaxed focus:outline-none resize-none placeholder:text-faint"
|
||||
/>
|
||||
{(block.content.includes('[[') || block.content.includes('#') || (block.content.includes('[') && block.content.includes(']('))) && (
|
||||
<div className="text-xs text-muted flex flex-wrap items-center gap-1 pt-0.5">
|
||||
{renderFormattedText(block.content)}
|
||||
) : (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!isReadOnly) {
|
||||
focusBlock(block.id);
|
||||
}
|
||||
}}
|
||||
className={`w-full text-sm text-main leading-relaxed break-words min-h-[22px] ${
|
||||
isReadOnly ? 'cursor-default' : 'cursor-text'
|
||||
}`}
|
||||
>
|
||||
{renderFormattedText(block.content, block.id)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -3877,8 +3968,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs"
|
||||
onClick={() => {
|
||||
const blockId = linkModalBlockId;
|
||||
const wasEditingLink = Boolean(editingLinkOriginalRaw);
|
||||
setLinkModalBlockId(null);
|
||||
if (blockId) focusBlock(blockId);
|
||||
setEditingLinkOriginalRaw(null);
|
||||
if (blockId && !wasEditingLink) focusBlock(blockId);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -3891,14 +3984,18 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<div className="p-1.5 rounded-btn bg-accent/15 text-accent">
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-main">Link einfügen</h3>
|
||||
<h3 className="text-sm font-semibold text-main">
|
||||
{editingLinkOriginalRaw ? 'Link bearbeiten' : 'Link einfügen'}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const blockId = linkModalBlockId;
|
||||
const wasEditingLink = Boolean(editingLinkOriginalRaw);
|
||||
setLinkModalBlockId(null);
|
||||
if (blockId) focusBlock(blockId);
|
||||
setEditingLinkOriginalRaw(null);
|
||||
if (blockId && !wasEditingLink) focusBlock(blockId);
|
||||
}}
|
||||
className="p-1 text-faint hover:text-main rounded-btn transition-colors cursor-pointer"
|
||||
>
|
||||
@@ -3949,8 +4046,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const blockId = linkModalBlockId;
|
||||
const wasEditingLink = Boolean(editingLinkOriginalRaw);
|
||||
setLinkModalBlockId(null);
|
||||
if (blockId) focusBlock(blockId);
|
||||
setEditingLinkOriginalRaw(null);
|
||||
if (blockId && !wasEditingLink) focusBlock(blockId);
|
||||
} else if (e.key === 'Enter') {
|
||||
const filtered = docs.filter(
|
||||
(d) =>
|
||||
@@ -4031,8 +4130,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const blockId = linkModalBlockId;
|
||||
const wasEditingLink = Boolean(editingLinkOriginalRaw);
|
||||
setLinkModalBlockId(null);
|
||||
if (blockId) focusBlock(blockId);
|
||||
setEditingLinkOriginalRaw(null);
|
||||
if (blockId && !wasEditingLink) focusBlock(blockId);
|
||||
}
|
||||
}}
|
||||
className="input-field w-full h-9 text-xs"
|
||||
@@ -4050,8 +4151,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const blockId = linkModalBlockId;
|
||||
const wasEditingLink = Boolean(editingLinkOriginalRaw);
|
||||
setLinkModalBlockId(null);
|
||||
if (blockId) focusBlock(blockId);
|
||||
setEditingLinkOriginalRaw(null);
|
||||
if (blockId && !wasEditingLink) focusBlock(blockId);
|
||||
}
|
||||
}}
|
||||
className="input-field w-full h-9 text-xs"
|
||||
@@ -4063,8 +4166,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const blockId = linkModalBlockId;
|
||||
const wasEditingLink = Boolean(editingLinkOriginalRaw);
|
||||
setLinkModalBlockId(null);
|
||||
if (blockId) focusBlock(blockId);
|
||||
setEditingLinkOriginalRaw(null);
|
||||
if (blockId && !wasEditingLink) focusBlock(blockId);
|
||||
}}
|
||||
className="btn-action h-8 px-3 text-xs cursor-pointer"
|
||||
>
|
||||
@@ -4075,7 +4180,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
disabled={!linkWebUrl.trim()}
|
||||
className="btn-primary h-8 px-4 text-xs font-medium cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
Link einfügen
|
||||
{editingLinkOriginalRaw ? 'Link speichern' : 'Link einfügen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in new issue
Block a user