summaryrefslogblamecommitdiff
path: root/frontend/src/components/document/AutoSavePlugin.tsx
blob: d3d0eb5e5c770db63f1f0db69fc6e1462d53d116 (plain) (tree)











































































































































                                                                                                       
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<number | null>(null);
  const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const startTimeRef = useRef<number>(0);
  const pendingContentRef = useRef<string>('');
  const lastSavedContentRef = useRef<string>('');

  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 (
    <div className="autosave-bar">
      <span className="autosave-bar-text">
        Saving in {secondsLeft}s... <kbd>Esc</kbd> to cancel
      </span>
      <div className="autosave-bar-progress-track">
        <div
          className="autosave-bar-progress-fill"
          style={{ width: `${progressPercent}%` }}
        />
      </div>
      <button
        className="autosave-bar-cancel"
        onClick={cancelCountdown}
        type="button"
      >
        Cancel
      </button>
    </div>
  );
}