"use client" import * as React from "react" import { useHotkeys } from "react-hotkeys-hook" import { type Editor } from "@tiptap/react" // --- Hooks --- import { useTiptapEditor } from "@/hooks/use-tiptap-editor" import { useIsMobile } from "@/hooks/use-mobile" // --- Lib --- import { isNodeTypeSelected } from "@/lib/tiptap-utils" // --- Icons --- import { Redo2Icon } from "@/components/tiptap-icons/redo2-icon" import { Undo2Icon } from "@/components/tiptap-icons/undo2-icon" export type UndoRedoAction = "undo" | "redo" /** * Configuration for the history functionality */ export interface UseUndoRedoConfig { /** * The Tiptap editor instance. */ editor?: Editor | null /** * The history action to perform (undo or redo). */ action: UndoRedoAction /** * Whether the button should hide when action is not available. * @default false */ hideWhenUnavailable?: boolean /** * Callback function called after a successful action execution. */ onExecuted?: () => void } export const UNDO_REDO_SHORTCUT_KEYS: Record = { undo: "mod+z", redo: "mod+shift+z", } export const historyActionLabels: Record = { undo: "Undo", redo: "Redo", } export const historyIcons = { undo: Undo2Icon, redo: Redo2Icon, } /** * Checks if a history action can be executed */ export function canExecuteUndoRedoAction( editor: Editor | null, action: UndoRedoAction ): boolean { if (!editor || !editor.isEditable) return false if (isNodeTypeSelected(editor, ["image"])) return false return action === "undo" ? editor.can().undo() : editor.can().redo() } /** * Executes a history action on the editor */ export function executeUndoRedoAction( editor: Editor | null, action: UndoRedoAction ): boolean { if (!editor || !editor.isEditable) return false if (!canExecuteUndoRedoAction(editor, action)) return false const chain = editor.chain().focus() return action === "undo" ? chain.undo().run() : chain.redo().run() } /** * Determines if the history button should be shown */ export function shouldShowButton(props: { editor: Editor | null hideWhenUnavailable: boolean action: UndoRedoAction }): boolean { const { editor, hideWhenUnavailable, action } = props if (!editor || !editor.isEditable) return false if (hideWhenUnavailable && !editor.isActive("code")) { return canExecuteUndoRedoAction(editor, action) } return true } /** * Custom hook that provides history functionality for Tiptap editor * * @example * ```tsx * // Simple usage * function MySimpleUndoButton() { * const { isVisible, handleAction } = useHistory({ action: "undo" }) * * if (!isVisible) return null * * return * } * * // Advanced usage with configuration * function MyAdvancedRedoButton() { * const { isVisible, handleAction, label } = useHistory({ * editor: myEditor, * action: "redo", * hideWhenUnavailable: true, * onExecuted: () => console.log('Action executed!') * }) * * if (!isVisible) return null * * return ( * * Redo * * ) * } * ``` */ export function useUndoRedo(config: UseUndoRedoConfig) { const { editor: providedEditor, action, hideWhenUnavailable = false, onExecuted, } = config const { editor } = useTiptapEditor(providedEditor) const isMobile = useIsMobile() const [isVisible, setIsVisible] = React.useState(true) const canExecute = canExecuteUndoRedoAction(editor, action) React.useEffect(() => { if (!editor) return const handleUpdate = () => { setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, action })) } handleUpdate() editor.on("transaction", handleUpdate) return () => { editor.off("transaction", handleUpdate) } }, [editor, hideWhenUnavailable, action]) const handleAction = React.useCallback(() => { if (!editor) return false const success = executeUndoRedoAction(editor, action) if (success) { onExecuted?.() } return success }, [editor, action, onExecuted]) useHotkeys( UNDO_REDO_SHORTCUT_KEYS[action], (event) => { event.preventDefault() handleAction() }, { enabled: isVisible && canExecute, enableOnContentEditable: !isMobile, enableOnFormTags: true, } ) return { isVisible, handleAction, canExecute, label: historyActionLabels[action], shortcutKeys: UNDO_REDO_SHORTCUT_KEYS[action], Icon: historyIcons[action], } }