Dev #3
@@ -996,12 +996,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Insert a new bullet immediately after index, inheriting level
|
// Insert a new bullet immediately after index, inheriting level
|
||||||
const insertNewBulletAfter = (afterIndex: number, level: number, type: BlockType = 'bullet') => {
|
const insertNewBulletAfter = (
|
||||||
|
afterIndex: number,
|
||||||
|
level: number,
|
||||||
|
type: BlockType = 'bullet',
|
||||||
|
initialContent: string = ''
|
||||||
|
) => {
|
||||||
if (isReadOnly || !selectedDoc) return;
|
if (isReadOnly || !selectedDoc) return;
|
||||||
const newBlock: DocumentBlock = {
|
const newBlock: DocumentBlock = {
|
||||||
id: `blk_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
id: `blk_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||||
type,
|
type,
|
||||||
content: '',
|
content: initialContent,
|
||||||
level,
|
level,
|
||||||
checked: false,
|
checked: false,
|
||||||
};
|
};
|
||||||
@@ -1016,6 +1021,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const el = blockInputRefs.current[newBlock.id];
|
const el = blockInputRefs.current[newBlock.id];
|
||||||
if (el) {
|
if (el) {
|
||||||
el.focus();
|
el.focus();
|
||||||
|
if ('setSelectionRange' in el && typeof (el as any).value === 'string') {
|
||||||
|
(el as any).setSelectionRange(0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 20);
|
}, 20);
|
||||||
};
|
};
|
||||||
@@ -1179,7 +1187,20 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSlashBlockId(null);
|
setSlashBlockId(null);
|
||||||
insertNewBulletAfter(index, block.level || 0);
|
const target = e.target as HTMLTextAreaElement | HTMLInputElement;
|
||||||
|
let afterContent = '';
|
||||||
|
if (target && typeof target.selectionStart === 'number') {
|
||||||
|
const cursor = target.selectionStart;
|
||||||
|
const fullContent = block.content || '';
|
||||||
|
const before = fullContent.slice(0, cursor);
|
||||||
|
const after = fullContent.slice(cursor);
|
||||||
|
if (after.length > 0) {
|
||||||
|
updateBlock(block.id, { content: before });
|
||||||
|
afterContent = after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
insertNewBulletAfter(index, block.level || 0, block.type === 'todo' ? 'todo' : 'bullet', afterContent);
|
||||||
|
return;
|
||||||
} else if (e.key === 'Tab') {
|
} else if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
@@ -1202,6 +1223,34 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const handleContentChange = (block: DocumentBlock, newContent: string) => {
|
const handleContentChange = (block: DocumentBlock, newContent: string) => {
|
||||||
if (isReadOnly) return;
|
if (isReadOnly) return;
|
||||||
ensureLock();
|
ensureLock();
|
||||||
|
|
||||||
|
// Markdown shortcut: typing '---' creates a divider and inserts a new bullet below
|
||||||
|
if (newContent.trim() === '---') {
|
||||||
|
const blkIdx = selectedDoc ? selectedDoc.blocks.findIndex((b) => b.id === block.id) : -1;
|
||||||
|
if (selectedDoc && blkIdx !== -1) {
|
||||||
|
const curLevel = block.level || 0;
|
||||||
|
const newBlockId = `blk_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||||
|
const newBullet: DocumentBlock = {
|
||||||
|
id: newBlockId,
|
||||||
|
type: 'bullet',
|
||||||
|
content: '',
|
||||||
|
level: curLevel,
|
||||||
|
checked: false,
|
||||||
|
};
|
||||||
|
const nextBlocks = [...selectedDoc.blocks];
|
||||||
|
nextBlocks[blkIdx] = {
|
||||||
|
...block,
|
||||||
|
type: 'divider',
|
||||||
|
content: '',
|
||||||
|
};
|
||||||
|
nextBlocks.splice(blkIdx + 1, 0, newBullet);
|
||||||
|
persistDocument({ ...selectedDoc, blocks: nextBlocks });
|
||||||
|
setSlashBlockId(null);
|
||||||
|
focusBlock(newBlockId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateBlock(block.id, { content: newContent });
|
updateBlock(block.id, { content: newContent });
|
||||||
|
|
||||||
// Check if user just typed '/'
|
// Check if user just typed '/'
|
||||||
@@ -1270,6 +1319,21 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}, 20);
|
}, 20);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCanvasBottomClick = () => {
|
||||||
|
if (isReadOnly || !selectedDoc) return;
|
||||||
|
if (selectedDoc.blocks.length === 0) {
|
||||||
|
insertNewBulletAfter(-1, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lastIndex = selectedDoc.blocks.length - 1;
|
||||||
|
const lastBlock = selectedDoc.blocks[lastIndex];
|
||||||
|
if (lastBlock && lastBlock.type !== 'divider' && (!lastBlock.content || lastBlock.content.trim() === '')) {
|
||||||
|
focusBlock(lastBlock.id);
|
||||||
|
} else {
|
||||||
|
insertNewBulletAfter(lastIndex, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const applySlashCommand = (blockId: string, cmd: SlashCommandItem) => {
|
const applySlashCommand = (blockId: string, cmd: SlashCommandItem) => {
|
||||||
if (isReadOnly || !selectedDoc) return;
|
if (isReadOnly || !selectedDoc) return;
|
||||||
const block = selectedDoc.blocks.find((b) => b.id === blockId);
|
const block = selectedDoc.blocks.find((b) => b.id === blockId);
|
||||||
@@ -1290,6 +1354,29 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cmd.type) {
|
if (cmd.type) {
|
||||||
|
if (cmd.type === 'divider') {
|
||||||
|
const blkIdx = selectedDoc.blocks.findIndex((b) => b.id === blockId);
|
||||||
|
const curLevel = block.level || 0;
|
||||||
|
const newBlockId = `blk_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||||
|
const newBullet: DocumentBlock = {
|
||||||
|
id: newBlockId,
|
||||||
|
type: 'bullet',
|
||||||
|
content: '',
|
||||||
|
level: curLevel,
|
||||||
|
checked: false,
|
||||||
|
};
|
||||||
|
const nextBlocks = [...selectedDoc.blocks];
|
||||||
|
nextBlocks[blkIdx] = {
|
||||||
|
...block,
|
||||||
|
type: 'divider',
|
||||||
|
content: '',
|
||||||
|
};
|
||||||
|
nextBlocks.splice(blkIdx + 1, 0, newBullet);
|
||||||
|
persistDocument({ ...selectedDoc, blocks: nextBlocks });
|
||||||
|
focusBlock(newBlockId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
updateBlock(blockId, { type: cmd.type, content: cleanContent, checked: cmd.type === 'todo' ? false : undefined });
|
updateBlock(blockId, { type: cmd.type, content: cleanContent, checked: cmd.type === 'todo' ? false : undefined });
|
||||||
focusBlock(blockId);
|
focusBlock(blockId);
|
||||||
}
|
}
|
||||||
@@ -2266,7 +2353,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
{/* Right Logseq Outliner Editor */}
|
{/* Right Logseq Outliner Editor */}
|
||||||
<div className="lg:col-span-8">
|
<div className="lg:col-span-8">
|
||||||
{selectedDoc ? (
|
{selectedDoc ? (
|
||||||
<div className="p-4 panel-surface space-y-3 min-h-[500px]">
|
<div
|
||||||
|
className="p-4 panel-surface space-y-3 min-h-[500px]"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
handleCanvasBottomClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Document Header & Metadata */}
|
{/* Document Header & Metadata */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-3 panel-header">
|
<div className="flex flex-wrap items-center justify-between gap-3 pb-3 panel-header">
|
||||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted">
|
<div className="flex flex-wrap items-center gap-2 text-xs text-muted">
|
||||||
@@ -2519,15 +2613,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
{/* LOGSEQ HIERARCHICAL BULLET LIST CANVAS */}
|
{/* LOGSEQ HIERARCHICAL BULLET LIST CANVAS */}
|
||||||
{/* =========================================================== */}
|
{/* =========================================================== */}
|
||||||
<div
|
<div
|
||||||
className="space-y-1 pt-2 min-h-[300px] cursor-text"
|
className="space-y-1 pt-2 min-h-[400px] pb-20 cursor-text"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
if (selectedDoc.blocks.length === 0) {
|
handleCanvasBottomClick();
|
||||||
insertNewBulletAfter(-1, 0);
|
|
||||||
} else {
|
|
||||||
const lastBlock = selectedDoc.blocks[selectedDoc.blocks.length - 1];
|
|
||||||
blockInputRefs.current[lastBlock.id]?.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -2822,8 +2911,34 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
|
|
||||||
{/* TYPE: DIVIDER */}
|
{/* TYPE: DIVIDER */}
|
||||||
{block.type === 'divider' && (
|
{block.type === 'divider' && (
|
||||||
<div className="py-2">
|
<div
|
||||||
<hr className="border-border" />
|
tabIndex={isReadOnly ? undefined : 0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (!isReadOnly && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
insertNewBulletAfter(originalIndex, block.level || 0);
|
||||||
|
} else if (!isReadOnly && (e.key === 'Backspace' || e.key === 'Delete')) {
|
||||||
|
e.preventDefault();
|
||||||
|
deleteBlockAndFocusPrev(originalIndex);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!isReadOnly) {
|
||||||
|
insertNewBulletAfter(originalIndex, block.level || 0);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="py-2.5 my-1 group/divider relative cursor-pointer focus:outline-none"
|
||||||
|
title={isReadOnly ? undefined : "Klicken oder Enter drücken, um darunter einen neuen Aufzählungspunkt einzufügen"}
|
||||||
|
>
|
||||||
|
<div className="h-0.5 w-full bg-border group-hover/divider:bg-accent/70 group-focus/divider:bg-accent transition-colors rounded-full" />
|
||||||
|
{!isReadOnly && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover/divider:opacity-100 transition-opacity pointer-events-none">
|
||||||
|
<span className="bg-panel border border-border text-[10px] text-muted px-2 py-0.5 rounded-full shadow-xs flex items-center gap-1 font-medium">
|
||||||
|
<Plus className="w-3 h-3 text-accent" />
|
||||||
|
<span>Punkt einfügen</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -2899,6 +3014,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* Explicit bottom clickable area */}
|
||||||
|
{!isReadOnly && (
|
||||||
|
<div
|
||||||
|
className="pt-3 pb-8 px-2 text-xs text-faint hover:text-muted cursor-pointer transition-colors flex items-center gap-1.5 opacity-60 hover:opacity-100 select-none"
|
||||||
|
onClick={handleCanvasBottomClick}
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>Neuen Punkt hinzufügen...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in new issue
Block a user