import { useEffect, useRef, useState, useCallback } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { UNDO_COMMAND } from 'lexical'; const COUNTDOWN_DURATION_MS = 3000; const TICK_INTERVAL_MS = 50; interface AutoSavePluginProps { onAutoSave: (content: string) => void; getContent: () => string; enabled?: boolean; } export default function AutoSavePlugin({ onAutoSave, getContent, enabled = true, }: AutoSavePluginProps) { const [editor] = useLexicalComposerContext(); const [countdown, setCountdown] = useState(null); const timerRef = useRef | null>(null); const startTimeRef = useRef(0); const pendingContentRef = useRef(''); const lastSavedContentRef = useRef(''); const clearTimer = useCallback(() => { if (timerRef.current !== null) { clearInterval(timerRef.current); timerRef.current = null; } setCountdown(null); }, []); const cancelCountdown = useCallback(() => { clearTimer(); }, [clearTimer]); const startCountdown = useCallback( (content: string) => { pendingContentRef.current = content; clearTimer(); startTimeRef.current = Date.now(); setCountdown(COUNTDOWN_DURATION_MS); timerRef.current = setInterval(() => { const elapsed = Date.now() - startTimeRef.current; const remaining = COUNTDOWN_DURATION_MS - elapsed; if (remaining <= 0) { clearTimer(); lastSavedContentRef.current = pendingContentRef.current; onAutoSave(pendingContentRef.current); } else { setCountdown(remaining); } }, TICK_INTERVAL_MS); }, [clearTimer, onAutoSave] ); // Listen for editor updates (content changes) useEffect(() => { if (!enabled) return; const unregister = editor.registerUpdateListener(({ editorState, dirtyElements, dirtyLeaves }) => { // Only trigger on actual content changes if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return; const content = getContent(); if (content !== lastSavedContentRef.current) { startCountdown(content); } }); return unregister; }, [editor, enabled, getContent, startCountdown]); // Listen for undo command to cancel countdown useEffect(() => { const unregister = editor.registerCommand( UNDO_COMMAND, () => { cancelCountdown(); return false; // Don't prevent the undo from executing }, 1 // COMMAND_PRIORITY_LOW ); return unregister; }, [editor, cancelCountdown]); // Listen for Escape key to cancel countdown useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && countdown !== null) { e.preventDefault(); cancelCountdown(); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [countdown, cancelCountdown]); // Cleanup on unmount useEffect(() => { return () => { if (timerRef.current !== null) { clearInterval(timerRef.current); } }; }, []); if (countdown === null) return null; const progressPercent = (countdown / COUNTDOWN_DURATION_MS) * 100; const secondsLeft = Math.ceil(countdown / 1000); return (
Saving in {secondsLeft}s... Esc to cancel
); }