Switch to useEditorState

- Set shouldRerenderOnTransaction to false
This commit is contained in:
Philipinho
2025-08-15 01:19:05 -07:00
parent 71dfcf6bce
commit c2cd412ac7
10 changed files with 263 additions and 102 deletions
@@ -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<BubbleMenuProps, "children" | "editor"> & {
editor: ReturnType<typeof useEditor>;
editor: Editor | null;
};
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
@@ -42,38 +43,61 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (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<EditorBubbleMenuProps> = (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<EditorBubbleMenuProps> = (props) => {
},
};
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
const [isTextAlignmentSelectorOpen, setIsTextAlignmentOpen] = useState(false);
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
return (
<BubbleMenu {...bubbleMenuProps}>
<div className={classes.bubbleMenu}>
@@ -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<typeof useEditor>;
editor: Editor | null;
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}
@@ -108,12 +109,36 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
setIsOpen,
}) => {
const { t } = useTranslation();
const editorState = useEditorState({
editor,
selector: ctx => {
if (!ctx.editor) {
return null;
}
const activeColors: Record<string, boolean> = {};
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<ColorSelectorProps> = ({
justify="left"
fullWidth
rightSection={
editor.isActive("textStyle", { color }) && (
editorState[`text_${color}`] && (
<IconCheck style={{ width: rem(16) }} />
)
}
@@ -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<typeof useEditor>;
editor: Editor | null;
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}
@@ -36,6 +37,27 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
}) => {
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<NodeSelectorProps> = ({
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<NodeSelectorProps> = ({
.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,
},
];
@@ -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<typeof useEditor>;
editor: Editor | null;
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}
@@ -31,36 +32,54 @@ export const TextAlignmentSelector: FC<TextAlignmentProps> = ({
}) => {
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 (
<Popover opened={isOpen} withArrow>
@@ -73,7 +92,7 @@ export const TextAlignmentSelector: FC<TextAlignmentProps> = ({
rightSection={<IconChevronDown size={16} />}
onClick={() => setIsOpen(!isOpen)}
>
<IconAlignLeft style={{ width: rem(16) }} stroke={2} />
<activeItem.icon style={{ width: rem(16) }} stroke={2} />
</Button>
</Popover.Target>
@@ -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"}
>
<IconInfoCircleFilled size={18} />
</ActionIcon>
@@ -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"}
>
<IconCircleCheckFilled size={18} />
</ActionIcon>
@@ -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"}
>
<IconAlertTriangleFilled size={18} />
</ActionIcon>
@@ -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"}
>
<IconCircleXFilled size={18} />
</ActionIcon>
@@ -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"
}
>
<IconLayoutAlignLeft size={18} />
@@ -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"
}
>
<IconLayoutAlignCenter size={18} />
@@ -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"
}
>
<IconLayoutAlignRight size={18} />
@@ -133,10 +151,10 @@ export function ImageMenu({ editor }: EditorMenuProps) {
</Tooltip>
</ActionIcon.Group>
{editor.getAttributes("image")?.width && (
{editorState?.imageWidth && (
<NodeWidthResize
onChange={onWidthChange}
value={parseInt(editor.getAttributes("image").width)}
value={editorState.imageWidth}
/>
)}
</BaseBubbleMenu>
@@ -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<typeof useEditor>;
editor: Editor | null;
}
const TABLE_COLORS: TableColorItem[] = [
@@ -37,6 +38,34 @@ export const TableBackgroundColor: FC<TableBackgroundColorProps> = ({
}) => {
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<TableBackgroundColorProps> = ({
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 (
<Popover
@@ -123,7 +139,7 @@ export const TableBackgroundColor: FC<TableBackgroundColorProps> = ({
cursor: "pointer",
}}
>
{currentColor === item.color && (
{editorState.currentColor === item.color && (
<IconCheck
size={18}
style={{
@@ -13,11 +13,12 @@ import {
ScrollArea,
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";
interface TableTextAlignmentProps {
editor: ReturnType<typeof useEditor>;
editor: Editor | null;
}
interface AlignmentItem {
@@ -31,26 +32,45 @@ interface AlignmentItem {
export const TableTextAlignment: FC<TableTextAlignmentProps> = ({ 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,
},
@@ -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"
}
>
<IconLayoutAlignLeft size={18} />
@@ -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"
}
>
<IconLayoutAlignCenter size={18} />
@@ -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"
}
>
<IconLayoutAlignRight size={18} />
@@ -133,10 +151,10 @@ export function VideoMenu({ editor }: EditorMenuProps) {
</Tooltip>
</ActionIcon.Group>
{editor.getAttributes("video")?.width && (
{editorState?.videoWidth && (
<NodeWidthResize
onChange={onWidthChange}
value={parseInt(editor.getAttributes("video").width)}
value={editorState.videoWidth}
/>
)}
</BaseBubbleMenu>
@@ -203,7 +203,7 @@ export default function PageEditor({
extensions,
editable,
immediatelyRender: true,
shouldRerenderOnTransaction: true,
shouldRerenderOnTransaction: false,
editorProps: {
scrollThreshold: 80,
scrollMargin: 80,