mirror of
https://github.com/docmost/docmost.git
synced 2026-08-29 18:14:58 +08:00
Merge branch 'main' into feat/bases
This commit is contained in:
@@ -23,7 +23,7 @@ import {
|
|||||||
} from "@/features/comment/atoms/comment-atom";
|
} from "@/features/comment/atoms/comment-atom";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { v7 as uuid7 } from "uuid";
|
import { v7 as uuid7 } from "uuid";
|
||||||
import { isCellSelection, isTextSelected } from "@docmost/editor-ext";
|
import { isCellSelection, isEditorReady, isTextSelected } from "@docmost/editor-ext";
|
||||||
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { showAiMenuAtom, showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms";
|
import { showAiMenuAtom, showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms";
|
||||||
@@ -226,7 +226,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
aria-label={t(item.name)}
|
aria-label={t(item.name)}
|
||||||
className={clsx({ [classes.active]: item.isActive() })}
|
className={clsx({ [classes.active]: item.isActive() })}
|
||||||
style={{ border: "none" }}
|
style={{ border: "none" }}
|
||||||
onClick={item.command}
|
onClick={() => isEditorReady(props.editor) && item.command()}
|
||||||
>
|
>
|
||||||
<item.icon style={{ width: rem(16) }} stroke={2} />
|
<item.icon style={{ width: rem(16) }} stroke={2} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -256,7 +256,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
radius="6px"
|
radius="6px"
|
||||||
aria-label={t(commentItem.name)}
|
aria-label={t(commentItem.name)}
|
||||||
style={{ border: "none" }}
|
style={{ border: "none" }}
|
||||||
onClick={commentItem.command}
|
onClick={() => isEditorReady(props.editor) && commentItem.command()}
|
||||||
>
|
>
|
||||||
<IconMessage size={16} stroke={2} />
|
<IconMessage size={16} stroke={2} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import type { Editor } from "@tiptap/react";
|
import type { Editor } from "@tiptap/react";
|
||||||
import { useEditorState } from "@tiptap/react";
|
import { useEditorState } from "@tiptap/react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { isEditorReady } from "@docmost/editor-ext";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import classes from "./bubble-menu.module.css";
|
import classes from "./bubble-menu.module.css";
|
||||||
|
|
||||||
@@ -253,6 +254,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
|||||||
<SimpleGrid cols={5} spacing="xs">
|
<SimpleGrid cols={5} spacing="xs">
|
||||||
{TEXT_COLORS.map(({ name, color }, index) => {
|
{TEXT_COLORS.map(({ name, color }, index) => {
|
||||||
const applyTextColor = () => {
|
const applyTextColor = () => {
|
||||||
|
if (!isEditorReady(editor)) return;
|
||||||
if (name === "Default") {
|
if (name === "Default") {
|
||||||
editor.commands.unsetColor();
|
editor.commands.unsetColor();
|
||||||
} else {
|
} else {
|
||||||
@@ -316,6 +318,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
|||||||
<SimpleGrid cols={5} spacing="xs">
|
<SimpleGrid cols={5} spacing="xs">
|
||||||
{HIGHLIGHT_COLORS.map(({ name, color }, index) => {
|
{HIGHLIGHT_COLORS.map(({ name, color }, index) => {
|
||||||
const applyHighlight = () => {
|
const applyHighlight = () => {
|
||||||
|
if (!isEditorReady(editor)) return;
|
||||||
if (name === "Default") {
|
if (name === "Default") {
|
||||||
editor.commands.unsetHighlight();
|
editor.commands.unsetHighlight();
|
||||||
} else {
|
} else {
|
||||||
@@ -386,8 +389,10 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
|||||||
data-color-grid="remove"
|
data-color-grid="remove"
|
||||||
className={classes.removeColor}
|
className={classes.removeColor}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.commands.unsetColor();
|
if (isEditorReady(editor)) {
|
||||||
editor.commands.unsetHighlight();
|
editor.commands.unsetColor();
|
||||||
|
editor.commands.unsetHighlight();
|
||||||
|
}
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Popover, Button, ScrollArea, Tooltip } from "@mantine/core";
|
|||||||
import type { Editor } from "@tiptap/react";
|
import type { Editor } from "@tiptap/react";
|
||||||
import { useEditorState } from "@tiptap/react";
|
import { useEditorState } from "@tiptap/react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { isEditorReady } from "@docmost/editor-ext";
|
||||||
import classes from "./bubble-menu.module.css";
|
import classes from "./bubble-menu.module.css";
|
||||||
|
|
||||||
interface NodeSelectorProps {
|
interface NodeSelectorProps {
|
||||||
@@ -193,7 +194,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
|||||||
justify="left"
|
justify="left"
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
item.command();
|
if (isEditorReady(editor)) item.command();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
style={{ border: "none" }}
|
style={{ border: "none" }}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Menu, Button, Tooltip, rem } from "@mantine/core";
|
|||||||
import type { Editor } from "@tiptap/react";
|
import type { Editor } from "@tiptap/react";
|
||||||
import { useEditorState } from "@tiptap/react";
|
import { useEditorState } from "@tiptap/react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { isEditorReady } from "@docmost/editor-ext";
|
||||||
|
|
||||||
interface TextAlignmentProps {
|
interface TextAlignmentProps {
|
||||||
editor: Editor | null;
|
editor: Editor | null;
|
||||||
@@ -117,7 +118,7 @@ export const TextAlignmentSelector: FC<TextAlignmentProps> = ({
|
|||||||
activeItem.name === item.name ? <IconCheck size={16} /> : null
|
activeItem.name === item.name ? <IconCheck size={16} /> : null
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
item.command();
|
if (isEditorReady(editor)) item.command();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ async function reuploadPastedAttachments(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (reuploadResults.size === 0) return;
|
if (reuploadResults.size === 0) return;
|
||||||
|
if (editor.isDestroyed) return;
|
||||||
|
|
||||||
editor.chain().command(({ tr }) => {
|
editor.chain().command(({ tr }) => {
|
||||||
const sorted = [...nodesToReupload].sort((a, b) => b.pos - a.pos);
|
const sorted = [...nodesToReupload].sort((a, b) => b.pos - a.pos);
|
||||||
|
|||||||
+5
-5
@@ -25,11 +25,11 @@ export const BlockTypeGroup: FC<Props> = ({ editor }) => {
|
|||||||
const state = useEditorState({
|
const state = useEditorState({
|
||||||
editor,
|
editor,
|
||||||
selector: (ctx) => ({
|
selector: (ctx) => ({
|
||||||
isHeading1: ctx.editor.isActive("heading", { level: 1 }),
|
isHeading1: !!ctx.editor?.isActive("heading", { level: 1 }),
|
||||||
isHeading2: ctx.editor.isActive("heading", { level: 2 }),
|
isHeading2: !!ctx.editor?.isActive("heading", { level: 2 }),
|
||||||
isHeading3: ctx.editor.isActive("heading", { level: 3 }),
|
isHeading3: !!ctx.editor?.isActive("heading", { level: 3 }),
|
||||||
isBlockquote: ctx.editor.isActive("blockquote"),
|
isBlockquote: !!ctx.editor?.isActive("blockquote"),
|
||||||
isCodeBlock: ctx.editor.isActive("codeBlock"),
|
isCodeBlock: !!ctx.editor?.isActive("codeBlock"),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface ToolbarState {
|
|||||||
// static editor (mainExtensions only, undoRedo disabled), neither is loaded
|
// static editor (mainExtensions only, undoRedo disabled), neither is loaded
|
||||||
// and editor.can().undo/redo is undefined.
|
// and editor.can().undo/redo is undefined.
|
||||||
function safeCan(editor: Editor, command: "undo" | "redo"): boolean {
|
function safeCan(editor: Editor, command: "undo" | "redo"): boolean {
|
||||||
const can = editor.can() as Record<string, unknown>;
|
const can = editor?.can() as Record<string, unknown>;
|
||||||
const fn = can[command];
|
const fn = can[command];
|
||||||
return typeof fn === "function" ? (fn as () => boolean)() : false;
|
return typeof fn === "function" ? (fn as () => boolean)() : false;
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export function useToolbarState(editor: Editor | null): ToolbarState | null {
|
|||||||
return useEditorState({
|
return useEditorState({
|
||||||
editor,
|
editor,
|
||||||
selector: (ctx) => {
|
selector: (ctx) => {
|
||||||
if (!ctx.editor) return null;
|
if (!ctx.editor || ctx.editor.isDestroyed) return null;
|
||||||
return {
|
return {
|
||||||
isBold: ctx.editor.isActive("bold"),
|
isBold: ctx.editor.isActive("bold"),
|
||||||
isItalic: ctx.editor.isActive("italic"),
|
isItalic: ctx.editor.isActive("italic"),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const recalculateLinks = (nodePos: NodePos[]) => {
|
|||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
const label = item.node.textContent;
|
const label = item.node.textContent;
|
||||||
const level = Number(item.node.attrs.level);
|
const level = Number(item.node.attrs.level);
|
||||||
if (label.length && level <= 4) {
|
if (label.length && level <= 6) {
|
||||||
acc.push({
|
acc.push({
|
||||||
label,
|
label,
|
||||||
level,
|
level,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useTableHandleDrag } from "./hooks/use-table-handle-drag";
|
import { useTableHandleDrag } from "./hooks/use-table-handle-drag";
|
||||||
import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle";
|
import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle";
|
||||||
import { ColumnHandleMenu } from "./menus/column-handle-menu";
|
import { ColumnHandleMenu } from "./menus/column-handle-menu";
|
||||||
|
import { isEditorReady } from "@docmost/editor-ext";
|
||||||
import classes from "./handle.module.css";
|
import classes from "./handle.module.css";
|
||||||
|
|
||||||
interface ColumnHandleProps {
|
interface ColumnHandleProps {
|
||||||
@@ -35,7 +36,9 @@ export const ColumnHandle = React.memo(function ColumnHandle({
|
|||||||
// an external drop reflows the doc before the plugin re-emits
|
// an external drop reflows the doc before the plugin re-emits
|
||||||
// hoveringCell), it can resolve to a Text node, on which `.closest` is
|
// hoveringCell), it can resolve to a Text node, on which `.closest` is
|
||||||
// undefined. Filter to HTMLElement so downstream consumers stay safe.
|
// undefined. Filter to HTMLElement so downstream consumers stay safe.
|
||||||
const lookupDom = editor.view.nodeDOM(anchorPos);
|
const lookupDom = isEditorReady(editor)
|
||||||
|
? editor.view.nodeDOM(anchorPos)
|
||||||
|
: null;
|
||||||
const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null;
|
const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null;
|
||||||
const [cellDom, setCellDom] = useState<HTMLElement | null>(lookupCellDom);
|
const [cellDom, setCellDom] = useState<HTMLElement | null>(lookupCellDom);
|
||||||
const lastCellDomRef = useRef<HTMLElement | null>(lookupCellDom);
|
const lastCellDomRef = useRef<HTMLElement | null>(lookupCellDom);
|
||||||
|
|||||||
@@ -101,14 +101,14 @@ export const CellChevronMenu = React.memo(function CellChevronMenu({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<IconBoxMargin size={16} />}
|
leftSection={<IconBoxMargin size={16} />}
|
||||||
onClick={() => editor.chain().focus().mergeCells().run()}
|
onClick={() => editor.chain().focus().mergeCells().run()}
|
||||||
disabled={!editor.can().mergeCells()}
|
disabled={!editor?.can().mergeCells()}
|
||||||
>
|
>
|
||||||
{t("Merge cells")}
|
{t("Merge cells")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<IconSquareToggle size={16} />}
|
leftSection={<IconSquareToggle size={16} />}
|
||||||
onClick={() => editor.chain().focus().splitCell().run()}
|
onClick={() => editor.chain().focus().splitCell().run()}
|
||||||
disabled={!editor.can().splitCell()}
|
disabled={!editor?.can().splitCell()}
|
||||||
>
|
>
|
||||||
{t("Split cell")}
|
{t("Split cell")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useTableHandleDrag } from "./hooks/use-table-handle-drag";
|
import { useTableHandleDrag } from "./hooks/use-table-handle-drag";
|
||||||
import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle";
|
import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle";
|
||||||
import { RowHandleMenu } from "./menus/row-handle-menu";
|
import { RowHandleMenu } from "./menus/row-handle-menu";
|
||||||
|
import { isEditorReady } from "@docmost/editor-ext";
|
||||||
import classes from "./handle.module.css";
|
import classes from "./handle.module.css";
|
||||||
|
|
||||||
interface RowHandleProps {
|
interface RowHandleProps {
|
||||||
@@ -33,7 +34,9 @@ export const RowHandle = React.memo(function RowHandle({
|
|||||||
// an external drop reflows the doc before the plugin re-emits
|
// an external drop reflows the doc before the plugin re-emits
|
||||||
// hoveringCell), it can resolve to a Text node, on which `.closest` is
|
// hoveringCell), it can resolve to a Text node, on which `.closest` is
|
||||||
// undefined. Filter to HTMLElement so downstream consumers stay safe.
|
// undefined. Filter to HTMLElement so downstream consumers stay safe.
|
||||||
const lookupDom = editor.view.nodeDOM(anchorPos);
|
const lookupDom = isEditorReady(editor)
|
||||||
|
? editor.view.nodeDOM(anchorPos)
|
||||||
|
: null;
|
||||||
const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null;
|
const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null;
|
||||||
const [cellDom, setCellDom] = useState<HTMLElement | null>(lookupCellDom);
|
const [cellDom, setCellDom] = useState<HTMLElement | null>(lookupCellDom);
|
||||||
const lastCellDomRef = useRef<HTMLElement | null>(lookupCellDom);
|
const lastCellDomRef = useRef<HTMLElement | null>(lookupCellDom);
|
||||||
|
|||||||
+1
@@ -105,6 +105,7 @@ function TransclusionReferenceBody({
|
|||||||
sourcePageId,
|
sourcePageId,
|
||||||
transclusionId,
|
transclusionId,
|
||||||
});
|
});
|
||||||
|
if (editor.isDestroyed) return;
|
||||||
const pos = getPos();
|
const pos = getPos();
|
||||||
if (typeof pos !== "number") return;
|
if (typeof pos !== "number") return;
|
||||||
const from = pos;
|
const from = pos;
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ export const useEditorScroll = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (editor.isDestroyed) {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const dom = editor.view.dom.querySelector(`[id="${targetId}"], [data-id="${targetId}"]`);
|
const dom = editor.view.dom.querySelector(`[id="${targetId}"], [data-id="${targetId}"]`);
|
||||||
if (dom) {
|
if (dom) {
|
||||||
dom.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
dom.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
|||||||
@@ -468,7 +468,9 @@ export default function PageEditor({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => editor.commands.focus("end")}
|
onClick={() => {
|
||||||
|
if (editor && !editor.isDestroyed) editor.commands.focus("end");
|
||||||
|
}}
|
||||||
style={{ paddingBottom: "20vh" }}
|
style={{ paddingBottom: "20vh" }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ export function useHistoryRestore() {
|
|||||||
|
|
||||||
const handleRestore = useCallback(() => {
|
const handleRestore = useCallback(() => {
|
||||||
if (!activeHistoryData) return;
|
if (!activeHistoryData) return;
|
||||||
|
if (
|
||||||
|
!mainEditor ||
|
||||||
|
mainEditor.isDestroyed ||
|
||||||
|
!mainEditorTitle ||
|
||||||
|
mainEditorTitle.isDestroyed
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
mainEditorTitle
|
mainEditorTitle
|
||||||
.chain()
|
.chain()
|
||||||
|
|||||||
Reference in New Issue
Block a user