diff --git a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx index 3b7692f4..d28eae98 100644 --- a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx @@ -3,6 +3,7 @@ import { BubbleMenuProps, isNodeSelection, useEditor, + useEditorState, } from "@tiptap/react"; import { FC, useEffect, useRef, useState } from "react"; import { @@ -50,34 +51,52 @@ export const EditorBubbleMenu: FC = (props) => { showCommentPopupRef.current = showCommentPopup; }, [showCommentPopup]); + const editorState = useEditorState({ + editor: props.editor, + selector: (ctx) => { + if (!props.editor) { + return null; + } + + return { + isBold: ctx.editor.isActive("bold"), + isItalic: ctx.editor.isActive("italic"), + isUnderline: ctx.editor.isActive("underline"), + isStrike: ctx.editor.isActive("strike"), + isCode: ctx.editor.isActive("code"), + isComment: ctx.editor.isActive("comment"), + }; + }, + }); + const items: BubbleMenuItem[] = [ { name: "Bold", - isActive: () => props.editor.isActive("bold"), + isActive: () => editorState?.isBold, command: () => props.editor.chain().focus().toggleBold().run(), icon: IconBold, }, { name: "Italic", - isActive: () => props.editor.isActive("italic"), + isActive: () => editorState?.isItalic, command: () => props.editor.chain().focus().toggleItalic().run(), icon: IconItalic, }, { name: "Underline", - isActive: () => props.editor.isActive("underline"), + isActive: () => editorState?.isUnderline, command: () => props.editor.chain().focus().toggleUnderline().run(), icon: IconUnderline, }, { name: "Strike", - isActive: () => props.editor.isActive("strike"), + isActive: () => editorState?.isStrike, command: () => props.editor.chain().focus().toggleStrike().run(), icon: IconStrikethrough, }, { name: "Code", - isActive: () => props.editor.isActive("code"), + isActive: () => editorState?.isCode, command: () => props.editor.chain().focus().toggleCode().run(), icon: IconCode, }, @@ -85,7 +104,7 @@ export const EditorBubbleMenu: FC = (props) => { const commentItem: BubbleMenuItem = { name: "Comment", - isActive: () => props.editor.isActive("comment"), + isActive: () => editorState?.isComment, command: () => { const commentId = uuid7(); diff --git a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx index 1148e0f4..a59eb8e4 100644 --- a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx @@ -9,7 +9,8 @@ import { Text, Tooltip, } from "@mantine/core"; -import { useEditor } from "@tiptap/react"; +import type { Editor } from "@tiptap/react"; +import { useEditorState } from "@tiptap/react"; import { useTranslation } from "react-i18next"; export interface BubbleColorMenuItem { @@ -18,7 +19,7 @@ export interface BubbleColorMenuItem { } interface ColorSelectorProps { - editor: ReturnType; + editor: Editor | null; isOpen: boolean; setIsOpen: Dispatch>; } @@ -108,12 +109,36 @@ export const ColorSelector: FC = ({ setIsOpen, }) => { const { t } = useTranslation(); + + const editorState = useEditorState({ + editor, + selector: ctx => { + if (!ctx.editor) { + return null; + } + + const activeColors: Record = {}; + TEXT_COLORS.forEach(({ color }) => { + activeColors[`text_${color}`] = ctx.editor.isActive("textStyle", { color }); + }); + HIGHLIGHT_COLORS.forEach(({ color }) => { + activeColors[`highlight_${color}`] = ctx.editor.isActive("highlight", { color }); + }); + + return activeColors; + }, + }); + + if (!editor || !editorState) { + return null; + } + const activeColorItem = TEXT_COLORS.find(({ color }) => - editor.isActive("textStyle", { color }), + editorState[`text_${color}`] ); const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) => - editor.isActive("highlight", { color }), + editorState[`highlight_${color}`] ); return ( @@ -151,7 +176,7 @@ export const ColorSelector: FC = ({ justify="left" fullWidth rightSection={ - editor.isActive("textStyle", { color }) && ( + editorState[`text_${color}`] && ( ) } diff --git a/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx index bc2eb702..13b2117f 100644 --- a/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx @@ -13,11 +13,12 @@ import { IconTypography, } from "@tabler/icons-react"; import { Popover, Button, ScrollArea } from "@mantine/core"; -import { useEditor } from "@tiptap/react"; +import type { Editor } from "@tiptap/react"; +import { useEditorState } from "@tiptap/react"; import { useTranslation } from "react-i18next"; interface NodeSelectorProps { - editor: ReturnType; + editor: Editor | null; isOpen: boolean; setIsOpen: Dispatch>; } @@ -36,6 +37,27 @@ export const NodeSelector: FC = ({ }) => { const { t } = useTranslation(); + const editorState = useEditorState({ + editor, + selector: (ctx) => { + if (!editor) { + return null; + } + + return { + isParagraph: ctx.editor.isActive("paragraph"), + isBulletList: ctx.editor.isActive("bulletList"), + isOrderedList: ctx.editor.isActive("orderedList"), + isHeading1: ctx.editor.isActive("heading", { level: 1 }), + isHeading2: ctx.editor.isActive("heading", { level: 2 }), + isHeading3: ctx.editor.isActive("heading", { level: 3 }), + isTaskItem: ctx.editor.isActive("taskItem"), + isBlockquote: ctx.editor.isActive("blockquote"), + isCodeBlock: ctx.editor.isActive("codeBlock"), + }; + }, + }); + const items: BubbleMenuItem[] = [ { name: "Text", @@ -43,45 +65,45 @@ export const NodeSelector: FC = ({ command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(), isActive: () => - editor.isActive("paragraph") && - !editor.isActive("bulletList") && - !editor.isActive("orderedList"), + editorState?.isParagraph && + !editorState?.isBulletList && + !editorState?.isOrderedList, }, { name: "Heading 1", icon: IconH1, command: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), - isActive: () => editor.isActive("heading", { level: 1 }), + isActive: () => editorState?.isHeading1, }, { name: "Heading 2", icon: IconH2, command: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), - isActive: () => editor.isActive("heading", { level: 2 }), + isActive: () => editorState?.isHeading2, }, { name: "Heading 3", icon: IconH3, command: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), - isActive: () => editor.isActive("heading", { level: 3 }), + isActive: () => editorState?.isHeading3, }, { name: "To-do List", icon: IconCheckbox, command: () => editor.chain().focus().toggleTaskList().run(), - isActive: () => editor.isActive("taskItem"), + isActive: () => editorState?.isTaskItem, }, { name: "Bullet List", icon: IconList, command: () => editor.chain().focus().toggleBulletList().run(), - isActive: () => editor.isActive("bulletList"), + isActive: () => editorState?.isBulletList, }, { name: "Numbered List", icon: IconListNumbers, command: () => editor.chain().focus().toggleOrderedList().run(), - isActive: () => editor.isActive("orderedList"), + isActive: () => editorState?.isOrderedList, }, { name: "Blockquote", @@ -93,13 +115,13 @@ export const NodeSelector: FC = ({ .toggleNode("paragraph", "paragraph") .toggleBlockquote() .run(), - isActive: () => editor.isActive("blockquote"), + isActive: () => editorState?.isBlockquote, }, { name: "Code", icon: IconCode, command: () => editor.chain().focus().toggleCodeBlock().run(), - isActive: () => editor.isActive("codeBlock"), + isActive: () => editorState?.isCodeBlock, }, ]; diff --git a/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx index 8330684b..b5277651 100644 --- a/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx @@ -8,11 +8,12 @@ import { IconChevronDown, } from "@tabler/icons-react"; import { Popover, Button, ScrollArea, rem } from "@mantine/core"; -import { useEditor } from "@tiptap/react"; +import type { Editor } from "@tiptap/react"; +import { useEditorState } from "@tiptap/react"; import { useTranslation } from "react-i18next"; interface TextAlignmentProps { - editor: ReturnType; + editor: Editor | null; isOpen: boolean; setIsOpen: Dispatch>; } @@ -31,36 +32,54 @@ export const TextAlignmentSelector: FC = ({ }) => { const { t } = useTranslation(); + const editorState = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) { + return null; + } + + return { + isAlignLeft: ctx.editor.isActive({ textAlign: "left" }), + isAlignCenter: ctx.editor.isActive({ textAlign: "center" }), + isAlignRight: ctx.editor.isActive({ textAlign: "right" }), + isAlignJustify: ctx.editor.isActive({ textAlign: "justify" }), + }; + }, + }); + + if (!editor || !editorState) { + return null; + } + const items: BubbleMenuItem[] = [ { name: "Align left", - isActive: () => editor.isActive({ textAlign: "left" }), + isActive: () => editorState?.isAlignLeft, command: () => editor.chain().focus().setTextAlign("left").run(), icon: IconAlignLeft, }, { name: "Align center", - isActive: () => editor.isActive({ textAlign: "center" }), + isActive: () => editorState?.isAlignCenter, command: () => editor.chain().focus().setTextAlign("center").run(), icon: IconAlignCenter, }, { name: "Align right", - isActive: () => editor.isActive({ textAlign: "right" }), + isActive: () => editorState?.isAlignRight, command: () => editor.chain().focus().setTextAlign("right").run(), icon: IconAlignRight, }, { name: "Justify", - isActive: () => editor.isActive({ textAlign: "justify" }), + isActive: () => editorState?.isAlignJustify, command: () => editor.chain().focus().setTextAlign("justify").run(), icon: IconAlignJustified, }, ]; - const activeItem = items.filter((item) => item.isActive()).pop() ?? { - name: "Multiple", - }; + const activeItem = items.filter((item) => item.isActive()).pop() ?? items[0]; return ( @@ -73,7 +92,7 @@ export const TextAlignmentSelector: FC = ({ rightSection={} onClick={() => setIsOpen(!isOpen)} > - + diff --git a/apps/client/src/features/editor/components/callout/callout-menu.tsx b/apps/client/src/features/editor/components/callout/callout-menu.tsx index 988f214a..c0485614 100644 --- a/apps/client/src/features/editor/components/callout/callout-menu.tsx +++ b/apps/client/src/features/editor/components/callout/callout-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu, findParentNode, posToDOMRect, + useEditorState, } from "@tiptap/react"; import React, { useCallback } from "react"; import { Node as PMNode } from "prosemirror-model"; @@ -9,7 +10,7 @@ import { EditorMenuProps, ShouldShowProps, } from "@/features/editor/components/table/types/types.ts"; -import { ActionIcon, Tooltip, Divider } from "@mantine/core"; +import { ActionIcon, Tooltip } from "@mantine/core"; import { IconAlertTriangleFilled, IconCircleCheckFilled, @@ -35,6 +36,23 @@ export function CalloutMenu({ editor }: EditorMenuProps) { [editor], ); + const editorState = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) { + return null; + } + + return { + isCallout: ctx.editor.isActive("callout"), + isInfo: ctx.editor.isActive("callout", { type: "info" }), + isSuccess: ctx.editor.isActive("callout", { type: "success" }), + isWarning: ctx.editor.isActive("callout", { type: "warning" }), + isDanger: ctx.editor.isActive("callout", { type: "danger" }), + }; + }, + }); + const getReferenceClientRect = useCallback(() => { const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "callout"; @@ -92,7 +110,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) { return ( setCalloutType("info")} size="lg" aria-label={t("Info")} - variant={ - editor.isActive("callout", { type: "info" }) ? "light" : "default" - } + variant={editorState?.isInfo ? "light" : "default"} > @@ -124,11 +140,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) { onClick={() => setCalloutType("success")} size="lg" aria-label={t("Success")} - variant={ - editor.isActive("callout", { type: "success" }) - ? "light" - : "default" - } + variant={editorState?.isSuccess ? "light" : "default"} > @@ -139,11 +151,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) { onClick={() => setCalloutType("warning")} size="lg" aria-label={t("Warning")} - variant={ - editor.isActive("callout", { type: "warning" }) - ? "light" - : "default" - } + variant={editorState?.isWarning ? "light" : "default"} > @@ -154,11 +162,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) { onClick={() => setCalloutType("danger")} size="lg" aria-label={t("Danger")} - variant={ - editor.isActive("callout", { type: "danger" }) - ? "light" - : "default" - } + variant={editorState?.isDanger ? "light" : "default"} > diff --git a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx index 76771b10..0efc2ec0 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx @@ -2,15 +2,16 @@ import { BubbleMenu as BaseBubbleMenu, findParentNode, posToDOMRect, -} from '@tiptap/react'; -import { useCallback } from 'react'; -import { sticky } from 'tippy.js'; -import { Node as PMNode } from 'prosemirror-model'; + useEditorState, +} from "@tiptap/react"; +import { useCallback } from "react"; +import { sticky } from "tippy.js"; +import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, ShouldShowProps, -} from '@/features/editor/components/table/types/types.ts'; -import { NodeWidthResize } from '@/features/editor/components/common/node-width-resize.tsx'; +} from "@/features/editor/components/table/types/types.ts"; +import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx"; export function DrawioMenu({ editor }: EditorMenuProps) { const shouldShow = useCallback( @@ -19,14 +20,29 @@ export function DrawioMenu({ editor }: EditorMenuProps) { return false; } - return editor.isActive('drawio') && editor.getAttributes('drawio')?.src; + return editor.isActive("drawio") && editor.getAttributes("drawio")?.src; }, - [editor] + [editor], ); + const editorState = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) { + return null; + } + + const drawioAttr = ctx.editor.getAttributes("drawio"); + return { + isDrawio: ctx.editor.isActive("drawio"), + width: drawioAttr?.width ? parseInt(drawioAttr.width) : null, + }; + }, + }); + const getReferenceClientRect = useCallback(() => { const { selection } = editor.state; - const predicate = (node: PMNode) => node.type.name === 'drawio'; + const predicate = (node: PMNode) => node.type.name === "drawio"; const parent = findParentNode(predicate)(selection); if (parent) { @@ -39,40 +55,37 @@ export function DrawioMenu({ editor }: EditorMenuProps) { const onWidthChange = useCallback( (value: number) => { - editor.commands.updateAttributes('drawio', { width: `${value}%` }); + editor.commands.updateAttributes("drawio", { width: `${value}%` }); }, - [editor] + [editor], ); return (
- {editor.getAttributes('drawio')?.width && ( - + {editorState?.width && ( + )}
diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx index 5672e4f8..42329e5c 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx @@ -2,15 +2,16 @@ import { BubbleMenu as BaseBubbleMenu, findParentNode, posToDOMRect, -} from '@tiptap/react'; -import { useCallback } from 'react'; -import { sticky } from 'tippy.js'; -import { Node as PMNode } from 'prosemirror-model'; + useEditorState, +} from "@tiptap/react"; +import { useCallback } from "react"; +import { sticky } from "tippy.js"; +import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, ShouldShowProps, -} from '@/features/editor/components/table/types/types.ts'; -import { NodeWidthResize } from '@/features/editor/components/common/node-width-resize.tsx'; +} from "@/features/editor/components/table/types/types.ts"; +import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx"; export function ExcalidrawMenu({ editor }: EditorMenuProps) { const shouldShow = useCallback( @@ -19,14 +20,31 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { return false; } - return editor.isActive('excalidraw') && editor.getAttributes('excalidraw')?.src; + return ( + editor.isActive("excalidraw") && editor.getAttributes("excalidraw")?.src + ); }, - [editor] + [editor], ); + const editorState = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) { + return null; + } + + const excalidrawAttr = ctx.editor.getAttributes("excalidraw"); + return { + isExcalidraw: ctx.editor.isActive("excalidraw"), + width: excalidrawAttr?.width ? parseInt(excalidrawAttr.width) : null, + }; + }, + }); + const getReferenceClientRect = useCallback(() => { const { selection } = editor.state; - const predicate = (node: PMNode) => node.type.name === 'excalidraw'; + const predicate = (node: PMNode) => node.type.name === "excalidraw"; const parent = findParentNode(predicate)(selection); if (parent) { @@ -39,9 +57,9 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { const onWidthChange = useCallback( (value: number) => { - editor.commands.updateAttributes('excalidraw', { width: `${value}%` }); + editor.commands.updateAttributes("excalidraw", { width: `${value}%` }); }, - [editor] + [editor], ); return ( @@ -54,25 +72,22 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { offset: [0, 8], zIndex: 99, popperOptions: { - modifiers: [{ name: 'flip', enabled: false }], + modifiers: [{ name: "flip", enabled: false }], }, plugins: [sticky], - sticky: 'popper', + sticky: "popper", }} shouldShow={shouldShow} >
- {editor.getAttributes('excalidraw')?.width && ( - + {editorState?.width && ( + )}
diff --git a/apps/client/src/features/editor/components/image/image-menu.tsx b/apps/client/src/features/editor/components/image/image-menu.tsx index abb1c1ca..723ec299 100644 --- a/apps/client/src/features/editor/components/image/image-menu.tsx +++ b/apps/client/src/features/editor/components/image/image-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu, findParentNode, posToDOMRect, + useEditorState, } from "@tiptap/react"; import React, { useCallback } from "react"; import { sticky } from "tippy.js"; @@ -32,6 +33,25 @@ export function ImageMenu({ editor }: EditorMenuProps) { [editor], ); + const editorState = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) { + return null; + } + + const imageAttrs = ctx.editor.getAttributes("image"); + + return { + isImage: ctx.editor.isActive("image"), + isAlignLeft: ctx.editor.isActive("image", { align: "left" }), + isAlignCenter: ctx.editor.isActive("image", { align: "center" }), + isAlignRight: ctx.editor.isActive("image", { align: "right" }), + width: imageAttrs?.width ? parseInt(imageAttrs.width) : null, + }; + }, + }); + const getReferenceClientRect = useCallback(() => { const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "image"; @@ -83,7 +103,7 @@ export function ImageMenu({ editor }: EditorMenuProps) { return ( @@ -116,11 +134,7 @@ export function ImageMenu({ editor }: EditorMenuProps) { onClick={alignImageCenter} size="lg" aria-label={t("Align center")} - variant={ - editor.isActive("image", { align: "center" }) - ? "light" - : "default" - } + variant={editorState?.isAlignCenter ? "light" : "default"} > @@ -131,20 +145,15 @@ export function ImageMenu({ editor }: EditorMenuProps) { onClick={alignImageRight} size="lg" aria-label={t("Align right")} - variant={ - editor.isActive("image", { align: "right" }) ? "light" : "default" - } + variant={editorState?.isAlignRight ? "light" : "default"} > - {editor.getAttributes("image")?.width && ( - + {editorState?.width && ( + )} ); diff --git a/apps/client/src/features/editor/components/link/link-menu.tsx b/apps/client/src/features/editor/components/link/link-menu.tsx index 7cdd2f0f..69f7c449 100644 --- a/apps/client/src/features/editor/components/link/link-menu.tsx +++ b/apps/client/src/features/editor/components/link/link-menu.tsx @@ -1,4 +1,4 @@ -import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu, useEditorState } from "@tiptap/react"; import React, { useCallback, useState } from "react"; import { EditorMenuProps } from "@/features/editor/components/table/types/types.ts"; import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx"; @@ -12,7 +12,18 @@ export function LinkMenu({ editor, appendTo }: EditorMenuProps) { return editor.isActive("link"); }, [editor]); - const { href: link } = editor.getAttributes("link"); + const editorState = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) { + return null; + } + const link = ctx.editor.getAttributes("link"); + return { + href: link.href, + }; + }, + }); const handleEdit = useCallback(() => { setShowEdit(true); @@ -70,11 +81,14 @@ export function LinkMenu({ editor, appendTo }: EditorMenuProps) { padding="xs" bg="var(--mantine-color-body)" > - + ) : ( diff --git a/apps/client/src/features/editor/components/table/table-background-color.tsx b/apps/client/src/features/editor/components/table/table-background-color.tsx index 204f0b02..7508d4fe 100644 --- a/apps/client/src/features/editor/components/table/table-background-color.tsx +++ b/apps/client/src/features/editor/components/table/table-background-color.tsx @@ -9,7 +9,8 @@ import { Tooltip, UnstyledButton, } from "@mantine/core"; -import { useEditor } from "@tiptap/react"; +import type { Editor } from "@tiptap/react"; +import { useEditorState } from "@tiptap/react"; import { useTranslation } from "react-i18next"; export interface TableColorItem { @@ -18,7 +19,7 @@ export interface TableColorItem { } interface TableBackgroundColorProps { - editor: ReturnType; + editor: Editor | null; } const TABLE_COLORS: TableColorItem[] = [ @@ -38,37 +39,50 @@ export const TableBackgroundColor: FC = ({ const { t } = useTranslation(); const [opened, setOpened] = React.useState(false); + const editorState = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) { + return null; + } + + let currentColor = ""; + if (ctx.editor.isActive("tableCell")) { + const attrs = ctx.editor.getAttributes("tableCell"); + currentColor = attrs.backgroundColor || ""; + } else if (ctx.editor.isActive("tableHeader")) { + const attrs = ctx.editor.getAttributes("tableHeader"); + currentColor = attrs.backgroundColor || ""; + } + + return { + currentColor, + isTableCell: ctx.editor.isActive("tableCell"), + isTableHeader: ctx.editor.isActive("tableHeader"), + }; + }, + }); + + if (!editor || !editorState) { + return null; + } + const setTableCellBackground = (color: string, colorName: string) => { editor .chain() .focus() - .updateAttributes("tableCell", { + .updateAttributes("tableCell", { backgroundColor: color || null, - backgroundColorName: color ? colorName : null + backgroundColorName: color ? colorName : null, }) - .updateAttributes("tableHeader", { + .updateAttributes("tableHeader", { backgroundColor: color || null, - backgroundColorName: color ? colorName : null + backgroundColorName: color ? colorName : null, }) .run(); setOpened(false); }; - // Get current cell's background color - const getCurrentColor = () => { - if (editor.isActive("tableCell")) { - const attrs = editor.getAttributes("tableCell"); - return attrs.backgroundColor || ""; - } - if (editor.isActive("tableHeader")) { - const attrs = editor.getAttributes("tableHeader"); - return attrs.backgroundColor || ""; - } - return ""; - }; - - const currentColor = getCurrentColor(); - return ( = ({ cursor: "pointer", }} > - {currentColor === item.color && ( + {editorState.currentColor === item.color && ( ; + editor: Editor | null; } interface AlignmentItem { @@ -32,25 +32,44 @@ export const TableTextAlignment: FC = ({ editor }) => { const { t } = useTranslation(); const [opened, setOpened] = React.useState(false); + const editorState = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) { + return null; + } + + return { + isAlignLeft: ctx.editor.isActive({ textAlign: "left" }), + isAlignCenter: ctx.editor.isActive({ textAlign: "center" }), + isAlignRight: ctx.editor.isActive({ textAlign: "right" }), + }; + }, + }); + + if (!editor || !editorState) { + return null; + } + const items: AlignmentItem[] = [ { name: "Align left", value: "left", - isActive: () => editor.isActive({ textAlign: "left" }), + isActive: () => editorState?.isAlignLeft, command: () => editor.chain().focus().setTextAlign("left").run(), icon: IconAlignLeft, }, { name: "Align center", value: "center", - isActive: () => editor.isActive({ textAlign: "center" }), + isActive: () => editorState?.isAlignCenter, command: () => editor.chain().focus().setTextAlign("center").run(), icon: IconAlignCenter, }, { name: "Align right", value: "right", - isActive: () => editor.isActive({ textAlign: "right" }), + isActive: () => editorState?.isAlignRight, command: () => editor.chain().focus().setTextAlign("right").run(), icon: IconAlignRight, }, @@ -64,7 +83,7 @@ export const TableTextAlignment: FC = ({ editor }) => { onChange={setOpened} position="bottom" withArrow - transitionProps={{ transition: 'pop' }} + transitionProps={{ transition: "pop" }} > @@ -87,9 +106,7 @@ export const TableTextAlignment: FC = ({ editor }) => { key={index} variant="default" leftSection={} - rightSection={ - item.isActive() && - } + rightSection={item.isActive() && } justify="left" fullWidth onClick={() => { @@ -106,4 +123,4 @@ export const TableTextAlignment: FC = ({ editor }) => { ); -}; \ No newline at end of file +}; diff --git a/apps/client/src/features/editor/components/video/video-menu.tsx b/apps/client/src/features/editor/components/video/video-menu.tsx index 0c671cd6..3252e621 100644 --- a/apps/client/src/features/editor/components/video/video-menu.tsx +++ b/apps/client/src/features/editor/components/video/video-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu, findParentNode, posToDOMRect, + useEditorState, } from "@tiptap/react"; import React, { useCallback } from "react"; import { sticky } from "tippy.js"; @@ -32,6 +33,25 @@ export function VideoMenu({ editor }: EditorMenuProps) { [editor], ); + const editorState = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) { + return null; + } + + const videoAttrs = ctx.editor.getAttributes("video"); + + return { + isVideo: ctx.editor.isActive("video"), + isAlignLeft: ctx.editor.isActive("video", { align: "left" }), + isAlignCenter: ctx.editor.isActive("video", { align: "center" }), + isAlignRight: ctx.editor.isActive("video", { align: "right" }), + width: videoAttrs?.width ? parseInt(videoAttrs.width) : null, + }; + }, + }); + const getReferenceClientRect = useCallback(() => { const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "video"; @@ -83,7 +103,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { return ( @@ -116,11 +134,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { onClick={alignVideoCenter} size="lg" aria-label={t("Align center")} - variant={ - editor.isActive("video", { align: "center" }) - ? "light" - : "default" - } + variant={editorState?.isAlignCenter ? "light" : "default"} > @@ -131,20 +145,15 @@ export function VideoMenu({ editor }: EditorMenuProps) { onClick={alignVideoRight} size="lg" aria-label={t("Align right")} - variant={ - editor.isActive("video", { align: "right" }) ? "light" : "default" - } + variant={editorState?.isAlignRight ? "light" : "default"} > - {editor.getAttributes("video")?.width && ( - + {editorState?.width && ( + )} ); diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index e97a783f..a2dc0d93 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -7,7 +7,12 @@ import { onAuthenticationFailedParameters, WebSocketStatus, } from "@hocuspocus/provider"; -import { EditorContent, EditorProvider, useEditor } from "@tiptap/react"; +import { + EditorContent, + EditorProvider, + useEditor, + useEditorState, +} from "@tiptap/react"; import { collabExtensions, mainExtensions, @@ -50,7 +55,7 @@ import { extractPageSlugId } from "@/lib"; import { FIVE_MINUTES } from "@/lib/constants.ts"; import { PageEditMode } from "@/features/user/types/user.types.ts"; import { jwtDecode } from "jwt-decode"; -import { searchSpotlight } from '@/features/search/constants.ts'; +import { searchSpotlight } from "@/features/search/constants.ts"; interface PageEditorProps { pageId: string; @@ -77,7 +82,7 @@ export default function PageEditor({ const [isLocalSynced, setLocalSynced] = useState(false); const [isRemoteSynced, setRemoteSynced] = useState(false); const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom( - yjsConnectionStatusAtom + yjsConnectionStatusAtom, ); const menuContainerRef = useRef(null); const documentName = `page.${pageId}`; @@ -213,17 +218,17 @@ export default function PageEditor({ extensions, editable, immediatelyRender: true, - shouldRerenderOnTransaction: true, + shouldRerenderOnTransaction: false, editorProps: { scrollThreshold: 80, scrollMargin: 80, handleDOMEvents: { keydown: (_view, event) => { - if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') { + if ((event.ctrlKey || event.metaKey) && event.code === "KeyS") { event.preventDefault(); return true; } - if ((event.ctrlKey || event.metaKey) && event.code === 'KeyK') { + if ((event.ctrlKey || event.metaKey) && event.code === "KeyK") { searchSpotlight.open(); return true; } @@ -268,9 +273,16 @@ export default function PageEditor({ debouncedUpdateContent(editorJson); }, }, - [pageId, editable, remoteProvider] + [pageId, editable, remoteProvider], ); + const editorIsEditable = useEditorState({ + editor, + selector: (ctx) => { + return ctx.editor?.isEditable ?? false; + }, + }); + const debouncedUpdateContent = useDebouncedCallback((newContent: any) => { const pageData = queryClient.getQueryData(["pages", slugId]); @@ -306,7 +318,7 @@ export default function PageEditor({ return () => { document.removeEventListener( "ACTIVE_COMMENT_EVENT", - handleActiveCommentEvent + handleActiveCommentEvent, ); }; }, []); @@ -389,7 +401,7 @@ export default function PageEditor({ )} - {editor && editor.isEditable && ( + {editor && editorIsEditable && (