mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 22:53:08 +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 { 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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+29
-10
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user