mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-02 04:53:27 +00:00
Compare commits
1 Commits
bcd8aa9378
...
jf/right_c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80b23c3f4b |
94
src/hooks/use-click-outside.ts
Normal file
94
src/hooks/use-click-outside.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Hook that handles clicks outside of the passed ref elements
|
||||
* @param refs - Array of refs to consider as "inside" (e.g., inputRef, buttonRef)
|
||||
* @param handler - Function to call when clicking outside
|
||||
* @param enabled - Whether the hook is active (default: true)
|
||||
* @param delay - Delay before adding listeners to avoid immediate trigger (default: 100ms)
|
||||
*/
|
||||
export function useClickOutside(
|
||||
refs: RefObject<HTMLElement>[],
|
||||
handler: (() => void) | null,
|
||||
enabled = true,
|
||||
delay = 100
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (!enabled || !handler) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// Check if click is on any of the refs
|
||||
const isInsideAnyRef = refs.some((ref) => {
|
||||
if (!ref.current) return false;
|
||||
return ref.current.contains(target) || ref.current === target;
|
||||
});
|
||||
|
||||
if (!isInsideAnyRef) {
|
||||
handler();
|
||||
}
|
||||
};
|
||||
|
||||
// Small delay to avoid immediate trigger when entering edit mode
|
||||
const timer = setTimeout(() => {
|
||||
// Use capture phase to catch events before they might be stopped
|
||||
document.addEventListener('mousedown', handleClickOutside, true);
|
||||
document.addEventListener('click', handleClickOutside, true);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
document.removeEventListener('mousedown', handleClickOutside, true);
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
};
|
||||
}, [refs, handler, enabled, delay]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified version for edit mode scenarios
|
||||
* Automatically saves and exits edit mode when clicking outside
|
||||
*/
|
||||
export function useEditClickOutside(
|
||||
inputRef: RefObject<HTMLElement>,
|
||||
editMode: boolean,
|
||||
onSave: () => void,
|
||||
delay = 100
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (!editMode) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
// Check if click is on the input
|
||||
const isInput =
|
||||
inputRef.current &&
|
||||
(inputRef.current.contains(target) ||
|
||||
inputRef.current === target);
|
||||
|
||||
// Check if click is on a button inside the same parent
|
||||
const isRelatedButton =
|
||||
target.closest('button') &&
|
||||
inputRef.current?.parentElement?.contains(target);
|
||||
|
||||
if (!isInput && !isRelatedButton) {
|
||||
onSave();
|
||||
}
|
||||
};
|
||||
|
||||
// Small delay to avoid immediate trigger when entering edit mode
|
||||
const timer = setTimeout(() => {
|
||||
// Use capture phase to catch events before they might be stopped
|
||||
document.addEventListener('mousedown', handleClickOutside, true);
|
||||
document.addEventListener('click', handleClickOutside, true);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
document.removeEventListener('mousedown', handleClickOutside, true);
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
};
|
||||
}, [editMode, inputRef, onSave, delay]);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from '@/components/context-menu/context-menu';
|
||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import type { Area } from '@/lib/domain/area';
|
||||
import { Pencil, Trash2 } from 'lucide-react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
export interface AreaNodeContextMenuProps {
|
||||
area: Area;
|
||||
onEditName?: () => void;
|
||||
}
|
||||
|
||||
export const AreaNodeContextMenu: React.FC<
|
||||
React.PropsWithChildren<AreaNodeContextMenuProps>
|
||||
> = ({ children, area, onEditName }) => {
|
||||
const { removeArea, readonly } = useChartDB();
|
||||
const { isMd: isDesktop } = useBreakpoint('md');
|
||||
|
||||
const removeAreaHandler = useCallback(() => {
|
||||
removeArea(area.id);
|
||||
}, [removeArea, area.id]);
|
||||
|
||||
if (!isDesktop || readonly) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
{onEditName && (
|
||||
<ContextMenuItem
|
||||
onClick={onEditName}
|
||||
className="flex justify-between gap-3"
|
||||
>
|
||||
<span>Edit Area Name</span>
|
||||
<Pencil className="size-3.5" />
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuItem
|
||||
onClick={removeAreaHandler}
|
||||
className="flex justify-between gap-3"
|
||||
>
|
||||
<span>Delete Area</span>
|
||||
<Trash2 className="size-3.5 text-red-700" />
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { useEditClickOutside } from '@/hooks/use-click-outside';
|
||||
import type { NodeProps, Node } from '@xyflow/react';
|
||||
import { NodeResizer } from '@xyflow/react';
|
||||
import type { Area } from '@/lib/domain/area';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { Input } from '@/components/input/input';
|
||||
import { useClickAway, useKeyPressEvent } from 'react-use';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -12,9 +13,10 @@ import {
|
||||
} from '@/components/tooltip/tooltip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Check, GripVertical } from 'lucide-react';
|
||||
import { Check, GripVertical, Pencil } from 'lucide-react';
|
||||
import { Button } from '@/components/button/button';
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import { AreaNodeContextMenu } from './area-node-context-menu';
|
||||
|
||||
export type AreaNodeType = Node<
|
||||
{
|
||||
@@ -35,12 +37,11 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo(
|
||||
const focused = !!selected && !dragging;
|
||||
|
||||
const editAreaName = useCallback(() => {
|
||||
if (!editMode) return;
|
||||
if (areaName.trim()) {
|
||||
updateArea(area.id, { name: areaName.trim() });
|
||||
}
|
||||
setEditMode(false);
|
||||
}, [areaName, area.id, updateArea, editMode]);
|
||||
}, [areaName, area.id, updateArea]);
|
||||
|
||||
const abortEdit = useCallback(() => {
|
||||
setEditMode(false);
|
||||
@@ -52,89 +53,129 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo(
|
||||
openAreaFromSidebar(area.id);
|
||||
}, [selectSidebarSection, openAreaFromSidebar, area.id]);
|
||||
|
||||
useClickAway(inputRef, editAreaName);
|
||||
// Handle click outside to save and exit edit mode
|
||||
useEditClickOutside(inputRef, editMode, editAreaName);
|
||||
|
||||
useKeyPressEvent('Enter', editAreaName);
|
||||
useKeyPressEvent('Escape', abortEdit);
|
||||
|
||||
const enterEditMode = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setEditMode(true);
|
||||
};
|
||||
const enterEditMode = useCallback(
|
||||
(e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
setAreaName(area.name); // Reset to current name
|
||||
setEditMode(true);
|
||||
},
|
||||
[area.name]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (editMode) {
|
||||
// Small delay to ensure the input is rendered
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, 50); // Slightly longer delay to ensure DOM is ready
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [editMode]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full flex-col rounded-md border-2 shadow-sm',
|
||||
selected ? 'border-pink-600' : 'border-transparent'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: `${area.color}15`,
|
||||
borderColor: selected ? undefined : area.color,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
openAreaInEditor();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!readonly ? (
|
||||
<NodeResizer
|
||||
isVisible={focused}
|
||||
lineClassName="!border-4 !border-transparent"
|
||||
handleClassName="!h-[10px] !w-[10px] !rounded-full !bg-pink-600"
|
||||
minHeight={100}
|
||||
minWidth={100}
|
||||
/>
|
||||
) : null}
|
||||
<div className="group flex h-8 items-center justify-between rounded-t-md px-2">
|
||||
<div className="flex w-full items-center gap-1">
|
||||
<GripVertical className="size-4 shrink-0 text-slate-700 opacity-60 dark:text-slate-300" />
|
||||
<AreaNodeContextMenu area={area} onEditName={enterEditMode}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full flex-col rounded-md border-2 shadow-sm',
|
||||
selected ? 'border-pink-600' : 'border-transparent'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: `${area.color}15`,
|
||||
borderColor: selected ? undefined : area.color,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
openAreaInEditor();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!readonly ? (
|
||||
<NodeResizer
|
||||
isVisible={focused}
|
||||
lineClassName="!border-4 !border-transparent"
|
||||
handleClassName="!h-[18px] !w-[18px] !rounded-full !bg-pink-600"
|
||||
minHeight={100}
|
||||
minWidth={100}
|
||||
/>
|
||||
) : null}
|
||||
<div className="group flex h-8 items-center justify-between rounded-t-md px-2">
|
||||
<div className="flex w-full items-center gap-1">
|
||||
<GripVertical className="size-4 shrink-0 text-slate-700 opacity-60 dark:text-slate-300" />
|
||||
|
||||
{editMode && !readonly ? (
|
||||
<div className="flex w-full items-center">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={area.name}
|
||||
value={areaName}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) =>
|
||||
setAreaName(e.target.value)
|
||||
}
|
||||
className="h-6 bg-white/70 focus-visible:ring-0 dark:bg-slate-900/70"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="ml-1 size-6 p-0 hover:bg-white/20"
|
||||
onClick={editAreaName}
|
||||
>
|
||||
<Check className="size-3.5 text-slate-700 dark:text-slate-300" />
|
||||
</Button>
|
||||
</div>
|
||||
) : !readonly ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="text-editable max-w-[200px] cursor-text truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300"
|
||||
onDoubleClick={enterEditMode}
|
||||
{editMode && !readonly ? (
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder={area.name}
|
||||
value={areaName}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) =>
|
||||
setAreaName(e.target.value)
|
||||
}
|
||||
className="h-6 bg-white/70 focus-visible:ring-0 dark:bg-slate-900/70"
|
||||
style={{
|
||||
width: `${Math.max(
|
||||
areaName.length * 8 + 20,
|
||||
80
|
||||
)}px`,
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="ml-1 size-6 shrink-0 p-0 hover:bg-white/20"
|
||||
onClick={editAreaName}
|
||||
>
|
||||
{area.name}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t('tool_tips.double_click_to_edit')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300">
|
||||
{area.name}
|
||||
</div>
|
||||
)}
|
||||
<Check className="size-3.5 text-slate-700 dark:text-slate-300" />
|
||||
</Button>
|
||||
</div>
|
||||
) : !readonly ? (
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="text-editable cursor-text truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300"
|
||||
onDoubleClick={enterEditMode}
|
||||
>
|
||||
{area.name}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t(
|
||||
'tool_tips.double_click_to_edit'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="ml-1 size-5 shrink-0 p-0 opacity-0 transition-opacity hover:bg-white/20 group-hover:opacity-100"
|
||||
onClick={enterEditMode}
|
||||
>
|
||||
<Pencil className="size-3 text-slate-700 dark:text-slate-300" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300">
|
||||
{area.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
</AreaNodeContextMenu>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Input } from '@/components/input/input';
|
||||
import type { DBTable } from '@/lib/domain';
|
||||
import { FileType2, X } from 'lucide-react';
|
||||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { useClickOutside } from '@/hooks/use-click-outside';
|
||||
import { TableEditModeField } from './table-edit-mode-field';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
||||
@@ -33,6 +34,9 @@ export const TableEditMode: React.FC<TableEditModeProps> = React.memo(
|
||||
);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Handle click outside to close edit mode
|
||||
useClickOutside([containerRef], onClose, true, 200);
|
||||
|
||||
useEffect(() => {
|
||||
setFocusFieldId(focusFieldIdProp);
|
||||
if (!focusFieldIdProp) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useEditClickOutside } from '@/hooks/use-click-outside';
|
||||
import {
|
||||
Pencil,
|
||||
EllipsisVertical,
|
||||
@@ -11,7 +12,7 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
import { useReactFlow } from '@xyflow/react';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useFocusOn } from '@/hooks/use-focus-on';
|
||||
import { useClickAway, useKeyPressEvent } from 'react-use';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -42,29 +43,34 @@ export const RelationshipListItemHeader: React.FC<
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const editRelationshipName = useCallback(() => {
|
||||
if (!editMode) return;
|
||||
if (relationshipName.trim() && relationshipName !== relationship.name) {
|
||||
updateRelationship(relationship.id, {
|
||||
name: relationshipName.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
setEditMode(false);
|
||||
}, [
|
||||
relationshipName,
|
||||
relationship.id,
|
||||
updateRelationship,
|
||||
editMode,
|
||||
relationship.name,
|
||||
]);
|
||||
|
||||
useClickAway(inputRef, editRelationshipName);
|
||||
const abortEdit = useCallback(() => {
|
||||
setEditMode(false);
|
||||
setRelationshipName(relationship.name);
|
||||
}, [relationship.name]);
|
||||
|
||||
// Handle click outside to save and exit edit mode
|
||||
useEditClickOutside(inputRef, editMode, editRelationshipName);
|
||||
useKeyPressEvent('Enter', editRelationshipName);
|
||||
useKeyPressEvent('Escape', abortEdit);
|
||||
|
||||
const enterEditMode = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
setRelationshipName(relationship.name); // Reset to current name
|
||||
setEditMode(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useEditClickOutside } from '@/hooks/use-click-outside';
|
||||
import {
|
||||
CircleDotDashed,
|
||||
GripVertical,
|
||||
@@ -15,7 +16,7 @@ import { ListItemHeaderButton } from '@/pages/editor-page/side-panel/list-item-h
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { Input } from '@/components/input/input';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useClickAway, useKeyPressEvent } from 'react-use';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
import { useFocusOn } from '@/hooks/use-focus-on';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -61,26 +63,27 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
|
||||
const { openTableSchemaDialog } = useDialog();
|
||||
const { t } = useTranslation();
|
||||
const { focusOnTable } = useFocusOn();
|
||||
const { openedTableInSidebar } = useLayout();
|
||||
const [editMode, setEditMode] = React.useState(false);
|
||||
const [tableName, setTableName] = React.useState(table.name);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const { listeners } = useSortable({ id: table.id });
|
||||
|
||||
const editTableName = useCallback(() => {
|
||||
if (!editMode) return;
|
||||
if (tableName.trim()) {
|
||||
updateTable(table.id, { name: tableName.trim() });
|
||||
}
|
||||
|
||||
setEditMode(false);
|
||||
}, [tableName, table.id, updateTable, editMode]);
|
||||
}, [tableName, table.id, updateTable]);
|
||||
|
||||
const abortEdit = useCallback(() => {
|
||||
setEditMode(false);
|
||||
setTableName(table.name);
|
||||
}, [table.name]);
|
||||
|
||||
useClickAway(inputRef, editTableName);
|
||||
// Handle click outside to save and exit edit mode
|
||||
useEditClickOutside(inputRef, editMode, editTableName);
|
||||
|
||||
useKeyPressEvent('Enter', editTableName);
|
||||
useKeyPressEvent('Escape', abortEdit);
|
||||
|
||||
@@ -249,6 +252,23 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
|
||||
}
|
||||
}, [table.name]);
|
||||
|
||||
// Auto-select table name for newly created tables when opened in sidebar
|
||||
useEffect(() => {
|
||||
// Check if this is a new table that was just opened in the sidebar
|
||||
if (
|
||||
table.name.startsWith('table_') &&
|
||||
openedTableInSidebar === table.id
|
||||
) {
|
||||
setEditMode(true);
|
||||
// Select all text when input is ready
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [table.name, openedTableInSidebar, table.id]);
|
||||
|
||||
return (
|
||||
<div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">
|
||||
{!readonly ? (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useEditClickOutside } from '@/hooks/use-click-outside';
|
||||
import { Button } from '@/components/button/button';
|
||||
import { Check } from 'lucide-react';
|
||||
import { Check, Pencil } from 'lucide-react';
|
||||
import { Input } from '@/components/input/input';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useClickAway, useKeyPressEvent } from 'react-use';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import { DiagramIcon } from '@/components/diagram-icon/diagram-icon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -32,22 +33,39 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
|
||||
}, [diagramName]);
|
||||
|
||||
const editDiagramName = useCallback(() => {
|
||||
if (!editMode) return;
|
||||
if (editedDiagramName.trim()) {
|
||||
updateDiagramName(editedDiagramName.trim());
|
||||
}
|
||||
setEditMode(false);
|
||||
}, [editedDiagramName, updateDiagramName, editMode]);
|
||||
}, [editedDiagramName, updateDiagramName]);
|
||||
|
||||
// Handle click outside to save and exit edit mode
|
||||
useEditClickOutside(inputRef, editMode, editDiagramName);
|
||||
|
||||
useClickAway(inputRef, editDiagramName);
|
||||
useKeyPressEvent('Enter', editDiagramName);
|
||||
|
||||
const enterEditMode = (
|
||||
event: React.MouseEvent<HTMLHeadingElement, MouseEvent>
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
setEditMode(true);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (editMode) {
|
||||
// Small delay to ensure the input is rendered
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, 50); // Slightly longer delay to ensure DOM is ready
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [editMode]);
|
||||
|
||||
const enterEditMode = useCallback(
|
||||
(event: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
event.stopPropagation();
|
||||
setEditedDiagramName(diagramName);
|
||||
setEditMode(true);
|
||||
},
|
||||
[diagramName]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="group">
|
||||
@@ -69,7 +87,7 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
|
||||
/>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{editMode ? (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
autoFocus
|
||||
@@ -81,15 +99,21 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
|
||||
setEditedDiagramName(e.target.value)
|
||||
}
|
||||
className="ml-1 h-7 focus-visible:ring-0"
|
||||
style={{
|
||||
width: `${Math.max(
|
||||
editedDiagramName.length * 8 + 20,
|
||||
100
|
||||
)}px`,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300"
|
||||
className="ml-1 flex size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300"
|
||||
onClick={editDiagramName}
|
||||
>
|
||||
<Check />
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip>
|
||||
@@ -110,6 +134,13 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
|
||||
{t('tool_tips.double_click_to_edit')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="ml-1 size-5 p-0 opacity-0 transition-opacity hover:bg-primary-foreground group-hover:opacity-100"
|
||||
onClick={enterEditMode}
|
||||
>
|
||||
<Pencil className="size-3 text-slate-500 dark:text-slate-400" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user