fix(workspace): auto-create bullet below divider and allow click to create bullet at bottom
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
This commit is contained in:
1 parent
d34178aa1a
commit
31847ffaa3
1 file changed
+139
-13
@@ -996,12 +996,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
};
|
||||
|
||||
// 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;
|
||||
const newBlock: DocumentBlock = {
|
||||
id: `blk_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||
type,
|
||||
content: '',
|
||||
content: initialContent,
|
||||
level,
|
||||
checked: false,
|
||||
};
|
||||
@@ -1016,6 +1021,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const el = blockInputRefs.current[newBlock.id];
|
||||
if (el) {
|
||||
el.focus();
|
||||
if ('setSelectionRange' in el && typeof (el as any).value === 'string') {
|
||||
(el as any).setSelectionRange(0, 0);
|
||||
}
|
||||
}
|
||||
}, 20);
|
||||
};
|
||||
@@ -1179,7 +1187,20 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
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') {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
@@ -1202,6 +1223,34 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const handleContentChange = (block: DocumentBlock, newContent: string) => {
|
||||
if (isReadOnly) return;
|
||||
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 });
|
||||
|
||||
// Check if user just typed '/'
|
||||
@@ -1270,6 +1319,21 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
}, 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) => {
|
||||
if (isReadOnly || !selectedDoc) return;
|
||||
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 === '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 });
|
||||
focusBlock(blockId);
|
||||
}
|
||||
@@ -2266,7 +2353,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
{/* Right Logseq Outliner Editor */}
|
||||
<div className="lg:col-span-8">
|
||||
{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 */}
|
||||
<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">
|
||||
@@ -2519,15 +2613,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
{/* LOGSEQ HIERARCHICAL BULLET LIST CANVAS */}
|
||||
{/* =========================================================== */}
|
||||
<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) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
if (selectedDoc.blocks.length === 0) {
|
||||
insertNewBulletAfter(-1, 0);
|
||||
} else {
|
||||
const lastBlock = selectedDoc.blocks[selectedDoc.blocks.length - 1];
|
||||
blockInputRefs.current[lastBlock.id]?.focus();
|
||||
}
|
||||
handleCanvasBottomClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -2822,8 +2911,34 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
{/* TYPE: DIVIDER */}
|
||||
{block.type === 'divider' && (
|
||||
<div className="py-2">
|
||||
<hr className="border-border" />
|
||||
<div
|
||||
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>
|
||||
)}
|
||||
|
||||
@@ -2899,6 +3014,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
</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>
|
||||
) : (
|
||||
|
||||
Reference in new issue
Block a user