diff --git a/src/components/tiptap-ui/blockquote-button/use-blockquote.ts b/src/components/tiptap-ui/blockquote-button/use-blockquote.ts index 1a892ef5..6e21708f 100644 --- a/src/components/tiptap-ui/blockquote-button/use-blockquote.ts +++ b/src/components/tiptap-ui/blockquote-button/use-blockquote.ts @@ -1,7 +1,8 @@ "use client" -import { useCallback, useEffect, useState } from "react" +import { useCallback } from "react" import type { Editor } from "@tiptap/react" +import { useEditorState } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" // --- Hooks --- @@ -231,25 +232,28 @@ export function useBlockquote(config?: UseBlockquoteConfig) { } = config || {} const { editor } = useTiptapEditor(providedEditor) - const [isVisible, setIsVisible] = useState(true) - const canToggle = canToggleBlockquote(editor) - const isActive = editor?.isActive("blockquote") || false - - useEffect(() => { - if (!editor) return - - const handleSelectionUpdate = () => { - setIsVisible(shouldShowButton({ editor, hideWhenUnavailable })) - } - - handleSelectionUpdate() - - editor.on("selectionUpdate", handleSelectionUpdate) - - return () => { - editor.off("selectionUpdate", handleSelectionUpdate) - } - }, [editor, hideWhenUnavailable]) + const toolbarState = useEditorState({ + editor, + selector: ({ editor: currentEditor }) => { + if (!currentEditor) { + return { + isVisible: true, + isActive: false, + canToggle: false, + } + } + + return { + isVisible: shouldShowButton({ + editor: currentEditor, + hideWhenUnavailable, + }), + isActive: currentEditor.isActive("blockquote"), + canToggle: canToggleBlockquote(currentEditor), + } + }, + })! + const { isVisible, isActive, canToggle } = toolbarState const handleToggle = useCallback(() => { if (!editor) return false diff --git a/src/components/tiptap-ui/code-block-button/use-code-block.ts b/src/components/tiptap-ui/code-block-button/use-code-block.ts index 8cbf401a..a0d7fe25 100644 --- a/src/components/tiptap-ui/code-block-button/use-code-block.ts +++ b/src/components/tiptap-ui/code-block-button/use-code-block.ts @@ -1,7 +1,8 @@ "use client" -import { useCallback, useEffect, useState } from "react" +import { useCallback } from "react" import { type Editor } from "@tiptap/react" +import { useEditorState } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" // --- Hooks --- @@ -241,25 +242,32 @@ export function useCodeBlock(config?: UseCodeBlockConfig) { } = config || {} const { editor } = useTiptapEditor(providedEditor) - const [isVisible, setIsVisible] = useState(true) - const canToggleState = canToggle(editor) - const isActive = editor?.isActive("codeBlock") || false - - useEffect(() => { - if (!editor) return - - const handleSelectionUpdate = () => { - setIsVisible(shouldShowButton({ editor, hideWhenUnavailable })) - } - - handleSelectionUpdate() - - editor.on("selectionUpdate", handleSelectionUpdate) - - return () => { - editor.off("selectionUpdate", handleSelectionUpdate) - } - }, [editor, hideWhenUnavailable]) + const toolbarState = useEditorState({ + editor, + selector: ({ editor: currentEditor }) => { + if (!currentEditor) { + return { + isVisible: true, + isActive: false, + canToggle: false, + } + } + + return { + isVisible: shouldShowButton({ + editor: currentEditor, + hideWhenUnavailable, + }), + isActive: currentEditor.isActive("codeBlock"), + canToggle: canToggle(currentEditor), + } + }, + })! + const { + isVisible, + isActive, + canToggle: canToggleState, + } = toolbarState const handleToggle = useCallback(() => { if (!editor) return false diff --git a/src/components/tiptap-ui/color-highlight-button/use-color-highlight.ts b/src/components/tiptap-ui/color-highlight-button/use-color-highlight.ts index e2cb5f0c..babf2fec 100644 --- a/src/components/tiptap-ui/color-highlight-button/use-color-highlight.ts +++ b/src/components/tiptap-ui/color-highlight-button/use-color-highlight.ts @@ -1,7 +1,8 @@ "use client" -import { useCallback, useEffect, useState } from "react" +import { useCallback } from "react" import { type Editor } from "@tiptap/react" +import { useEditorState } from "@tiptap/react" import { useHotkeys } from "react-hotkeys-hook" // --- Hooks --- @@ -293,28 +294,36 @@ export function useColorHighlight(config: UseColorHighlightConfig) { const { editor } = useTiptapEditor(providedEditor) const isMobile = useIsBreakpoint() - const [isVisible, setIsVisible] = useState(true) - const canColorHighlightState = canColorHighlight(editor, mode) const actualColor = highlightColor ? getHighlightColorValue(highlightColor, useColorValue) : highlightColor - const isActive = isColorHighlightActive(editor, actualColor, mode) - - useEffect(() => { - if (!editor) return - - const handleSelectionUpdate = () => { - setIsVisible(shouldShowButton({ editor, hideWhenUnavailable, mode })) - } - - handleSelectionUpdate() - - editor.on("selectionUpdate", handleSelectionUpdate) + const toolbarState = useEditorState({ + editor, + selector: ({ editor: currentEditor }) => { + if (!currentEditor) { + return { + isVisible: true, + isActive: false, + canColorHighlight: false, + } + } - return () => { - editor.off("selectionUpdate", handleSelectionUpdate) - } - }, [editor, hideWhenUnavailable, mode]) + return { + isVisible: shouldShowButton({ + editor: currentEditor, + hideWhenUnavailable, + mode, + }), + isActive: isColorHighlightActive(currentEditor, actualColor, mode), + canColorHighlight: canColorHighlight(currentEditor, mode), + } + }, + })! + const { + isVisible, + isActive, + canColorHighlight: canColorHighlightState, + } = toolbarState const handleColorHighlight = useCallback(() => { if (!editor || !canColorHighlightState || !actualColor || !label) diff --git a/src/components/tiptap-ui/heading-button/use-heading.ts b/src/components/tiptap-ui/heading-button/use-heading.ts index ba843f62..1ada1db5 100644 --- a/src/components/tiptap-ui/heading-button/use-heading.ts +++ b/src/components/tiptap-ui/heading-button/use-heading.ts @@ -1,7 +1,8 @@ "use client" -import { useCallback, useEffect, useState } from "react" +import { useCallback } from "react" import { type Editor } from "@tiptap/react" +import { useEditorState } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" // --- Hooks --- @@ -301,25 +302,33 @@ export function useHeading(config: UseHeadingConfig) { } = config const { editor } = useTiptapEditor(providedEditor) - const [isVisible, setIsVisible] = useState(true) - const canToggleState = canToggle(editor, level) - const isActive = isHeadingActive(editor, level) - - useEffect(() => { - if (!editor) return - - const handleSelectionUpdate = () => { - setIsVisible(shouldShowButton({ editor, level, hideWhenUnavailable })) - } - - handleSelectionUpdate() - - editor.on("selectionUpdate", handleSelectionUpdate) - - return () => { - editor.off("selectionUpdate", handleSelectionUpdate) - } - }, [editor, level, hideWhenUnavailable]) + const toolbarState = useEditorState({ + editor, + selector: ({ editor: currentEditor }) => { + if (!currentEditor) { + return { + isVisible: true, + isActive: false, + canToggle: false, + } + } + + return { + isVisible: shouldShowButton({ + editor: currentEditor, + level, + hideWhenUnavailable, + }), + isActive: isHeadingActive(currentEditor, level), + canToggle: canToggle(currentEditor, level), + } + }, + })! + const { + isVisible, + isActive, + canToggle: canToggleState, + } = toolbarState const handleToggle = useCallback(() => { if (!editor) return false diff --git a/src/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts b/src/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts index 5bd2db3d..a73e2866 100644 --- a/src/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +++ b/src/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts @@ -1,7 +1,7 @@ "use client" -import { useEffect, useState } from "react" import type { Editor } from "@tiptap/react" +import { useEditorState } from "@tiptap/react" // --- Hooks --- import { useTiptapEditor } from "@/hooks/use-tiptap-editor" @@ -96,29 +96,36 @@ export function useHeadingDropdownMenu(config?: UseHeadingDropdownMenuConfig) { } = config || {} const { editor } = useTiptapEditor(providedEditor) - const [isVisible, setIsVisible] = useState(true) + const toolbarState = useEditorState({ + editor, + selector: ({ editor: currentEditor }) => { + if (!currentEditor) { + return { + isVisible: true, + activeLevel: undefined, + isActive: false, + canToggle: false, + } + } - const activeLevel = getActiveHeadingLevel(editor, levels) - const isActive = isHeadingActive(editor) - const canToggleState = canToggle(editor) - - useEffect(() => { - if (!editor) return - - const handleSelectionUpdate = () => { - setIsVisible( - shouldShowButton({ editor, hideWhenUnavailable, level: levels }) - ) - } - - handleSelectionUpdate() - - editor.on("selectionUpdate", handleSelectionUpdate) - - return () => { - editor.off("selectionUpdate", handleSelectionUpdate) - } - }, [editor, hideWhenUnavailable, levels]) + return { + isVisible: shouldShowButton({ + editor: currentEditor, + hideWhenUnavailable, + level: levels, + }), + activeLevel: getActiveHeadingLevel(currentEditor, levels), + isActive: isHeadingActive(currentEditor, levels), + canToggle: canToggle(currentEditor), + } + }, + })! + const { + isVisible, + activeLevel, + isActive, + canToggle: canToggleState, + } = toolbarState return { isVisible, diff --git a/src/components/tiptap-ui/list-button/use-list.ts b/src/components/tiptap-ui/list-button/use-list.ts index 9007f9cd..2c2a1936 100644 --- a/src/components/tiptap-ui/list-button/use-list.ts +++ b/src/components/tiptap-ui/list-button/use-list.ts @@ -1,7 +1,8 @@ "use client" -import { useCallback, useEffect, useState } from "react" +import { useCallback } from "react" import { type Editor } from "@tiptap/react" +import { useEditorState } from "@tiptap/react" import { NodeSelection, TextSelection } from "@tiptap/pm/state" // --- Hooks --- @@ -309,25 +310,29 @@ export function useList(config: UseListConfig) { } = config const { editor } = useTiptapEditor(providedEditor) - const [isVisible, setIsVisible] = useState(true) - const canToggle = canToggleList(editor, type) - const isActive = isListActive(editor, type) - - useEffect(() => { - if (!editor) return - - const handleSelectionUpdate = () => { - setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable })) - } - - handleSelectionUpdate() - - editor.on("selectionUpdate", handleSelectionUpdate) + const toolbarState = useEditorState({ + editor, + selector: ({ editor: currentEditor }) => { + if (!currentEditor) { + return { + isVisible: true, + isActive: false, + canToggle: false, + } + } - return () => { - editor.off("selectionUpdate", handleSelectionUpdate) - } - }, [editor, type, hideWhenUnavailable]) + return { + isVisible: shouldShowButton({ + editor: currentEditor, + type, + hideWhenUnavailable, + }), + isActive: isListActive(currentEditor, type), + canToggle: canToggleList(currentEditor, type), + } + }, + })! + const { isVisible, isActive, canToggle } = toolbarState const handleToggle = useCallback(() => { if (!editor) return false diff --git a/src/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts b/src/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts index d9b9d73e..73d59730 100644 --- a/src/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +++ b/src/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts @@ -1,7 +1,8 @@ "use client" -import { useEffect, useMemo, useState } from "react" +import { useMemo } from "react" import type { Editor } from "@tiptap/react" +import { useEditorState } from "@tiptap/react" // --- Hooks --- import { useTiptapEditor } from "@/hooks/use-tiptap-editor" @@ -170,40 +171,45 @@ export function useListDropdownMenu(config?: UseListDropdownMenuConfig) { } = config || {} const { editor } = useTiptapEditor(providedEditor) - const [isVisible, setIsVisible] = useState(true) - - const listInSchema = types.some((type) => isNodeInSchema(type, editor)) - const filteredLists = useMemo(() => getFilteredListOptions(types), [types]) + const toolbarState = useEditorState({ + editor, + selector: ({ editor: currentEditor }) => { + if (!currentEditor) { + return { + isVisible: true, + canToggleAny: false, + isAnyActive: false, + activeType: undefined, + } + } + + const listInSchema = types.some((type) => + isNodeInSchema(type, currentEditor) + ) + const canToggleAny = canToggleAnyList(currentEditor, types) - const canToggleAny = canToggleAnyList(editor, types) - const isAnyActive = isAnyListActive(editor, types) - const activeType = getActiveListType(editor, types) - const activeList = filteredLists.find((option) => option.type === activeType) - - useEffect(() => { - if (!editor) return - - const handleSelectionUpdate = () => { - setIsVisible( - shouldShowListDropdown({ - editor, + return { + isVisible: shouldShowListDropdown({ + editor: currentEditor, listTypes: types, hideWhenUnavailable, listInSchema, canToggleAny, - }) - ) - } - - handleSelectionUpdate() - - editor.on("selectionUpdate", handleSelectionUpdate) - - return () => { - editor.off("selectionUpdate", handleSelectionUpdate) - } - }, [canToggleAny, editor, hideWhenUnavailable, listInSchema, types]) + }), + canToggleAny, + isAnyActive: isAnyListActive(currentEditor, types), + activeType: getActiveListType(currentEditor, types), + } + }, + })! + const { + isVisible, + canToggleAny, + isAnyActive, + activeType, + } = toolbarState + const activeList = filteredLists.find((option) => option.type === activeType) return { isVisible, diff --git a/src/components/tiptap-ui/mark-button/use-mark.ts b/src/components/tiptap-ui/mark-button/use-mark.ts index 64655e8d..30f3e323 100644 --- a/src/components/tiptap-ui/mark-button/use-mark.ts +++ b/src/components/tiptap-ui/mark-button/use-mark.ts @@ -1,7 +1,8 @@ "use client" -import { useCallback, useEffect, useState } from "react" +import { useCallback } from "react" import type { Editor } from "@tiptap/react" +import { useEditorState } from "@tiptap/react" // --- Hooks --- import { useTiptapEditor } from "@/hooks/use-tiptap-editor" @@ -173,25 +174,29 @@ export function useMark(config: UseMarkConfig) { } = config const { editor } = useTiptapEditor(providedEditor) - const [isVisible, setIsVisible] = useState(true) - const canToggle = canToggleMark(editor, type) - const isActive = isMarkActive(editor, type) - - useEffect(() => { - if (!editor) return - - const handleSelectionUpdate = () => { - setIsVisible(shouldShowButton({ editor, type, hideWhenUnavailable })) - } - - handleSelectionUpdate() - - editor.on("selectionUpdate", handleSelectionUpdate) - - return () => { - editor.off("selectionUpdate", handleSelectionUpdate) - } - }, [editor, type, hideWhenUnavailable]) + const toolbarState = useEditorState({ + editor, + selector: ({ editor: currentEditor }) => { + if (!currentEditor) { + return { + isVisible: true, + isActive: false, + canToggle: false, + } + } + + return { + isVisible: shouldShowButton({ + editor: currentEditor, + type, + hideWhenUnavailable, + }), + isActive: isMarkActive(currentEditor, type), + canToggle: canToggleMark(currentEditor, type), + } + }, + })! + const { isVisible, isActive, canToggle } = toolbarState const handleMark = useCallback(() => { if (!editor) return false diff --git a/src/components/tiptap-ui/text-align-button/use-text-align.ts b/src/components/tiptap-ui/text-align-button/use-text-align.ts index 05fd4f93..75de5310 100644 --- a/src/components/tiptap-ui/text-align-button/use-text-align.ts +++ b/src/components/tiptap-ui/text-align-button/use-text-align.ts @@ -1,8 +1,9 @@ "use client" -import { useCallback, useEffect, useState } from "react" +import { useCallback } from "react" import type { ChainedCommands } from "@tiptap/react" import { type Editor } from "@tiptap/react" +import { useEditorState } from "@tiptap/react" // --- Hooks --- import { useTiptapEditor } from "@/hooks/use-tiptap-editor" @@ -186,25 +187,29 @@ export function useTextAlign(config: UseTextAlignConfig) { } = config const { editor } = useTiptapEditor(providedEditor) - const [isVisible, setIsVisible] = useState(true) - const canAlign = canSetTextAlign(editor, align) - const isActive = isTextAlignActive(editor, align) - - useEffect(() => { - if (!editor) return - - const handleSelectionUpdate = () => { - setIsVisible(shouldShowButton({ editor, align, hideWhenUnavailable })) - } - - handleSelectionUpdate() - - editor.on("selectionUpdate", handleSelectionUpdate) - - return () => { - editor.off("selectionUpdate", handleSelectionUpdate) - } - }, [editor, hideWhenUnavailable, align]) + const toolbarState = useEditorState({ + editor, + selector: ({ editor: currentEditor }) => { + if (!currentEditor) { + return { + isVisible: true, + isActive: false, + canAlign: false, + } + } + + return { + isVisible: shouldShowButton({ + editor: currentEditor, + align, + hideWhenUnavailable, + }), + isActive: isTextAlignActive(currentEditor, align), + canAlign: canSetTextAlign(currentEditor, align), + } + }, + })! + const { isVisible, isActive, canAlign } = toolbarState const handleTextAlign = useCallback(() => { if (!editor) return false diff --git a/src/hooks/use-tiptap-editor.ts b/src/hooks/use-tiptap-editor.ts index 6ec37d26..5636e6f4 100644 --- a/src/hooks/use-tiptap-editor.ts +++ b/src/hooks/use-tiptap-editor.ts @@ -1,7 +1,7 @@ "use client" import type { Editor } from "@tiptap/react" -import { useCurrentEditor, useEditorState } from "@tiptap/react" +import { useCurrentEditor } from "@tiptap/react" import { useMemo } from "react" /** @@ -17,33 +17,12 @@ import { useMemo } from "react" */ export function useTiptapEditor(providedEditor?: Editor | null): { editor: Editor | null - editorState?: Editor["state"] - canCommand?: Editor["can"] } { const { editor: coreEditor } = useCurrentEditor() - const mainEditor = useMemo( + const editor = useMemo( () => providedEditor || coreEditor, [providedEditor, coreEditor] ) - const editorState = useEditorState({ - editor: mainEditor, - selector(context) { - if (!context.editor) { - return { - editor: null, - editorState: undefined, - canCommand: undefined, - } - } - - return { - editor: context.editor, - editorState: context.editor.state, - canCommand: context.editor.can, - } - }, - }) - - return editorState || { editor: null } + return { editor: editor || null } }