mirror of
https://github.com/docmost/docmost.git
synced 2026-05-21 01:04:39 +08:00
fix bubble menu
This commit is contained in:
@@ -147,7 +147,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu {...bubbleMenuProps}>
|
<BubbleMenu {...bubbleMenuProps} style={{ zIndex: 200, position: "relative"}}>
|
||||||
<div className={classes.bubbleMenu}>
|
<div className={classes.bubbleMenu}>
|
||||||
<NodeSelector
|
<NodeSelector
|
||||||
editor={props.editor}
|
editor={props.editor}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const shouldShow = useCallback(
|
const shouldShow = useCallback(
|
||||||
({ state }: ShouldShowProps) => {
|
({ state }: ShouldShowProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -50,17 +49,28 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
[editor],
|
[editor],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getReferenceClientRect = useCallback(() => {
|
const getReferencedVirtualElement = useCallback(() => {
|
||||||
const { selection } = editor.state;
|
const { selection } = editor.state;
|
||||||
const predicate = (node: PMNode) => node.type.name === "callout";
|
const predicate = (node: PMNode) => node.type.name === "callout";
|
||||||
const parent = findParentNode(predicate)(selection);
|
const parent = findParentNode(predicate)(selection);
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||||
return dom.getBoundingClientRect();
|
const domRect = dom.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
getBoundingClientRect: () => domRect,
|
||||||
|
getClientRects: () => [domRect],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
console.log('callout')
|
||||||
|
|
||||||
|
|
||||||
|
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
|
||||||
|
return {
|
||||||
|
getBoundingClientRect: () => domRect,
|
||||||
|
getClientRects: () => [domRect],
|
||||||
|
};
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
const setCalloutType = useCallback(
|
const setCalloutType = useCallback(
|
||||||
@@ -109,21 +119,13 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
|||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey={`callout-menu}`}
|
pluginKey={`callout-menu}`}
|
||||||
updateDelay={0}
|
updateDelay={0}
|
||||||
|
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||||
options={{
|
options={{
|
||||||
// getReferenceClientRect,
|
placement: "bottom",
|
||||||
placement: "right-end",
|
// offset: 233, // // offset: [0, 10],
|
||||||
// offset: 233,
|
|
||||||
// zIndex: 99,
|
// zIndex: 99,
|
||||||
flip: false,
|
flip: false,
|
||||||
}}
|
}}
|
||||||
//tippyOptions={{
|
|
||||||
// getReferenceClientRect,
|
|
||||||
// offset: [0, 10],
|
|
||||||
// placement: "bottom",
|
|
||||||
// zIndex: 99,
|
|
||||||
// popperOptions: {
|
|
||||||
// modifiers: [{ name: "flip", enabled: false }],
|
|
||||||
// },
|
|
||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<ActionIcon.Group className="actionIconGroup">
|
<ActionIcon.Group className="actionIconGroup">
|
||||||
|
|||||||
@@ -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 { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Node as PMNode } from "prosemirror-model";
|
import { Node as PMNode } from "prosemirror-model";
|
||||||
import {
|
import {
|
||||||
@@ -20,17 +20,40 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
[editor],
|
[editor],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getReferenceClientRect = useCallback(() => {
|
const editorState = useEditorState({
|
||||||
|
editor,
|
||||||
|
selector: (ctx) => {
|
||||||
|
if (!ctx.editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawioAttr = ctx.editor.getAttributes("drawio");
|
||||||
|
return {
|
||||||
|
isDrawio: ctx.editor.isActive("drawio"),
|
||||||
|
width: drawioAttr?.width ? parseInt(drawioAttr.width) : null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getReferencedVirtualElement = useCallback(() => {
|
||||||
const { selection } = editor.state;
|
const { selection } = editor.state;
|
||||||
const predicate = (node: PMNode) => node.type.name === "drawio";
|
const predicate = (node: PMNode) => node.type.name === "drawio";
|
||||||
const parent = findParentNode(predicate)(selection);
|
const parent = findParentNode(predicate)(selection);
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||||
return dom.getBoundingClientRect();
|
const domRect = dom.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
getBoundingClientRect: () => domRect,
|
||||||
|
getClientRects: () => [domRect],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
|
||||||
|
return {
|
||||||
|
getBoundingClientRect: () => domRect,
|
||||||
|
getClientRects: () => [domRect],
|
||||||
|
};
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
const onWidthChange = useCallback(
|
const onWidthChange = useCallback(
|
||||||
@@ -43,24 +66,14 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
return (
|
return (
|
||||||
<BaseBubbleMenu
|
<BaseBubbleMenu
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey={`drawio-menu}`}
|
pluginKey={`drawio-menu`}
|
||||||
updateDelay={0}
|
updateDelay={0}
|
||||||
|
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||||
options={{
|
options={{
|
||||||
//getReferenceClientRect,
|
placement: "top",
|
||||||
placement: "bottom",
|
|
||||||
offset: 8,
|
offset: 8,
|
||||||
// zIndex: 99,
|
|
||||||
flip: false,
|
flip: false,
|
||||||
}}
|
}}
|
||||||
// tippyOptions={{
|
|
||||||
// getReferenceClientRect,
|
|
||||||
// offset: [0, 8],
|
|
||||||
// zIndex: 99,
|
|
||||||
// popperOptions: {
|
|
||||||
// modifiers: [{ name: 'flip', enabled: false }],
|
|
||||||
// },
|
|
||||||
// plugins: [sticky],
|
|
||||||
// sticky: 'popper',
|
|
||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -70,11 +83,8 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{editor.getAttributes("drawio")?.width && (
|
{editorState?.width && (
|
||||||
<NodeWidthResize
|
<NodeWidthResize onChange={onWidthChange} value={editorState.width} />
|
||||||
onChange={onWidthChange}
|
|
||||||
value={parseInt(editor.getAttributes("drawio").width)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</BaseBubbleMenu>
|
</BaseBubbleMenu>
|
||||||
|
|||||||
@@ -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 { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Node as PMNode } from "prosemirror-model";
|
import { Node as PMNode } from "prosemirror-model";
|
||||||
import {
|
import {
|
||||||
@@ -22,17 +22,40 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
[editor],
|
[editor],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getReferenceClientRect = useCallback(() => {
|
const editorState = useEditorState({
|
||||||
|
editor,
|
||||||
|
selector: (ctx) => {
|
||||||
|
if (!ctx.editor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const excalidrawAttr = ctx.editor.getAttributes("excalidraw");
|
||||||
|
return {
|
||||||
|
isExcalidraw: ctx.editor.isActive("excalidraw"),
|
||||||
|
width: excalidrawAttr?.width ? parseInt(excalidrawAttr.width) : null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getReferencedVirtualElement = useCallback(() => {
|
||||||
const { selection } = editor.state;
|
const { selection } = editor.state;
|
||||||
const predicate = (node: PMNode) => node.type.name === "excalidraw";
|
const predicate = (node: PMNode) => node.type.name === "excalidraw";
|
||||||
const parent = findParentNode(predicate)(selection);
|
const parent = findParentNode(predicate)(selection);
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||||
return dom.getBoundingClientRect();
|
const domRect = dom.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
getBoundingClientRect: () => domRect,
|
||||||
|
getClientRects: () => [domRect],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
|
||||||
|
return {
|
||||||
|
getBoundingClientRect: () => domRect,
|
||||||
|
getClientRects: () => [domRect],
|
||||||
|
};
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
const onWidthChange = useCallback(
|
const onWidthChange = useCallback(
|
||||||
@@ -45,27 +68,14 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
return (
|
return (
|
||||||
<BaseBubbleMenu
|
<BaseBubbleMenu
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey={`excalidraw-menu}`}
|
pluginKey={`excalidraw-menu`}
|
||||||
updateDelay={0}
|
updateDelay={0}
|
||||||
|
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||||
options={{
|
options={{
|
||||||
//getReferenceClientRect,
|
placement: "top",
|
||||||
placement: "bottom",
|
|
||||||
offset: 8,
|
offset: 8,
|
||||||
// zIndex: 99,
|
|
||||||
// plugins: [sticky],
|
|
||||||
// sticky: 'popper',
|
|
||||||
flip: false,
|
flip: false,
|
||||||
}}
|
}}
|
||||||
// tippyOptions={{
|
|
||||||
// getReferenceClientRect,
|
|
||||||
// offset: [0, 8],
|
|
||||||
// zIndex: 99,
|
|
||||||
// popperOptions: {
|
|
||||||
// modifiers: [{ name: 'flip', enabled: false }],
|
|
||||||
// },
|
|
||||||
// plugins: [sticky],
|
|
||||||
// sticky: 'popper',
|
|
||||||
// }}
|
|
||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -75,11 +85,8 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{editor.getAttributes("excalidraw")?.width && (
|
{editorState?.width && (
|
||||||
<NodeWidthResize
|
<NodeWidthResize onChange={onWidthChange} value={editorState.width} />
|
||||||
onChange={onWidthChange}
|
|
||||||
value={parseInt(editor.getAttributes("excalidraw").width)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</BaseBubbleMenu>
|
</BaseBubbleMenu>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
|
|
||||||
const editorState = useEditorState({
|
const editorState = useEditorState({
|
||||||
editor,
|
editor,
|
||||||
selector: ctx => {
|
selector: (ctx) => {
|
||||||
if (!ctx.editor) {
|
if (!ctx.editor) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
isAlignLeft: ctx.editor.isActive("image", { align: "left" }),
|
isAlignLeft: ctx.editor.isActive("image", { align: "left" }),
|
||||||
isAlignCenter: ctx.editor.isActive("image", { align: "center" }),
|
isAlignCenter: ctx.editor.isActive("image", { align: "center" }),
|
||||||
isAlignRight: ctx.editor.isActive("image", { align: "right" }),
|
isAlignRight: ctx.editor.isActive("image", { align: "right" }),
|
||||||
imageWidth: imageAttrs?.width ? parseInt(imageAttrs.width) : null,
|
width: imageAttrs?.width ? parseInt(imageAttrs.width) : null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -48,17 +48,25 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
[editor],
|
[editor],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getReferenceClientRect = useCallback(() => {
|
const getReferencedVirtualElement = useCallback(() => {
|
||||||
const { selection } = editor.state;
|
const { selection } = editor.state;
|
||||||
const predicate = (node: PMNode) => node.type.name === "image";
|
const predicate = (node: PMNode) => node.type.name === "image";
|
||||||
const parent = findParentNode(predicate)(selection);
|
const parent = findParentNode(predicate)(selection);
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||||
return dom.getBoundingClientRect();
|
const domRect = dom.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
getBoundingClientRect: () => domRect,
|
||||||
|
getClientRects: () => [domRect],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
|
||||||
|
return {
|
||||||
|
getBoundingClientRect: () => domRect,
|
||||||
|
getClientRects: () => [domRect],
|
||||||
|
};
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
const alignImageLeft = useCallback(() => {
|
const alignImageLeft = useCallback(() => {
|
||||||
@@ -99,25 +107,14 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
return (
|
return (
|
||||||
<BaseBubbleMenu
|
<BaseBubbleMenu
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey={`image-menu}`}
|
pluginKey={`image-menu`}
|
||||||
updateDelay={0}
|
updateDelay={0}
|
||||||
|
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||||
options={{
|
options={{
|
||||||
// getReferenceClientRect,
|
placement: "top",
|
||||||
placement: "bottom",
|
|
||||||
offset: 8,
|
offset: 8,
|
||||||
//zIndex: 99,
|
|
||||||
flip: false,
|
flip: false,
|
||||||
}}
|
}}
|
||||||
// tippyOptions={{
|
|
||||||
// getReferenceClientRect,
|
|
||||||
// offset: [0, 8],
|
|
||||||
// zIndex: 99,
|
|
||||||
// popperOptions: {
|
|
||||||
// modifiers: [{ name: "flip", enabled: false }],
|
|
||||||
// },
|
|
||||||
// plugins: [sticky],
|
|
||||||
// sticky: "popper",
|
|
||||||
// }}
|
|
||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<ActionIcon.Group className="actionIconGroup">
|
<ActionIcon.Group className="actionIconGroup">
|
||||||
@@ -126,9 +123,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
onClick={alignImageLeft}
|
onClick={alignImageLeft}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Align left")}
|
aria-label={t("Align left")}
|
||||||
variant={
|
variant={editorState?.isAlignLeft ? "light" : "default"}
|
||||||
editorState?.isAlignLeft ? "light" : "default"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<IconLayoutAlignLeft size={18} />
|
<IconLayoutAlignLeft size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -139,9 +134,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
onClick={alignImageCenter}
|
onClick={alignImageCenter}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Align center")}
|
aria-label={t("Align center")}
|
||||||
variant={
|
variant={editorState?.isAlignCenter ? "light" : "default"}
|
||||||
editorState?.isAlignCenter ? "light" : "default"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<IconLayoutAlignCenter size={18} />
|
<IconLayoutAlignCenter size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -152,20 +145,15 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
onClick={alignImageRight}
|
onClick={alignImageRight}
|
||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t("Align right")}
|
aria-label={t("Align right")}
|
||||||
variant={
|
variant={editorState?.isAlignRight ? "light" : "default"}
|
||||||
editorState?.isAlignRight ? "light" : "default"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<IconLayoutAlignRight size={18} />
|
<IconLayoutAlignRight size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ActionIcon.Group>
|
</ActionIcon.Group>
|
||||||
|
|
||||||
{editorState?.imageWidth && (
|
{editorState?.width && (
|
||||||
<NodeWidthResize
|
<NodeWidthResize onChange={onWidthChange} value={editorState.width} />
|
||||||
onChange={onWidthChange}
|
|
||||||
value={editorState.imageWidth}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</BaseBubbleMenu>
|
</BaseBubbleMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EditorMenuProps,
|
EditorMenuProps,
|
||||||
ShouldShowProps,
|
ShouldShowProps,
|
||||||
} from "@/features/editor/components/table/types/types.ts";
|
} from "@/features/editor/components/table/types/types.ts";
|
||||||
import { isCellSelection } from "@docmost/editor-ext";
|
import { isCellSelection, TiptapTippyBubbleMenu } from '@docmost/editor-ext';
|
||||||
import { ActionIcon, Tooltip } from "@mantine/core";
|
import { ActionIcon, Tooltip } from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconBoxMargin,
|
IconBoxMargin,
|
||||||
@@ -53,19 +51,17 @@ export const TableCellMenu = React.memo(
|
|||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseBubbleMenu
|
<TiptapTippyBubbleMenu
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="table-cell-menu"
|
pluginKey="table-cell-menu"
|
||||||
updateDelay={0}
|
updateDelay={0}
|
||||||
options={{
|
tippyOptions={{
|
||||||
//appendTo: () => {
|
appendTo: () => {
|
||||||
// return appendTo?.current;
|
return appendTo?.current;
|
||||||
// },
|
},
|
||||||
placement: "bottom",
|
offset: [0, 15],
|
||||||
offset: 15,
|
zIndex: 99,
|
||||||
//zIndex: 99,
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<ActionIcon.Group>
|
<ActionIcon.Group>
|
||||||
@@ -127,7 +123,7 @@ export const TableCellMenu = React.memo(
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ActionIcon.Group>
|
</ActionIcon.Group>
|
||||||
</BaseBubbleMenu>
|
</TiptapTippyBubbleMenu>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
|
||||||
import { posToDOMRect, findParentNode } from "@tiptap/react";
|
import { posToDOMRect, findParentNode } from "@tiptap/react";
|
||||||
import { Node as PMNode } from "@tiptap/pm/model";
|
import { Node as PMNode } from "@tiptap/pm/model";
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EditorMenuProps,
|
EditorMenuProps,
|
||||||
ShouldShowProps,
|
ShouldShowProps,
|
||||||
@@ -19,7 +17,7 @@ import {
|
|||||||
IconTableRow,
|
IconTableRow,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { isCellSelection } from "@docmost/editor-ext";
|
import { isCellSelection, TiptapTippyBubbleMenu } from "@docmost/editor-ext";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const TableMenu = React.memo(
|
export const TableMenu = React.memo(
|
||||||
@@ -86,42 +84,37 @@ export const TableMenu = React.memo(
|
|||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseBubbleMenu
|
<TiptapTippyBubbleMenu
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey="table-menu"
|
pluginKey="table-menu"
|
||||||
updateDelay={0}
|
updateDelay={0}
|
||||||
options={{
|
tippyOptions={{
|
||||||
placement: "bottom",
|
getReferenceClientRect: getReferenceClientRect,
|
||||||
offset: 15,
|
offset: [0, 15],
|
||||||
//zIndex: 99,
|
zIndex: 99,
|
||||||
|
popperOptions: {
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: "preventOverflow",
|
||||||
|
enabled: true,
|
||||||
|
options: {
|
||||||
|
altAxis: true,
|
||||||
|
boundary: "clippingParents",
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "flip",
|
||||||
|
enabled: true,
|
||||||
|
options: {
|
||||||
|
boundary: editor.options.element,
|
||||||
|
fallbackPlacements: ["top", "bottom"],
|
||||||
|
padding: { top: 35, left: 8, right: 8, bottom: -Infinity },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
//tippyOptions={{
|
|
||||||
// getReferenceClientRect: getReferenceClientRect,
|
|
||||||
// offset: [0, 15],
|
|
||||||
// zIndex: 99,
|
|
||||||
// popperOptions: {
|
|
||||||
// modifiers: [
|
|
||||||
// {
|
|
||||||
// name: "preventOverflow",
|
|
||||||
// enabled: true,
|
|
||||||
// options: {
|
|
||||||
// altAxis: true,
|
|
||||||
// boundary: "clippingParents",
|
|
||||||
// padding: 8,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "flip",
|
|
||||||
// enabled: true,
|
|
||||||
// options: {
|
|
||||||
// boundary: editor.options.element,
|
|
||||||
// fallbackPlacements: ["top", "bottom"],
|
|
||||||
// padding: { top: 35, left: 8, right: 8, bottom: -Infinity },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// }}
|
|
||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
>
|
>
|
||||||
<ActionIcon.Group>
|
<ActionIcon.Group>
|
||||||
@@ -225,7 +218,7 @@ export const TableMenu = React.memo(
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ActionIcon.Group>
|
</ActionIcon.Group>
|
||||||
</BaseBubbleMenu>
|
</TiptapTippyBubbleMenu>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,26 +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({
|
const editorState = useEditorState({
|
||||||
editor,
|
editor,
|
||||||
selector: ctx => {
|
selector: ctx => {
|
||||||
if (!ctx.editor) {
|
if (!ctx.editor) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoAttrs = ctx.editor.getAttributes("video");
|
const videoAttrs = ctx.editor.getAttributes("video");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isVideo: ctx.editor.isActive("video"),
|
isVideo: ctx.editor.isActive("video"),
|
||||||
isAlignLeft: ctx.editor.isActive("video", { align: "left" }),
|
isAlignLeft: ctx.editor.isActive("video", { align: "left" }),
|
||||||
isAlignCenter: ctx.editor.isActive("video", { align: "center" }),
|
isAlignCenter: ctx.editor.isActive("video", { align: "center" }),
|
||||||
isAlignRight: ctx.editor.isActive("video", { align: "right" }),
|
isAlignRight: ctx.editor.isActive("video", { align: "right" }),
|
||||||
videoWidth: videoAttrs?.width ? parseInt(videoAttrs.width) : null,
|
width: videoAttrs?.width ? parseInt(videoAttrs.width) : null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const shouldShow = useCallback(
|
const shouldShow = useCallback(
|
||||||
({ state }: ShouldShowProps) => {
|
({ state }: ShouldShowProps) => {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -48,17 +48,25 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
[editor],
|
[editor],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getReferenceClientRect = useCallback(() => {
|
const getReferencedVirtualElement = useCallback(() => {
|
||||||
const { selection } = editor.state;
|
const { selection } = editor.state;
|
||||||
const predicate = (node: PMNode) => node.type.name === "video";
|
const predicate = (node: PMNode) => node.type.name === "video";
|
||||||
const parent = findParentNode(predicate)(selection);
|
const parent = findParentNode(predicate)(selection);
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||||
return dom.getBoundingClientRect();
|
const domRect = dom.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
getBoundingClientRect: () => domRect,
|
||||||
|
getClientRects: () => [domRect],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
|
||||||
|
return {
|
||||||
|
getBoundingClientRect: () => domRect,
|
||||||
|
getClientRects: () => [domRect],
|
||||||
|
};
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
const alignVideoLeft = useCallback(() => {
|
const alignVideoLeft = useCallback(() => {
|
||||||
@@ -99,13 +107,12 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
return (
|
return (
|
||||||
<BaseBubbleMenu
|
<BaseBubbleMenu
|
||||||
editor={editor}
|
editor={editor}
|
||||||
pluginKey={`video-menu}`}
|
pluginKey={`video-menu`}
|
||||||
updateDelay={0}
|
updateDelay={0}
|
||||||
|
getReferencedVirtualElement={getReferencedVirtualElement}
|
||||||
options={{
|
options={{
|
||||||
//getReferenceClientRect,
|
placement: "top",
|
||||||
placement: "bottom",
|
|
||||||
offset: 8,
|
offset: 8,
|
||||||
//zIndex: 99,
|
|
||||||
flip: false,
|
flip: false,
|
||||||
}}
|
}}
|
||||||
shouldShow={shouldShow}
|
shouldShow={shouldShow}
|
||||||
@@ -151,10 +158,10 @@ export function VideoMenu({ editor }: EditorMenuProps) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ActionIcon.Group>
|
</ActionIcon.Group>
|
||||||
|
|
||||||
{editorState?.videoWidth && (
|
{editorState?.width && (
|
||||||
<NodeWidthResize
|
<NodeWidthResize
|
||||||
onChange={onWidthChange}
|
onChange={onWidthChange}
|
||||||
value={editorState.videoWidth}
|
value={editorState.width}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</BaseBubbleMenu>
|
</BaseBubbleMenu>
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export const mainExtensions = [
|
|||||||
}),
|
}),
|
||||||
CustomCodeBlock.configure({
|
CustomCodeBlock.configure({
|
||||||
view: CodeBlockView,
|
view: CodeBlockView,
|
||||||
|
//@ts-ignore
|
||||||
lowlight,
|
lowlight,
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ export * from "./lib/markdown";
|
|||||||
export * from "./lib/search-and-replace";
|
export * from "./lib/search-and-replace";
|
||||||
export * from "./lib/embed-provider";
|
export * from "./lib/embed-provider";
|
||||||
export * from "./lib/subpages";
|
export * from "./lib/subpages";
|
||||||
|
export * from "./lib/tippy-bubble-menu";
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
// Source: https://github.com/ueberdosis/tiptap/blob/v2/packages/extension-bubble-menu/src/bubble-menu-plugin.ts - MIT
|
||||||
|
import {
|
||||||
|
Editor,
|
||||||
|
isNodeSelection,
|
||||||
|
isTextSelection,
|
||||||
|
posToDOMRect,
|
||||||
|
} from "@tiptap/core";
|
||||||
|
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
|
import { EditorView } from "@tiptap/pm/view";
|
||||||
|
import tippy, { Instance, Props } from "tippy.js";
|
||||||
|
|
||||||
|
export interface BubbleMenuPluginProps {
|
||||||
|
/**
|
||||||
|
* The plugin key.
|
||||||
|
* @type {PluginKey | string}
|
||||||
|
* @default 'bubbleMenu'
|
||||||
|
*/
|
||||||
|
pluginKey: PluginKey | string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The editor instance.
|
||||||
|
*/
|
||||||
|
editor: Editor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DOM element that contains your menu.
|
||||||
|
* @type {HTMLElement}
|
||||||
|
* @default null
|
||||||
|
*/
|
||||||
|
element: HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options for the tippy.js instance.
|
||||||
|
* @see https://atomiks.github.io/tippyjs/v6/all-props/
|
||||||
|
*/
|
||||||
|
tippyOptions?: Partial<Props>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The delay in milliseconds before the menu should be updated.
|
||||||
|
* This can be useful to prevent performance issues.
|
||||||
|
* @type {number}
|
||||||
|
* @default 250
|
||||||
|
*/
|
||||||
|
updateDelay?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that determines whether the menu should be shown or not.
|
||||||
|
* If this function returns `false`, the menu will be hidden, otherwise it will be shown.
|
||||||
|
*/
|
||||||
|
shouldShow?:
|
||||||
|
| ((props: {
|
||||||
|
editor: Editor;
|
||||||
|
element: HTMLElement;
|
||||||
|
view: EditorView;
|
||||||
|
state: EditorState;
|
||||||
|
oldState?: EditorState;
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
}) => boolean)
|
||||||
|
| null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BubbleMenuViewProps = BubbleMenuPluginProps & {
|
||||||
|
view: EditorView;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BubbleMenuView {
|
||||||
|
public editor: Editor;
|
||||||
|
|
||||||
|
public element: HTMLElement;
|
||||||
|
|
||||||
|
public view: EditorView;
|
||||||
|
|
||||||
|
public preventHide = false;
|
||||||
|
|
||||||
|
public tippy: Instance | undefined;
|
||||||
|
|
||||||
|
public tippyOptions?: Partial<Props>;
|
||||||
|
|
||||||
|
public updateDelay: number;
|
||||||
|
|
||||||
|
private updateDebounceTimer: number | undefined;
|
||||||
|
|
||||||
|
public shouldShow: Exclude<BubbleMenuPluginProps["shouldShow"], null> = ({
|
||||||
|
view,
|
||||||
|
state,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}) => {
|
||||||
|
const { doc, selection } = state;
|
||||||
|
const { empty } = selection;
|
||||||
|
|
||||||
|
// Sometime check for `empty` is not enough.
|
||||||
|
// Doubleclick an empty paragraph returns a node size of 2.
|
||||||
|
// So we check also for an empty text size.
|
||||||
|
const isEmptyTextBlock =
|
||||||
|
!doc.textBetween(from, to).length && isTextSelection(state.selection);
|
||||||
|
|
||||||
|
// When clicking on a element inside the bubble menu the editor "blur" event
|
||||||
|
// is called and the bubble menu item is focussed. In this case we should
|
||||||
|
// consider the menu as part of the editor and keep showing the menu
|
||||||
|
const isChildOfMenu = this.element.contains(document.activeElement);
|
||||||
|
|
||||||
|
const hasEditorFocus = view.hasFocus() || isChildOfMenu;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!hasEditorFocus ||
|
||||||
|
empty ||
|
||||||
|
isEmptyTextBlock ||
|
||||||
|
!this.editor.isEditable
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
editor,
|
||||||
|
element,
|
||||||
|
view,
|
||||||
|
tippyOptions = {},
|
||||||
|
updateDelay = 250,
|
||||||
|
shouldShow,
|
||||||
|
}: BubbleMenuViewProps) {
|
||||||
|
this.editor = editor;
|
||||||
|
this.element = element;
|
||||||
|
this.view = view;
|
||||||
|
this.updateDelay = updateDelay;
|
||||||
|
|
||||||
|
if (shouldShow) {
|
||||||
|
this.shouldShow = shouldShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.element.addEventListener("mousedown", this.mousedownHandler, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
this.view.dom.addEventListener("dragstart", this.dragstartHandler);
|
||||||
|
this.editor.on("focus", this.focusHandler);
|
||||||
|
this.editor.on("blur", this.blurHandler);
|
||||||
|
this.tippyOptions = tippyOptions;
|
||||||
|
// Detaches menu content from its current parent
|
||||||
|
this.element.remove();
|
||||||
|
this.element.style.visibility = "visible";
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedownHandler = () => {
|
||||||
|
this.preventHide = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
dragstartHandler = () => {
|
||||||
|
this.hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
focusHandler = () => {
|
||||||
|
// we use `setTimeout` to make sure `selection` is already updated
|
||||||
|
setTimeout(() => this.update(this.editor.view));
|
||||||
|
};
|
||||||
|
|
||||||
|
blurHandler = ({ event }: { event: FocusEvent }) => {
|
||||||
|
if (this.preventHide) {
|
||||||
|
this.preventHide = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event?.relatedTarget &&
|
||||||
|
this.element.parentNode?.contains(event.relatedTarget as Node)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event?.relatedTarget === this.editor.view.dom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
tippyBlurHandler = (event: FocusEvent) => {
|
||||||
|
this.blurHandler({ event });
|
||||||
|
};
|
||||||
|
|
||||||
|
createTooltip() {
|
||||||
|
const { element: editorElement } = this.editor.options;
|
||||||
|
//@ts-ignore
|
||||||
|
const editorIsAttached = !!editorElement.parentElement;
|
||||||
|
|
||||||
|
this.element.tabIndex = 0;
|
||||||
|
|
||||||
|
if (this.tippy || !editorIsAttached) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
this.tippy = tippy(editorElement, {
|
||||||
|
duration: 0,
|
||||||
|
getReferenceClientRect: null,
|
||||||
|
content: this.element,
|
||||||
|
interactive: true,
|
||||||
|
trigger: "manual",
|
||||||
|
placement: "top",
|
||||||
|
hideOnClick: "toggle",
|
||||||
|
...this.tippyOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// maybe we have to hide tippy on its own blur event as well
|
||||||
|
if (this.tippy.popper.firstChild) {
|
||||||
|
(this.tippy.popper.firstChild as HTMLElement).addEventListener(
|
||||||
|
"blur",
|
||||||
|
this.tippyBlurHandler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(view: EditorView, oldState?: EditorState) {
|
||||||
|
const { state } = view;
|
||||||
|
const hasValidSelection = state.selection.from !== state.selection.to;
|
||||||
|
|
||||||
|
if (this.updateDelay > 0 && hasValidSelection) {
|
||||||
|
this.handleDebouncedUpdate(view, oldState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectionChanged = !oldState?.selection.eq(view.state.selection);
|
||||||
|
const docChanged = !oldState?.doc.eq(view.state.doc);
|
||||||
|
|
||||||
|
this.updateHandler(view, selectionChanged, docChanged, oldState);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDebouncedUpdate = (view: EditorView, oldState?: EditorState) => {
|
||||||
|
const selectionChanged = !oldState?.selection.eq(view.state.selection);
|
||||||
|
const docChanged = !oldState?.doc.eq(view.state.doc);
|
||||||
|
|
||||||
|
if (!selectionChanged && !docChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.updateDebounceTimer) {
|
||||||
|
clearTimeout(this.updateDebounceTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateDebounceTimer = window.setTimeout(() => {
|
||||||
|
this.updateHandler(view, selectionChanged, docChanged, oldState);
|
||||||
|
}, this.updateDelay);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateHandler = (
|
||||||
|
view: EditorView,
|
||||||
|
selectionChanged: boolean,
|
||||||
|
docChanged: boolean,
|
||||||
|
oldState?: EditorState,
|
||||||
|
) => {
|
||||||
|
const { state, composing } = view;
|
||||||
|
const { selection } = state;
|
||||||
|
|
||||||
|
const isSame = !selectionChanged && !docChanged;
|
||||||
|
|
||||||
|
if (composing || isSame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.createTooltip();
|
||||||
|
|
||||||
|
// support for CellSelections
|
||||||
|
const { ranges } = selection;
|
||||||
|
const from = Math.min(...ranges.map((range) => range.$from.pos));
|
||||||
|
const to = Math.max(...ranges.map((range) => range.$to.pos));
|
||||||
|
|
||||||
|
const shouldShow = this.shouldShow?.({
|
||||||
|
editor: this.editor,
|
||||||
|
element: this.element,
|
||||||
|
view,
|
||||||
|
state,
|
||||||
|
oldState,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shouldShow) {
|
||||||
|
this.hide();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tippy?.setProps({
|
||||||
|
getReferenceClientRect:
|
||||||
|
this.tippyOptions?.getReferenceClientRect ||
|
||||||
|
(() => {
|
||||||
|
if (isNodeSelection(state.selection)) {
|
||||||
|
let node = view.nodeDOM(from) as HTMLElement;
|
||||||
|
|
||||||
|
if (node) {
|
||||||
|
const nodeViewWrapper = node.dataset.nodeViewWrapper
|
||||||
|
? node
|
||||||
|
: node.querySelector("[data-node-view-wrapper]");
|
||||||
|
|
||||||
|
if (nodeViewWrapper) {
|
||||||
|
node = nodeViewWrapper.firstChild as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node) {
|
||||||
|
return node.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return posToDOMRect(view, from, to);
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.tippy?.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.tippy?.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this.tippy?.popper.firstChild) {
|
||||||
|
(this.tippy.popper.firstChild as HTMLElement).removeEventListener(
|
||||||
|
"blur",
|
||||||
|
this.tippyBlurHandler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.tippy?.destroy();
|
||||||
|
this.element.removeEventListener("mousedown", this.mousedownHandler, {
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
this.view.dom.removeEventListener("dragstart", this.dragstartHandler);
|
||||||
|
this.editor.off("focus", this.focusHandler);
|
||||||
|
this.editor.off("blur", this.blurHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BubbleMenuPlugin = (options: BubbleMenuPluginProps) => {
|
||||||
|
return new Plugin({
|
||||||
|
key:
|
||||||
|
typeof options.pluginKey === "string"
|
||||||
|
? new PluginKey(options.pluginKey)
|
||||||
|
: options.pluginKey,
|
||||||
|
view: (view) => new BubbleMenuView({ view, ...options }),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Source: https://github.com/ueberdosis/tiptap/blob/v2/packages/react/src/BubbleMenu.tsx - MIT
|
||||||
|
import { BubbleMenuPlugin, BubbleMenuPluginProps } from "./bubble-menu-plugin";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useCurrentEditor } from "@tiptap/react";
|
||||||
|
|
||||||
|
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||||
|
|
||||||
|
export type BubbleMenuProps = Omit<
|
||||||
|
Optional<BubbleMenuPluginProps, "pluginKey">,
|
||||||
|
"element" | "editor"
|
||||||
|
> & {
|
||||||
|
editor: BubbleMenuPluginProps["editor"] | null;
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
updateDelay?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BubbleMenu = (props: BubbleMenuProps) => {
|
||||||
|
const [element, setElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
const { editor: currentEditor } = useCurrentEditor();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.editor?.isDestroyed || currentEditor?.isDestroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
pluginKey = "bubbleMenu",
|
||||||
|
editor,
|
||||||
|
tippyOptions = {},
|
||||||
|
updateDelay,
|
||||||
|
shouldShow = null,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const menuEditor = editor || currentEditor;
|
||||||
|
|
||||||
|
if (!menuEditor) {
|
||||||
|
console.warn(
|
||||||
|
"BubbleMenu component is not rendered inside of an editor component or does not have editor prop.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin = BubbleMenuPlugin({
|
||||||
|
updateDelay,
|
||||||
|
editor: menuEditor,
|
||||||
|
element,
|
||||||
|
pluginKey,
|
||||||
|
shouldShow,
|
||||||
|
tippyOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
menuEditor.registerPlugin(plugin);
|
||||||
|
return () => {
|
||||||
|
menuEditor.unregisterPlugin(pluginKey);
|
||||||
|
};
|
||||||
|
}, [props.editor, currentEditor, element]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setElement}
|
||||||
|
className={props.className}
|
||||||
|
style={{ visibility: "hidden" }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { BubbleMenu as TiptapTippyBubbleMenu } from "./bubble-menu-react";
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
|
"jsx": "react-jsx",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
|
|||||||
Reference in New Issue
Block a user