mirror of
https://github.com/docmost/docmost.git
synced 2026-05-21 01:04:39 +08:00
Switch to useEditorState
- Set shouldRerenderOnTransaction to false
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { BubbleMenu, BubbleMenuProps } from "@tiptap/react/menus";
|
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 { FC, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
IconBold,
|
IconBold,
|
||||||
@@ -33,7 +34,7 @@ export interface BubbleMenuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
|
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
|
||||||
editor: ReturnType<typeof useEditor>;
|
editor: Editor | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||||
@@ -42,38 +43,61 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
||||||
const showCommentPopupRef = useRef(showCommentPopup);
|
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(() => {
|
useEffect(() => {
|
||||||
showCommentPopupRef.current = showCommentPopup;
|
showCommentPopupRef.current = showCommentPopup;
|
||||||
}, [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[] = [
|
const items: BubbleMenuItem[] = [
|
||||||
{
|
{
|
||||||
name: "Bold",
|
name: "Bold",
|
||||||
isActive: () => props.editor.isActive("bold"),
|
isActive: () => editorState.isBold,
|
||||||
command: () => props.editor.chain().focus().toggleBold().run(),
|
command: () => props.editor.chain().focus().toggleBold().run(),
|
||||||
icon: IconBold,
|
icon: IconBold,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Italic",
|
name: "Italic",
|
||||||
isActive: () => props.editor.isActive("italic"),
|
isActive: () => editorState.isItalic,
|
||||||
command: () => props.editor.chain().focus().toggleItalic().run(),
|
command: () => props.editor.chain().focus().toggleItalic().run(),
|
||||||
icon: IconItalic,
|
icon: IconItalic,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Underline",
|
name: "Underline",
|
||||||
isActive: () => props.editor.isActive("underline"),
|
isActive: () => editorState.isUnderline,
|
||||||
command: () => props.editor.chain().focus().toggleUnderline().run(),
|
command: () => props.editor.chain().focus().toggleUnderline().run(),
|
||||||
icon: IconUnderline,
|
icon: IconUnderline,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Strike",
|
name: "Strike",
|
||||||
isActive: () => props.editor.isActive("strike"),
|
isActive: () => editorState.isStrike,
|
||||||
command: () => props.editor.chain().focus().toggleStrike().run(),
|
command: () => props.editor.chain().focus().toggleStrike().run(),
|
||||||
icon: IconStrikethrough,
|
icon: IconStrikethrough,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Code",
|
name: "Code",
|
||||||
isActive: () => props.editor.isActive("code"),
|
isActive: () => editorState.isCode,
|
||||||
command: () => props.editor.chain().focus().toggleCode().run(),
|
command: () => props.editor.chain().focus().toggleCode().run(),
|
||||||
icon: IconCode,
|
icon: IconCode,
|
||||||
},
|
},
|
||||||
@@ -81,7 +105,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
|
|
||||||
const commentItem: BubbleMenuItem = {
|
const commentItem: BubbleMenuItem = {
|
||||||
name: "Comment",
|
name: "Comment",
|
||||||
isActive: () => props.editor.isActive("comment"),
|
isActive: () => editorState.isComment,
|
||||||
command: () => {
|
command: () => {
|
||||||
const commentId = uuid7();
|
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 (
|
return (
|
||||||
<BubbleMenu {...bubbleMenuProps}>
|
<BubbleMenu {...bubbleMenuProps}>
|
||||||
<div className={classes.bubbleMenu}>
|
<div className={classes.bubbleMenu}>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useEditor } from "@tiptap/react";
|
import type { Editor } from "@tiptap/react";
|
||||||
|
import { useEditorState } from "@tiptap/react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface BubbleColorMenuItem {
|
export interface BubbleColorMenuItem {
|
||||||
@@ -18,7 +19,7 @@ export interface BubbleColorMenuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ColorSelectorProps {
|
interface ColorSelectorProps {
|
||||||
editor: ReturnType<typeof useEditor>;
|
editor: Editor | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
@@ -108,12 +109,36 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
|||||||
setIsOpen,
|
setIsOpen,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
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 }) =>
|
const activeColorItem = TEXT_COLORS.find(({ color }) =>
|
||||||
editor.isActive("textStyle", { color }),
|
editorState[`text_${color}`]
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) =>
|
const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) =>
|
||||||
editor.isActive("highlight", { color }),
|
editorState[`highlight_${color}`]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -151,7 +176,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
|||||||
justify="left"
|
justify="left"
|
||||||
fullWidth
|
fullWidth
|
||||||
rightSection={
|
rightSection={
|
||||||
editor.isActive("textStyle", { color }) && (
|
editorState[`text_${color}`] && (
|
||||||
<IconCheck style={{ width: rem(16) }} />
|
<IconCheck style={{ width: rem(16) }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ import {
|
|||||||
IconTypography,
|
IconTypography,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { Popover, Button, ScrollArea } from "@mantine/core";
|
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";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface NodeSelectorProps {
|
interface NodeSelectorProps {
|
||||||
editor: ReturnType<typeof useEditor>;
|
editor: Editor | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
@@ -36,6 +37,27 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
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[] = [
|
const items: BubbleMenuItem[] = [
|
||||||
{
|
{
|
||||||
name: "Text",
|
name: "Text",
|
||||||
@@ -43,45 +65,45 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
|||||||
command: () =>
|
command: () =>
|
||||||
editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
|
editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
|
||||||
isActive: () =>
|
isActive: () =>
|
||||||
editor.isActive("paragraph") &&
|
editorState.isParagraph &&
|
||||||
!editor.isActive("bulletList") &&
|
!editorState.isBulletList &&
|
||||||
!editor.isActive("orderedList"),
|
!editorState.isOrderedList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Heading 1",
|
name: "Heading 1",
|
||||||
icon: IconH1,
|
icon: IconH1,
|
||||||
command: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
command: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||||
isActive: () => editor.isActive("heading", { level: 1 }),
|
isActive: () => editorState.isHeading1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Heading 2",
|
name: "Heading 2",
|
||||||
icon: IconH2,
|
icon: IconH2,
|
||||||
command: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
command: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||||
isActive: () => editor.isActive("heading", { level: 2 }),
|
isActive: () => editorState.isHeading2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Heading 3",
|
name: "Heading 3",
|
||||||
icon: IconH3,
|
icon: IconH3,
|
||||||
command: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
command: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
||||||
isActive: () => editor.isActive("heading", { level: 3 }),
|
isActive: () => editorState.isHeading3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "To-do List",
|
name: "To-do List",
|
||||||
icon: IconCheckbox,
|
icon: IconCheckbox,
|
||||||
command: () => editor.chain().focus().toggleTaskList().run(),
|
command: () => editor.chain().focus().toggleTaskList().run(),
|
||||||
isActive: () => editor.isActive("taskItem"),
|
isActive: () => editorState.isTaskItem,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Bullet List",
|
name: "Bullet List",
|
||||||
icon: IconList,
|
icon: IconList,
|
||||||
command: () => editor.chain().focus().toggleBulletList().run(),
|
command: () => editor.chain().focus().toggleBulletList().run(),
|
||||||
isActive: () => editor.isActive("bulletList"),
|
isActive: () => editorState.isBulletList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Numbered List",
|
name: "Numbered List",
|
||||||
icon: IconListNumbers,
|
icon: IconListNumbers,
|
||||||
command: () => editor.chain().focus().toggleOrderedList().run(),
|
command: () => editor.chain().focus().toggleOrderedList().run(),
|
||||||
isActive: () => editor.isActive("orderedList"),
|
isActive: () => editorState.isOrderedList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Blockquote",
|
name: "Blockquote",
|
||||||
@@ -93,13 +115,13 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
|||||||
.toggleNode("paragraph", "paragraph")
|
.toggleNode("paragraph", "paragraph")
|
||||||
.toggleBlockquote()
|
.toggleBlockquote()
|
||||||
.run(),
|
.run(),
|
||||||
isActive: () => editor.isActive("blockquote"),
|
isActive: () => editorState.isBlockquote,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Code",
|
name: "Code",
|
||||||
icon: IconCode,
|
icon: IconCode,
|
||||||
command: () => editor.chain().focus().toggleCodeBlock().run(),
|
command: () => editor.chain().focus().toggleCodeBlock().run(),
|
||||||
isActive: () => editor.isActive("codeBlock"),
|
isActive: () => editorState.isCodeBlock,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+29
-10
@@ -8,11 +8,12 @@ import {
|
|||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { Popover, Button, ScrollArea, rem } from "@mantine/core";
|
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";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface TextAlignmentProps {
|
interface TextAlignmentProps {
|
||||||
editor: ReturnType<typeof useEditor>;
|
editor: Editor | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
@@ -31,36 +32,54 @@ export const TextAlignmentSelector: FC<TextAlignmentProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
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[] = [
|
const items: BubbleMenuItem[] = [
|
||||||
{
|
{
|
||||||
name: "Align left",
|
name: "Align left",
|
||||||
isActive: () => editor.isActive({ textAlign: "left" }),
|
isActive: () => editorState.isAlignLeft,
|
||||||
command: () => editor.chain().focus().setTextAlign("left").run(),
|
command: () => editor.chain().focus().setTextAlign("left").run(),
|
||||||
icon: IconAlignLeft,
|
icon: IconAlignLeft,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Align center",
|
name: "Align center",
|
||||||
isActive: () => editor.isActive({ textAlign: "center" }),
|
isActive: () => editorState.isAlignCenter,
|
||||||
command: () => editor.chain().focus().setTextAlign("center").run(),
|
command: () => editor.chain().focus().setTextAlign("center").run(),
|
||||||
icon: IconAlignCenter,
|
icon: IconAlignCenter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Align right",
|
name: "Align right",
|
||||||
isActive: () => editor.isActive({ textAlign: "right" }),
|
isActive: () => editorState.isAlignRight,
|
||||||
command: () => editor.chain().focus().setTextAlign("right").run(),
|
command: () => editor.chain().focus().setTextAlign("right").run(),
|
||||||
icon: IconAlignRight,
|
icon: IconAlignRight,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Justify",
|
name: "Justify",
|
||||||
isActive: () => editor.isActive({ textAlign: "justify" }),
|
isActive: () => editorState.isAlignJustify,
|
||||||
command: () => editor.chain().focus().setTextAlign("justify").run(),
|
command: () => editor.chain().focus().setTextAlign("justify").run(),
|
||||||
icon: IconAlignJustified,
|
icon: IconAlignJustified,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const activeItem = items.filter((item) => item.isActive()).pop() ?? {
|
const activeItem = items.filter((item) => item.isActive()).pop() ?? items[0];
|
||||||
name: "Multiple",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover opened={isOpen} withArrow>
|
<Popover opened={isOpen} withArrow>
|
||||||
@@ -73,7 +92,7 @@ export const TextAlignmentSelector: FC<TextAlignmentProps> = ({
|
|||||||
rightSection={<IconChevronDown size={16} />}
|
rightSection={<IconChevronDown size={16} />}
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
>
|
>
|
||||||
<IconAlignLeft style={{ width: rem(16) }} stroke={2} />
|
<activeItem.icon style={{ width: rem(16) }} stroke={2} />
|
||||||
</Button>
|
</Button>
|
||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
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 React, { useCallback } from "react";
|
||||||
import { Node as PMNode } from "prosemirror-model";
|
import { Node as PMNode } from "prosemirror-model";
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +18,24 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
export function CalloutMenu({ editor }: EditorMenuProps) {
|
export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||||
const { t } = useTranslation();
|
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(
|
const shouldShow = useCallback(
|
||||||
({ state }: ShouldShowProps) => {
|
({ state }: ShouldShowProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -58,10 +76,10 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
pluginKey={`callout-menu}`}
|
pluginKey={`callout-menu}`}
|
||||||
updateDelay={0}
|
updateDelay={0}
|
||||||
options={{
|
options={{
|
||||||
// getReferenceClientRect,
|
// getReferenceClientRect,
|
||||||
placement: "right-end",
|
placement: "right-end",
|
||||||
// offset: 233,
|
// offset: 233,
|
||||||
// zIndex: 99,
|
// zIndex: 99,
|
||||||
flip: false,
|
flip: false,
|
||||||
}}
|
}}
|
||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
@@ -72,9 +90,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
onClick={() => setCalloutType("info")}
|
onClick={() => setCalloutType("info")}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Info")}
|
aria-label={t("Info")}
|
||||||
variant={
|
variant={editorState?.isInfo ? "light" : "default"}
|
||||||
editor.isActive("callout", { type: "info" }) ? "light" : "default"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<IconInfoCircleFilled size={18} />
|
<IconInfoCircleFilled size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -85,11 +101,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
onClick={() => setCalloutType("success")}
|
onClick={() => setCalloutType("success")}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Success")}
|
aria-label={t("Success")}
|
||||||
variant={
|
variant={editorState?.isSuccess ? "light" : "default"}
|
||||||
editor.isActive("callout", { type: "success" })
|
|
||||||
? "light"
|
|
||||||
: "default"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<IconCircleCheckFilled size={18} />
|
<IconCircleCheckFilled size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -100,11 +112,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
onClick={() => setCalloutType("warning")}
|
onClick={() => setCalloutType("warning")}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Warning")}
|
aria-label={t("Warning")}
|
||||||
variant={
|
variant={editorState?.isWarning ? "light" : "default"}
|
||||||
editor.isActive("callout", { type: "warning" })
|
|
||||||
? "light"
|
|
||||||
: "default"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<IconAlertTriangleFilled size={18} />
|
<IconAlertTriangleFilled size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -115,11 +123,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
onClick={() => setCalloutType("danger")}
|
onClick={() => setCalloutType("danger")}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Danger")}
|
aria-label={t("Danger")}
|
||||||
variant={
|
variant={editorState?.isDanger ? "light" : "default"}
|
||||||
editor.isActive("callout", { type: "danger" })
|
|
||||||
? "light"
|
|
||||||
: "default"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<IconCircleXFilled size={18} />
|
<IconCircleXFilled size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
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 React, { useCallback } from "react";
|
||||||
import { Node as PMNode } from "prosemirror-model";
|
import { Node as PMNode } from "prosemirror-model";
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +17,26 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
export function ImageMenu({ editor }: EditorMenuProps) {
|
export function ImageMenu({ editor }: EditorMenuProps) {
|
||||||
const { t } = useTranslation();
|
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(
|
const shouldShow = useCallback(
|
||||||
({ state }: ShouldShowProps) => {
|
({ state }: ShouldShowProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -97,7 +117,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Align left")}
|
aria-label={t("Align left")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("image", { align: "left" }) ? "light" : "default"
|
editorState?.isAlignLeft ? "light" : "default"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconLayoutAlignLeft size={18} />
|
<IconLayoutAlignLeft size={18} />
|
||||||
@@ -110,9 +130,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Align center")}
|
aria-label={t("Align center")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("image", { align: "center" })
|
editorState?.isAlignCenter ? "light" : "default"
|
||||||
? "light"
|
|
||||||
: "default"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconLayoutAlignCenter size={18} />
|
<IconLayoutAlignCenter size={18} />
|
||||||
@@ -125,7 +143,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Align right")}
|
aria-label={t("Align right")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("image", { align: "right" }) ? "light" : "default"
|
editorState?.isAlignRight ? "light" : "default"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconLayoutAlignRight size={18} />
|
<IconLayoutAlignRight size={18} />
|
||||||
@@ -133,10 +151,10 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ActionIcon.Group>
|
</ActionIcon.Group>
|
||||||
|
|
||||||
{editor.getAttributes("image")?.width && (
|
{editorState?.imageWidth && (
|
||||||
<NodeWidthResize
|
<NodeWidthResize
|
||||||
onChange={onWidthChange}
|
onChange={onWidthChange}
|
||||||
value={parseInt(editor.getAttributes("image").width)}
|
value={editorState.imageWidth}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</BaseBubbleMenu>
|
</BaseBubbleMenu>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useEditor } from "@tiptap/react";
|
import type { Editor } from "@tiptap/react";
|
||||||
|
import { useEditorState } from "@tiptap/react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface TableColorItem {
|
export interface TableColorItem {
|
||||||
@@ -18,7 +19,7 @@ export interface TableColorItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TableBackgroundColorProps {
|
interface TableBackgroundColorProps {
|
||||||
editor: ReturnType<typeof useEditor>;
|
editor: Editor | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TABLE_COLORS: TableColorItem[] = [
|
const TABLE_COLORS: TableColorItem[] = [
|
||||||
@@ -38,6 +39,34 @@ export const TableBackgroundColor: FC<TableBackgroundColorProps> = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [opened, setOpened] = React.useState(false);
|
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) => {
|
const setTableCellBackground = (color: string, colorName: string) => {
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
@@ -54,20 +83,7 @@ export const TableBackgroundColor: FC<TableBackgroundColorProps> = ({
|
|||||||
setOpened(false);
|
setOpened(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get current cell's background color
|
const currentColor = editorState.currentColor;
|
||||||
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 (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
@@ -123,7 +139,7 @@ export const TableBackgroundColor: FC<TableBackgroundColorProps> = ({
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{currentColor === item.color && (
|
{editorState.currentColor === item.color && (
|
||||||
<IconCheck
|
<IconCheck
|
||||||
size={18}
|
size={18}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ import {
|
|||||||
ScrollArea,
|
ScrollArea,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useEditor } from "@tiptap/react";
|
import type { Editor } from "@tiptap/react";
|
||||||
|
import { useEditorState } from "@tiptap/react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface TableTextAlignmentProps {
|
interface TableTextAlignmentProps {
|
||||||
editor: ReturnType<typeof useEditor>;
|
editor: Editor | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AlignmentItem {
|
interface AlignmentItem {
|
||||||
@@ -32,25 +33,44 @@ export const TableTextAlignment: FC<TableTextAlignmentProps> = ({ editor }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [opened, setOpened] = React.useState(false);
|
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[] = [
|
const items: AlignmentItem[] = [
|
||||||
{
|
{
|
||||||
name: "Align left",
|
name: "Align left",
|
||||||
value: "left",
|
value: "left",
|
||||||
isActive: () => editor.isActive({ textAlign: "left" }),
|
isActive: () => editorState.isAlignLeft,
|
||||||
command: () => editor.chain().focus().setTextAlign("left").run(),
|
command: () => editor.chain().focus().setTextAlign("left").run(),
|
||||||
icon: IconAlignLeft,
|
icon: IconAlignLeft,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Align center",
|
name: "Align center",
|
||||||
value: "center",
|
value: "center",
|
||||||
isActive: () => editor.isActive({ textAlign: "center" }),
|
isActive: () => editorState.isAlignCenter,
|
||||||
command: () => editor.chain().focus().setTextAlign("center").run(),
|
command: () => editor.chain().focus().setTextAlign("center").run(),
|
||||||
icon: IconAlignCenter,
|
icon: IconAlignCenter,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Align right",
|
name: "Align right",
|
||||||
value: "right",
|
value: "right",
|
||||||
isActive: () => editor.isActive({ textAlign: "right" }),
|
isActive: () => editorState.isAlignRight,
|
||||||
command: () => editor.chain().focus().setTextAlign("right").run(),
|
command: () => editor.chain().focus().setTextAlign("right").run(),
|
||||||
icon: IconAlignRight,
|
icon: IconAlignRight,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
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 React, { useCallback } from "react";
|
||||||
import { Node as PMNode } from "prosemirror-model";
|
import { Node as PMNode } from "prosemirror-model";
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +17,26 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
export function VideoMenu({ editor }: EditorMenuProps) {
|
export function VideoMenu({ editor }: EditorMenuProps) {
|
||||||
const { t } = useTranslation();
|
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(
|
const shouldShow = useCallback(
|
||||||
({ state }: ShouldShowProps) => {
|
({ state }: ShouldShowProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -97,7 +117,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Align left")}
|
aria-label={t("Align left")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("video", { align: "left" }) ? "light" : "default"
|
editorState?.isAlignLeft ? "light" : "default"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconLayoutAlignLeft size={18} />
|
<IconLayoutAlignLeft size={18} />
|
||||||
@@ -110,9 +130,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Align center")}
|
aria-label={t("Align center")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("video", { align: "center" })
|
editorState?.isAlignCenter ? "light" : "default"
|
||||||
? "light"
|
|
||||||
: "default"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconLayoutAlignCenter size={18} />
|
<IconLayoutAlignCenter size={18} />
|
||||||
@@ -125,7 +143,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Align right")}
|
aria-label={t("Align right")}
|
||||||
variant={
|
variant={
|
||||||
editor.isActive("video", { align: "right" }) ? "light" : "default"
|
editorState?.isAlignRight ? "light" : "default"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconLayoutAlignRight size={18} />
|
<IconLayoutAlignRight size={18} />
|
||||||
@@ -133,10 +151,10 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ActionIcon.Group>
|
</ActionIcon.Group>
|
||||||
|
|
||||||
{editor.getAttributes("video")?.width && (
|
{editorState?.videoWidth && (
|
||||||
<NodeWidthResize
|
<NodeWidthResize
|
||||||
onChange={onWidthChange}
|
onChange={onWidthChange}
|
||||||
value={parseInt(editor.getAttributes("video").width)}
|
value={editorState.videoWidth}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</BaseBubbleMenu>
|
</BaseBubbleMenu>
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ export default function PageEditor({
|
|||||||
extensions,
|
extensions,
|
||||||
editable,
|
editable,
|
||||||
immediatelyRender: true,
|
immediatelyRender: true,
|
||||||
shouldRerenderOnTransaction: true,
|
shouldRerenderOnTransaction: false,
|
||||||
editorProps: {
|
editorProps: {
|
||||||
scrollThreshold: 80,
|
scrollThreshold: 80,
|
||||||
scrollMargin: 80,
|
scrollMargin: 80,
|
||||||
|
|||||||
Reference in New Issue
Block a user