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 67cae1a7..0f6153dc 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 @@ -1,5 +1,6 @@ import { BubbleMenu, BubbleMenuProps } from "@tiptap/react/menus"; -import { isNodeSelection, useEditor } from "@tiptap/react"; +import { isNodeSelection, useEditorState } from "@tiptap/react"; +import type { Editor } from "@tiptap/react"; import { FC, useEffect, useRef, useState } from "react"; import { IconBold, @@ -33,7 +34,7 @@ export interface BubbleMenuItem { } type EditorBubbleMenuProps = Omit & { - editor: ReturnType; + editor: Editor | null; }; export const EditorBubbleMenu: FC = (props) => { @@ -42,38 +43,61 @@ export const EditorBubbleMenu: FC = (props) => { const [, setDraftCommentId] = useAtom(draftCommentIdAtom); const showCommentPopupRef = useRef(showCommentPopup); + const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false); + const [isTextAlignmentSelectorOpen, setIsTextAlignmentOpen] = useState(false); + const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false); + const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); + useEffect(() => { 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, }, @@ -81,7 +105,7 @@ export const EditorBubbleMenu: FC = (props) => { const commentItem: BubbleMenuItem = { name: "Comment", - isActive: () => props.editor.isActive("comment"), + isActive: () => editorState.isComment, command: () => { const commentId = uuid7(); @@ -122,11 +146,6 @@ export const EditorBubbleMenu: FC = (props) => { }, }; - const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false); - const [isTextAlignmentSelectorOpen, setIsTextAlignmentOpen] = useState(false); - const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false); - const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); - return (
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..34d466b2 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..57f34a44 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..ae35eb48 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 843afbd8..12683bf4 100644 --- a/apps/client/src/features/editor/components/callout/callout-menu.tsx +++ b/apps/client/src/features/editor/components/callout/callout-menu.tsx @@ -1,5 +1,5 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; -import { findParentNode, posToDOMRect } from "@tiptap/react"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import React, { useCallback } from "react"; import { Node as PMNode } from "prosemirror-model"; import { @@ -18,6 +18,24 @@ import { useTranslation } from "react-i18next"; export function CalloutMenu({ editor }: EditorMenuProps) { const { t } = useTranslation(); + + 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 shouldShow = useCallback( ({ state }: ShouldShowProps) => { if (!state) { @@ -58,10 +76,10 @@ export function CalloutMenu({ editor }: EditorMenuProps) { pluginKey={`callout-menu}`} updateDelay={0} options={{ - // getReferenceClientRect, + // getReferenceClientRect, placement: "right-end", - // offset: 233, - // zIndex: 99, + // offset: 233, + // zIndex: 99, flip: false, }} shouldShow={shouldShow} @@ -72,9 +90,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) { onClick={() => setCalloutType("info")} size="lg" aria-label={t("Info")} - variant={ - editor.isActive("callout", { type: "info" }) ? "light" : "default" - } + variant={editorState?.isInfo ? "light" : "default"} > @@ -85,11 +101,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"} > @@ -100,11 +112,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"} > @@ -115,11 +123,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/image/image-menu.tsx b/apps/client/src/features/editor/components/image/image-menu.tsx index 59599b79..a75c109c 100644 --- a/apps/client/src/features/editor/components/image/image-menu.tsx +++ b/apps/client/src/features/editor/components/image/image-menu.tsx @@ -1,5 +1,5 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; -import { findParentNode, posToDOMRect } from "@tiptap/react"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import React, { useCallback } from "react"; import { Node as PMNode } from "prosemirror-model"; import { @@ -17,6 +17,26 @@ import { useTranslation } from "react-i18next"; export function ImageMenu({ editor }: EditorMenuProps) { const { t } = useTranslation(); + + 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" }), + imageWidth: imageAttrs?.width ? parseInt(imageAttrs.width) : null, + }; + }, + }); + const shouldShow = useCallback( ({ state }: ShouldShowProps) => { if (!state) { @@ -97,7 +117,7 @@ export function ImageMenu({ editor }: EditorMenuProps) { size="lg" aria-label={t("Align left")} variant={ - editor.isActive("image", { align: "left" }) ? "light" : "default" + editorState?.isAlignLeft ? "light" : "default" } > @@ -110,9 +130,7 @@ export function ImageMenu({ editor }: EditorMenuProps) { size="lg" aria-label={t("Align center")} variant={ - editor.isActive("image", { align: "center" }) - ? "light" - : "default" + editorState?.isAlignCenter ? "light" : "default" } > @@ -125,7 +143,7 @@ export function ImageMenu({ editor }: EditorMenuProps) { size="lg" aria-label={t("Align right")} variant={ - editor.isActive("image", { align: "right" }) ? "light" : "default" + editorState?.isAlignRight ? "light" : "default" } > @@ -133,10 +151,10 @@ export function ImageMenu({ editor }: EditorMenuProps) { - {editor.getAttributes("image")?.width && ( + {editorState?.imageWidth && ( )} 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..107a3474 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[] = [ @@ -37,6 +38,34 @@ 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 @@ -54,20 +83,7 @@ export const TableBackgroundColor: FC = ({ 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(); + const currentColor = editorState.currentColor; return ( = ({ cursor: "pointer", }} > - {currentColor === item.color && ( + {editorState.currentColor === item.color && ( ; + editor: Editor | null; } interface AlignmentItem { @@ -31,26 +32,45 @@ interface AlignmentItem { 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, }, 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 a2a696a7..c58de114 100644 --- a/apps/client/src/features/editor/components/video/video-menu.tsx +++ b/apps/client/src/features/editor/components/video/video-menu.tsx @@ -1,5 +1,5 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; -import { findParentNode, posToDOMRect } from "@tiptap/react"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import React, { useCallback } from "react"; import { Node as PMNode } from "prosemirror-model"; import { @@ -17,6 +17,26 @@ import { useTranslation } from "react-i18next"; export function VideoMenu({ editor }: EditorMenuProps) { const { t } = useTranslation(); + + 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" }), + videoWidth: videoAttrs?.width ? parseInt(videoAttrs.width) : null, + }; + }, + }); + const shouldShow = useCallback( ({ state }: ShouldShowProps) => { if (!state) { @@ -97,7 +117,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { size="lg" aria-label={t("Align left")} variant={ - editor.isActive("video", { align: "left" }) ? "light" : "default" + editorState?.isAlignLeft ? "light" : "default" } > @@ -110,9 +130,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { size="lg" aria-label={t("Align center")} variant={ - editor.isActive("video", { align: "center" }) - ? "light" - : "default" + editorState?.isAlignCenter ? "light" : "default" } > @@ -125,7 +143,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { size="lg" aria-label={t("Align right")} variant={ - editor.isActive("video", { align: "right" }) ? "light" : "default" + editorState?.isAlignRight ? "light" : "default" } > @@ -133,10 +151,10 @@ export function VideoMenu({ editor }: EditorMenuProps) { - {editor.getAttributes("video")?.width && ( + {editorState?.videoWidth && ( )} diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index b13e0092..b969f6e1 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -203,7 +203,7 @@ export default function PageEditor({ extensions, editable, immediatelyRender: true, - shouldRerenderOnTransaction: true, + shouldRerenderOnTransaction: false, editorProps: { scrollThreshold: 80, scrollMargin: 80,