Skip to content

Commit 2175e45

Browse files
committed
fix: improve title input UX and toolbar performance
1 parent b741057 commit 2175e45

File tree

2 files changed

+81
-15
lines changed

2 files changed

+81
-15
lines changed

src/components/editor/Editor/Toolbar.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type Editor } from '@tiptap/react';
2+
import { memo, useState, useEffect } from 'react';
23
import { ImageUpload } from '../extensions/ImageUpload';
34
import {
45
Bold,
@@ -42,7 +43,25 @@ interface ToolbarProps {
4243
editor: Editor | null;
4344
}
4445

45-
export function Toolbar({ editor }: ToolbarProps) {
46+
function ToolbarComponent({ editor }: ToolbarProps) {
47+
const [, forceUpdate] = useState({});
48+
49+
useEffect(() => {
50+
if (!editor) return;
51+
52+
const handleSelectionUpdate = () => {
53+
forceUpdate({});
54+
};
55+
56+
editor.on('selectionUpdate', handleSelectionUpdate);
57+
editor.on('transaction', handleSelectionUpdate);
58+
59+
return () => {
60+
editor.off('selectionUpdate', handleSelectionUpdate);
61+
editor.off('transaction', handleSelectionUpdate);
62+
};
63+
}, [editor]);
64+
4665
if (!editor) {
4766
return null;
4867
}
@@ -630,3 +649,5 @@ export function Toolbar({ editor }: ToolbarProps) {
630649
</div>
631650
);
632651
}
652+
653+
export const Toolbar = memo(ToolbarComponent);

src/components/editor/index.tsx

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useCallback, useEffect } from 'react';
1+
import { useState, useCallback, useEffect, useRef } from 'react';
22
import {
33
Star,
44
Archive,
@@ -107,6 +107,9 @@ export default function Index({
107107
const [isUploading, setIsUploading] = useState(false);
108108
const [uploadProgress, setUploadProgress] = useState(0);
109109
const [deletingIds, setDeletingIds] = useState<string[]>([]);
110+
const [localTitle, setLocalTitle] = useState(note?.title || '');
111+
const titleTimeoutRef = useRef<NodeJS.Timeout | null>(null);
112+
const isEditingTitleRef = useRef(false);
110113

111114
// Use custom hook for editor state management
112115
const {
@@ -355,33 +358,74 @@ export default function Index({
355358
}
356359
}, [editor, note]);
357360

358-
// Cleanup timeout on unmount
361+
// Cleanup timeouts on unmount
359362
useEffect(() => {
360363
return () => {
361364
if (saveTimeoutRef.current) {
362365
clearTimeout(saveTimeoutRef.current);
363366
}
367+
if (titleTimeoutRef.current) {
368+
clearTimeout(titleTimeoutRef.current);
369+
}
364370
};
365371
}, [saveTimeoutRef]);
366372

373+
// Sync local title when note changes, but not when actively editing
374+
useEffect(() => {
375+
if (!isEditingTitleRef.current) {
376+
setLocalTitle(note?.title || '');
377+
}
378+
// Reset editing state when switching notes
379+
if (note?.id) {
380+
isEditingTitleRef.current = false;
381+
}
382+
}, [note?.id, note?.title]);
383+
384+
const saveTitleToServer = useCallback((title: string) => {
385+
if (!note || title === note.title) return;
386+
387+
setSaveStatus('saving');
388+
try {
389+
onUpdateNote(note.id, { title });
390+
setTimeout(() => {
391+
setSaveStatus('saved');
392+
isEditingTitleRef.current = false; // Mark editing as complete
393+
}, 500);
394+
} catch (error) {
395+
setSaveStatus('error');
396+
console.error('Failed to save title:', error);
397+
isEditingTitleRef.current = false; // Mark editing as complete even on error
398+
}
399+
}, [note, onUpdateNote, setSaveStatus]);
400+
367401
const handleTitleChange = useCallback(
368402
(e: React.ChangeEvent<HTMLInputElement>) => {
369-
if (!note) return;
370403
const newTitle = e.target.value;
371-
setSaveStatus('saving');
372-
try {
373-
onUpdateNote(note.id, { title: newTitle });
374-
setTimeout(() => {
375-
setSaveStatus('saved');
376-
}, 500);
377-
} catch (error) {
378-
setSaveStatus('error');
379-
console.error('Failed to save title:', error);
404+
setLocalTitle(newTitle);
405+
isEditingTitleRef.current = true; // Mark as actively editing
406+
407+
// Clear existing timeout
408+
if (titleTimeoutRef.current) {
409+
clearTimeout(titleTimeoutRef.current);
380410
}
411+
412+
// Debounce the save operation
413+
titleTimeoutRef.current = setTimeout(() => {
414+
saveTitleToServer(newTitle);
415+
}, 1000); // Save after 1 second of no typing
381416
},
382-
[note, onUpdateNote, setSaveStatus]
417+
[saveTitleToServer]
383418
);
384419

420+
const handleTitleBlur = useCallback(() => {
421+
// Clear timeout and save immediately on blur
422+
if (titleTimeoutRef.current) {
423+
clearTimeout(titleTimeoutRef.current);
424+
titleTimeoutRef.current = null;
425+
}
426+
saveTitleToServer(localTitle);
427+
}, [localTitle, saveTitleToServer]);
428+
385429
const handleMoveNote = useCallback(
386430
(targetFolderId: string | null) => {
387431
if (note) {
@@ -575,8 +619,9 @@ export default function Index({
575619
)}
576620
<input
577621
type="text"
578-
value={note.title || ''}
622+
value={localTitle}
579623
onChange={handleTitleChange}
624+
onBlur={handleTitleBlur}
580625
className="text-foreground placeholder-muted-foreground min-w-0 flex-1 border-none bg-transparent text-2xl font-bold outline-none"
581626
placeholder="Untitled Note"
582627
disabled={note.hidden}

0 commit comments

Comments
 (0)