feat: Migrate tippy.js menus to Floating UI

This commit is contained in:
Arek Nawo
2026-01-09 00:33:28 +01:00
parent 601ed88931
commit 974bcea690
12 changed files with 250 additions and 588 deletions
-1
View File
@@ -55,7 +55,6 @@
"react-router-dom": "^7.0.1",
"semver": "^7.7.2",
"socket.io-client": "^4.8.1",
"tippy.js": "^6.3.7",
"tiptap-extension-global-drag-handle": "^0.1.18",
"zod": "^3.25.76"
},
@@ -1,16 +1,41 @@
import { ReactRenderer, useEditor } from "@tiptap/react";
import EmojiList from "./emoji-list";
import tippy from "tippy.js";
import { init } from "emoji-mart";
import {
autoUpdate,
computePosition,
flip,
offset,
shift,
} from "@floating-ui/dom";
const renderEmojiItems = () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
let popup: HTMLDivElement | null = null;
let cleanup: (() => void) | null = null;
let getReferenceClientRect: (() => DOMRect) | null = null;
const destroy = () => {
if (cleanup) {
cleanup();
cleanup = null;
}
if (popup) {
popup.remove();
popup = null;
}
if (component) {
component.destroy();
component = null;
}
};
return {
onBeforeStart: (props: {
editor: ReturnType<typeof useEditor>;
clientRect: DOMRect;
clientRect: () => DOMRect;
}) => {
init({
data: async () => (await import("@emoji-mart/data")).default,
@@ -25,51 +50,61 @@ const renderEmojiItems = () => {
return;
}
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom",
getReferenceClientRect = props.clientRect;
popup = document.createElement("div");
popup.style.zIndex = "9999";
popup.style.position = "absolute";
popup.style.top = "0";
popup.style.left = "0";
popup.appendChild(component.element);
document.body.appendChild(popup);
const virtualElement = {
getBoundingClientRect: () => {
return getReferenceClientRect
? getReferenceClientRect()
: new DOMRect(0, 0, 0, 0);
},
};
cleanup = autoUpdate(virtualElement, popup, () => {
if (!popup) return;
computePosition(virtualElement, popup, {
placement: "bottom-start",
middleware: [offset(10), flip(), shift()],
}).then(({ x, y }) => {
if (!popup) return;
Object.assign(popup.style, {
transform: `translate(${x}px, ${y}px)`,
});
});
});
},
onStart: (props: {
editor: ReturnType<typeof useEditor>;
clientRect: DOMRect;
clientRect: () => DOMRect;
}) => {
component?.updateProps({...props, isLoading: false});
component?.updateProps({ ...props, isLoading: false });
if (!props.clientRect) {
return;
if (props.clientRect) {
getReferenceClientRect = props.clientRect;
}
popup &&
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onUpdate: (props: {
editor: ReturnType<typeof useEditor>;
clientRect: DOMRect;
clientRect: () => DOMRect;
}) => {
component?.updateProps(props);
if (!props.clientRect) {
return;
if (props.clientRect) {
getReferenceClientRect = props.clientRect;
}
popup &&
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key === "Escape") {
popup?.[0].hide();
component?.destroy()
destroy();
return true;
}
@@ -78,13 +113,7 @@ const renderEmojiItems = () => {
return component?.ref?.onKeyDown(props);
},
onExit: () => {
if (popup && !popup[0]?.state.isDestroyed) {
popup[0]?.destroy();
}
if (component) {
component?.destroy();
}
destroy();
},
};
};
@@ -1,5 +1,11 @@
import { ReactRenderer, useEditor } from "@tiptap/react";
import tippy from "tippy.js";
import {
autoUpdate,
computePosition,
flip,
offset,
shift,
} from "@floating-ui/dom";
import MentionList from "@/features/editor/components/mention/mention-list.tsx";
function getWhitespaceCount(query: string) {
@@ -9,16 +15,27 @@ function getWhitespaceCount(query: string) {
const mentionRenderItems = () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
let activeClientRect: (() => DOMRect) | null = null;
let updatePositionCleanup: (() => void) | null = null;
const destroy = () => {
updatePositionCleanup?.();
updatePositionCleanup = null;
component?.destroy();
if (component?.element?.parentNode) {
component.element.parentNode.removeChild(component.element);
}
component = null;
};
return {
onStart: (props: {
editor: ReturnType<typeof useEditor>;
clientRect: DOMRect;
clientRect: () => DOMRect;
query: string;
}) => {
// query must not start with a whitespace
if (props.query.charAt(0) === ' '){
if (props.query.charAt(0) === " ") {
return;
}
@@ -37,37 +54,63 @@ const mentionRenderItems = () => {
return;
}
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
activeClientRect = props.clientRect;
const { element } = component;
document.body.appendChild(element);
updatePositionCleanup = autoUpdate(
{
getBoundingClientRect: () =>
activeClientRect ? activeClientRect() : new DOMRect(),
},
element,
() => {
if (!component?.element) return;
computePosition(
{
getBoundingClientRect: () => {
return activeClientRect ? activeClientRect() : new DOMRect();
},
},
element,
{
placement: "bottom-start",
middleware: [offset(0), flip(), shift()],
}
).then(({ x, y }) => {
Object.assign(element.style, {
left: `${x}px`,
top: `${y}px`,
position: "absolute",
zIndex: "9999",
});
});
}
);
},
onUpdate: (props: {
editor: ReturnType<typeof useEditor>;
clientRect: DOMRect;
clientRect: () => DOMRect;
query: string;
}) => {
// query must not start with a whitespace
if (props.query.charAt(0) === ' '){
component?.destroy();
if (props.query.charAt(0) === " ") {
destroy();
return;
}
// only update component if popup is not destroyed
if (!popup?.[0].state.isDestroyed) {
component?.updateProps(props);
if (component) {
component.updateProps(props);
}
if (!props || !props.clientRect) {
return;
}
activeClientRect = props.clientRect;
const whitespaceCount = getWhitespaceCount(props.query);
// destroy component if space is greater 3 without a match
@@ -76,37 +119,23 @@ const mentionRenderItems = () => {
//@ts-ignore
props.editor.storage.mentionItems.length === 0
) {
popup?.[0]?.destroy();
component?.destroy();
destroy();
return;
}
popup &&
!popup?.[0].state.isDestroyed &&
popup?.[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key)
if (
props.event.key === "Escape" ||
(props.event.key === "Enter" && !popup?.[0].state.isShown)
(props.event.key === "Enter" && !component)
) {
popup?.[0].destroy();
component?.destroy();
destroy();
return false;
}
return (component?.ref as any)?.onKeyDown(props);
},
onExit: () => {
if (popup && !popup?.[0].state.isDestroyed) {
popup[0].destroy();
}
if (component) {
component.destroy();
}
destroy();
},
};
};
@@ -1,10 +1,35 @@
import { ReactRenderer, useEditor } from "@tiptap/react";
import CommandList from "@/features/editor/components/slash-menu/command-list";
import tippy from "tippy.js";
import {
autoUpdate,
computePosition,
flip,
offset,
shift,
} from "@floating-ui/dom";
const renderItems = () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
let popup: HTMLElement | null = null;
let cleanup: (() => void) | null = null;
let getReferenceClientRect: (() => DOMRect) | null = null;
const updatePosition = () => {
if (!popup || !getReferenceClientRect) return;
// @ts-ignore
const rect = getReferenceClientRect();
computePosition({ getBoundingClientRect: () => rect }, popup, {
placement: "bottom-start",
middleware: [offset(0), flip(), shift()],
}).then(({ x, y }) => {
if (popup) {
popup.style.left = `${x}px`;
popup.style.top = `${y}px`;
}
});
};
return {
onStart: (props: {
@@ -21,15 +46,29 @@ const renderItems = () => {
}
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
getReferenceClientRect = props.clientRect;
popup = document.createElement("div");
popup.style.zIndex = "9999";
popup.style.position = "absolute";
popup.style.top = "0";
popup.style.left = "0";
document.body.appendChild(popup);
popup.appendChild(component.element);
cleanup = autoUpdate(
// @ts-ignore
{
getBoundingClientRect: () => {
return getReferenceClientRect
? getReferenceClientRect()
: new DOMRect();
},
},
popup,
updatePosition
);
},
onUpdate: (props: {
editor: ReturnType<typeof useEditor>;
@@ -41,14 +80,15 @@ const renderItems = () => {
return;
}
popup &&
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
// @ts-ignore
getReferenceClientRect = props.clientRect;
updatePosition();
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key === "Escape") {
popup?.[0].hide();
if (popup) {
popup.style.display = "none";
}
return true;
}
@@ -57,12 +97,19 @@ const renderItems = () => {
return component?.ref?.onKeyDown(props);
},
onExit: () => {
if (popup && !popup[0].state.isDestroyed) {
popup[0].destroy();
if (cleanup) {
cleanup();
cleanup = null;
}
if (popup) {
popup.remove();
popup = null;
}
if (component) {
component.destroy();
component = null;
}
},
};
@@ -29,7 +29,7 @@ export const SubpagesMenu = React.memo(
return editor.isActive("subpages");
},
[editor],
[editor]
);
const getReferenceClientRect = useCallback(() => {
@@ -60,16 +60,6 @@ export const SubpagesMenu = React.memo(
editor={editor}
pluginKey={`subpages-menu`}
updateDelay={0}
/* tippyOptions={{
getReferenceClientRect,
offset: [0, 8],
zIndex: 99,
popperOptions: {
modifiers: [{ name: "flip", enabled: false }],
},
plugins: [sticky],
sticky: "popper",
}}*/
shouldShow={shouldShow}
>
<Tooltip position="top" label={t("Delete")}>
@@ -85,7 +75,7 @@ export const SubpagesMenu = React.memo(
</Tooltip>
</BaseBubbleMenu>
);
},
}
);
export default SubpagesMenu;
@@ -3,7 +3,7 @@ import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { isCellSelection, TiptapTippyBubbleMenu } from '@docmost/editor-ext';
import { isCellSelection } from "@docmost/editor-ext";
import { ActionIcon, Tooltip } from "@mantine/core";
import {
IconBoxMargin,
@@ -15,6 +15,7 @@ import {
import { useTranslation } from "react-i18next";
import { TableBackgroundColor } from "./table-background-color";
import { TableTextAlignment } from "./table-text-alignment";
import { BubbleMenu } from "@tiptap/react/menus";
export const TableCellMenu = React.memo(
({ editor, appendTo }: EditorMenuProps): JSX.Element => {
@@ -27,7 +28,7 @@ export const TableCellMenu = React.memo(
return isCellSelection(state.selection);
},
[editor],
[editor]
);
const mergeCells = useCallback(() => {
@@ -51,16 +52,20 @@ export const TableCellMenu = React.memo(
}, [editor]);
return (
<TiptapTippyBubbleMenu
<BubbleMenu
editor={editor}
pluginKey="table-cell-menu"
updateDelay={0}
tippyOptions={{
appendTo: () => {
return appendTo?.current;
appendTo={() => {
return appendTo?.current;
}}
ref={(element) => {
element.style.zIndex = "99";
}}
options={{
offset: {
mainAxis: 15,
},
offset: [0, 15],
zIndex: 99,
}}
shouldShow={shouldShow}
>
@@ -123,9 +128,9 @@ export const TableCellMenu = React.memo(
</ActionIcon>
</Tooltip>
</ActionIcon.Group>
</TiptapTippyBubbleMenu>
</BubbleMenu>
);
},
}
);
export default TableCellMenu;
@@ -17,7 +17,8 @@ import {
IconTableRow,
IconTrashX,
} from "@tabler/icons-react";
import { isCellSelection, TiptapTippyBubbleMenu } from "@docmost/editor-ext";
import { BubbleMenu } from "@tiptap/react/menus";
import { isCellSelection } from "@docmost/editor-ext";
import { useTranslation } from "react-i18next";
export const TableMenu = React.memo(
@@ -31,20 +32,28 @@ export const TableMenu = React.memo(
return editor.isActive("table") && !isCellSelection(state.selection);
},
[editor],
[editor]
);
const getReferenceClientRect = useCallback(() => {
const getReferencedVirtualElement = useCallback(() => {
const { selection } = editor.state;
const predicate = (node: PMNode) => node.type.name === "table";
const parent = findParentNode(predicate)(selection);
if (parent) {
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
return dom.getBoundingClientRect();
const rect = dom.getBoundingClientRect();
return {
getBoundingClientRect: () => rect,
getClientRects: () => [rect],
};
}
return posToDOMRect(editor.view, selection.from, selection.to);
const rect = posToDOMRect(editor.view, selection.from, selection.to);
return {
getBoundingClientRect: () => rect,
getClientRects: () => [rect],
};
}, [editor]);
const toggleHeaderColumn = useCallback(() => {
@@ -84,35 +93,27 @@ export const TableMenu = React.memo(
}, [editor]);
return (
<TiptapTippyBubbleMenu
<BubbleMenu
editor={editor}
pluginKey="table-menu"
updateDelay={0}
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 },
},
},
],
resizeDelay={0}
getReferencedVirtualElement={getReferencedVirtualElement}
ref={(element) => {
element.style.zIndex = "99";
}}
options={{
placement: "top",
offset: {
mainAxis: 15,
},
flip: {
fallbackPlacements: ["top", "bottom"],
padding: { top: 35 + 15, left: 8, right: 8, bottom: -Infinity },
boundary: editor.options.element as HTMLElement,
},
shift: {
padding: 8 + 15,
crossAxis: true,
},
}}
shouldShow={shouldShow}
@@ -218,9 +219,9 @@ export const TableMenu = React.memo(
</ActionIcon>
</Tooltip>
</ActionIcon.Group>
</TiptapTippyBubbleMenu>
</BubbleMenu>
);
},
}
);
export default TableMenu;
-1
View File
@@ -23,4 +23,3 @@ export * from "./lib/subpages";
export * from "./lib/highlight";
export * from "./lib/heading/heading";
export * from "./lib/unique-id";
export * from "./lib/tippy-bubble-menu";
@@ -1,349 +0,0 @@
// 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 }),
});
};
@@ -1,72 +0,0 @@
// 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>
);
};
@@ -1 +0,0 @@
export { BubbleMenu as TiptapTippyBubbleMenu } from "./bubble-menu-react";
-15
View File
@@ -337,9 +337,6 @@ importers:
socket.io-client:
specifier: ^4.8.1
version: 4.8.1
tippy.js:
specifier: ^6.3.7
version: 6.3.7
tiptap-extension-global-drag-handle:
specifier: ^0.1.18
version: 0.1.18
@@ -3350,9 +3347,6 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
'@radix-ui/primitive@1.1.1':
resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==}
@@ -9423,9 +9417,6 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tippy.js@6.3.7:
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
tiptap-extension-global-drag-handle@0.1.18:
resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==}
@@ -13620,8 +13611,6 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@popperjs/core@2.11.8': {}
'@radix-ui/primitive@1.1.1': {}
'@radix-ui/react-arrow@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
@@ -20710,10 +20699,6 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tippy.js@6.3.7:
dependencies:
'@popperjs/core': 2.11.8
tiptap-extension-global-drag-handle@0.1.18: {}
tldts-core@6.1.72: {}