Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1592b59
chore: upgrade to tiptap v3
aaryan610 Jul 31, 2025
2dbfb54
chore: update starter kit
aaryan610 Jul 31, 2025
c97d40f
chore: tippy to floating-ui migration for mentions
aaryan610 Aug 1, 2025
5eac500
fix: merge conflicts resolved from preview
aaryan610 Aug 23, 2025
16e489a
chore: update remaining floating menus
aaryan610 Aug 23, 2025
cbfc39b
chore: update setEditorValue function
aaryan610 Aug 23, 2025
cd48989
fix: potential bugs
aaryan610 Aug 24, 2025
b2c23d1
chore: extract out common floating ui positioning logic
aaryan610 Aug 24, 2025
a0c14f1
Merge branch 'preview' into chore/tiptap-v3
Palanikannan1437 Sep 25, 2025
63d2299
fix: storage api
Palanikannan1437 Sep 25, 2025
e3c9b3b
Merge branch 'preview' of https://github.com/makeplane/plane into cho…
aaryan610 Sep 26, 2025
5a2184a
fix: bubble menu
aaryan610 Sep 26, 2025
5f58830
fix: type errors
aaryan610 Sep 29, 2025
e45f958
fix: type errors
aaryan610 Sep 29, 2025
d2b939e
fix: merge conflicts resolved from preview
aaryan610 Sep 29, 2025
7380696
chore: upgrade tiptap-markdown package
aaryan610 Sep 29, 2025
d277a52
fix: mentions close callback
aaryan610 Sep 29, 2025
de51429
chore: update bubbling sequence
aaryan610 Sep 29, 2025
605ac3c
chore: update package.json
aaryan610 Sep 29, 2025
b007c43
chore: update tiptap catalogs
aaryan610 Sep 29, 2025
fe93c1d
fix: merge conflicts resolved from preview
aaryan610 Sep 30, 2025
2ad7440
fix: add error handling
aaryan610 Sep 30, 2025
8d9b772
fix: file plugin types
aaryan610 Sep 30, 2025
25de9dc
fix: merge conflicts resolved from preview
aaryan610 Sep 30, 2025
73f54d9
chore: update live package.json
aaryan610 Sep 30, 2025
42dc2f9
fix: broken lock file
sriramveeraghanta Sep 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/live/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
"@hocuspocus/server": "^2.15.0",
"@plane/editor": "workspace:*",
"@plane/types": "workspace:*",
"@tiptap/core": "^2.22.3",
"@tiptap/html": "^2.22.3",
"@tiptap/core": "^3.0.7",
"@tiptap/html": "^3.0.7",
"axios": "1.11.0",
"compression": "1.8.1",
"cors": "^2.8.5",
Expand Down
37 changes: 18 additions & 19 deletions packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,24 @@
"@plane/types": "workspace:*",
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@tiptap/core": "^2.22.3",
"@tiptap/extension-blockquote": "^2.22.3",
"@tiptap/extension-character-count": "^2.22.3",
"@tiptap/extension-collaboration": "^2.22.3",
"@tiptap/extension-emoji": "^2.22.3",
"@tiptap/extension-image": "^2.22.3",
"@tiptap/extension-list-item": "^2.22.3",
"@tiptap/extension-mention": "^2.22.3",
"@tiptap/extension-placeholder": "^2.22.3",
"@tiptap/extension-task-item": "^2.22.3",
"@tiptap/extension-task-list": "^2.22.3",
"@tiptap/extension-text-align": "^2.22.3",
"@tiptap/extension-text-style": "^2.22.3",
"@tiptap/extension-underline": "^2.22.3",
"@tiptap/html": "^2.22.3",
"@tiptap/pm": "^2.22.3",
"@tiptap/react": "^2.22.3",
"@tiptap/starter-kit": "^2.22.3",
"@tiptap/suggestion": "^2.22.3",
"@tiptap/core": "^3.0.7",
"@tiptap/extension-blockquote": "^3.0.7",
"@tiptap/extension-collaboration": "^3.0.7",
"@tiptap/extension-emoji": "^3.0.7",
"@tiptap/extension-image": "^3.0.7",
"@tiptap/extension-list-item": "^3.0.7",
"@tiptap/extension-mention": "^3.0.7",
"@tiptap/extension-task-item": "^3.0.7",
"@tiptap/extension-task-list": "^3.0.7",
"@tiptap/extension-text-align": "^3.0.7",
"@tiptap/extension-text-style": "^3.0.7",
"@tiptap/extensions": "^3.0.7",
"@tiptap/html": "^3.0.7",
"@tiptap/pm": "^3.0.7",
"@tiptap/react": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@tiptap/suggestion": "^3.0.7",
"@tiptap/y-tiptap": "^3.0.0",
"emoji-regex": "^10.3.0",
"highlight.js": "^11.8.0",
"is-emoji-supported": "^0.0.5",
Expand Down
18 changes: 0 additions & 18 deletions packages/editor/src/ce/types/storage.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,4 @@
import { CharacterCountStorage } from "@tiptap/extension-character-count";
// constants
import type { EmojiStorage } from "@tiptap/extension-emoji";
import { CORE_EXTENSIONS } from "@/constants/extension";
// extensions
import type { HeadingExtensionStorage } from "@/extensions";
import type { CustomImageExtensionStorage } from "@/extensions/custom-image/types";
import type { CustomLinkStorage } from "@/extensions/custom-link";
import type { ImageExtensionStorage } from "@/extensions/image";
import type { UtilityExtensionStorage } from "@/extensions/utility";

export type ExtensionStorageMap = {
[CORE_EXTENSIONS.CUSTOM_IMAGE]: CustomImageExtensionStorage;
[CORE_EXTENSIONS.IMAGE]: ImageExtensionStorage;
[CORE_EXTENSIONS.CUSTOM_LINK]: CustomLinkStorage;
[CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage;
[CORE_EXTENSIONS.UTILITY]: UtilityExtensionStorage;
[CORE_EXTENSIONS.EMOJI]: EmojiStorage;
[CORE_EXTENSIONS.CHARACTER_COUNT]: CharacterCountStorage;
};

export type ExtensionFileSetStorageKey = Extract<keyof ImageExtensionStorage, "deletedImageSet">;
4 changes: 2 additions & 2 deletions packages/editor/src/core/components/menus/ai-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useCallback, useEffect, useRef, useState } from "react";
import tippy, { Instance } from "tippy.js";
import tippy, { type Instance } from "tippy.js";
// plane utils
import { cn } from "@plane/utils";
// types
import { TAIHandler } from "@/types";
import type { TAIHandler } from "@/types";

type Props = {
menu: TAIHandler["menu"];
Expand Down
208 changes: 127 additions & 81 deletions packages/editor/src/core/components/menus/block-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import {
useFloating,
autoUpdate,
offset,
flip,
shift,
useDismiss,
useInteractions,
FloatingPortal,
} from "@floating-ui/react";
import type { Editor } from "@tiptap/react";
import { Copy, LucideIcon, Trash2 } from "lucide-react";
import { useCallback, useEffect, useRef } from "react";
import tippy, { Instance } from "tippy.js";
import { useCallback, useEffect, useRef, useState } from "react";
import { cn } from "@plane/utils";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";

Expand All @@ -11,67 +21,71 @@ type Props = {

export const BlockMenu = (props: Props) => {
const { editor } = props;
const menuRef = useRef<HTMLDivElement>(null);
const popup = useRef<Instance | null>(null);

const handleClickDragHandle = useCallback((event: MouseEvent) => {
const target = event.target as HTMLElement;
if (target.matches("#drag-handle")) {
event.preventDefault();

popup.current?.setProps({
getReferenceClientRect: () => target.getBoundingClientRect(),
});

popup.current?.show();
return;
}

popup.current?.hide();
return;
}, []);

useEffect(() => {
if (menuRef.current) {
menuRef.current.remove();
menuRef.current.style.visibility = "visible";

// @ts-expect-error - Tippy types are incorrect
popup.current = tippy(document.body, {
getReferenceClientRect: null,
content: menuRef.current,
appendTo: () => document.querySelector(".frame-renderer"),
trigger: "manual",
interactive: true,
arrow: false,
placement: "left-start",
animation: "shift-away",
maxWidth: 500,
hideOnClick: true,
onShown: () => {
menuRef.current?.focus();
},
});
}

return () => {
popup.current?.destroy();
popup.current = null;
};
}, []);
const [isOpen, setIsOpen] = useState(false);
const [isAnimatedIn, setIsAnimatedIn] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
const virtualReferenceRef = useRef<{ getBoundingClientRect: () => DOMRect }>({
getBoundingClientRect: () => new DOMRect(),
});

// Set up Floating UI with virtual reference element
const { refs, floatingStyles, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
middleware: [offset({ crossAxis: -10 }), flip(), shift()],
whileElementsMounted: autoUpdate,
placement: "left-start",
});

const dismiss = useDismiss(context);
const { getFloatingProps } = useInteractions([dismiss]);

// Handle click on drag handle
const handleClickDragHandle = useCallback(
(event: MouseEvent) => {
const target = event.target as HTMLElement;
const dragHandle = target.closest("#drag-handle");

if (dragHandle) {
event.preventDefault();

// Update virtual reference with current drag handle position
virtualReferenceRef.current = {
getBoundingClientRect: () => dragHandle.getBoundingClientRect(),
};

// Set the virtual reference as the reference element
refs.setReference(virtualReferenceRef.current);

// Show the menu
setIsOpen(true);
return;
}

// If clicking outside and not on a menu item, hide the menu
if (menuRef.current && !menuRef.current.contains(target)) {
setIsOpen(false);
}
},
[refs]
);

// Set up event listeners
useEffect(() => {
const handleKeyDown = () => {
popup.current?.hide();
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setIsOpen(false);
}
};

const handleScroll = () => {
popup.current?.hide();
setIsOpen(false);
};

document.addEventListener("click", handleClickDragHandle);
document.addEventListener("contextmenu", handleClickDragHandle);
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("scroll", handleScroll, true); // Using capture phase
document.addEventListener("scroll", handleScroll, true);

return () => {
document.removeEventListener("click", handleClickDragHandle);
Expand All @@ -81,6 +95,23 @@ export const BlockMenu = (props: Props) => {
};
}, [handleClickDragHandle]);

// Animation effect
useEffect(() => {
if (isOpen) {
setIsAnimatedIn(false);
// Add a small delay before starting the animation
const timeout = setTimeout(() => {
requestAnimationFrame(() => {
setIsAnimatedIn(true);
});
}, 50);

return () => clearTimeout(timeout);
} else {
setIsAnimatedIn(false);
}
}, [isOpen]);

const MENU_ITEMS: {
icon: LucideIcon;
key: string;
Expand All @@ -94,7 +125,7 @@ export const BlockMenu = (props: Props) => {
label: "Delete",
onClick: (e) => {
editor.chain().deleteSelection().focus().run();
popup.current?.hide();
setIsOpen(false);
e.preventDefault();
e.stopPropagation();
},
Expand Down Expand Up @@ -143,36 +174,51 @@ export const BlockMenu = (props: Props) => {
console.error(error.message);
}
}

popup.current?.hide();
setIsOpen(false);
},
},
];

return (
<div
ref={menuRef}
className="z-10 max-h-60 min-w-[7rem] overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 shadow-custom-shadow-rg"
>
{MENU_ITEMS.map((item) => {
// Skip rendering the button if it should be disabled
if (item.isDisabled && item.key === "duplicate") {
return null;
}
if (!isOpen) {
return null;
}

return (
<button
key={item.key}
type="button"
className="flex w-full items-center gap-2 truncate rounded px-1 py-1.5 text-xs text-custom-text-200 hover:bg-custom-background-80"
onClick={item.onClick}
disabled={item.isDisabled}
>
<item.icon className="h-3 w-3" />
{item.label}
</button>
);
})}
</div>
return (
<FloatingPortal>
<div
ref={(node) => {
refs.setFloating(node);
menuRef.current = node;
}}
style={{
...floatingStyles,
animationFillMode: "forwards",
transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)", // Expo ease out
}}
className={cn(
"z-20 max-h-60 min-w-[7rem] overflow-y-scroll rounded-lg border border-custom-border-200 bg-custom-background-100 p-1.5 shadow-custom-shadow-rg",
"transition-all duration-300 transform origin-top-right",
isAnimatedIn ? "opacity-100 scale-100" : "opacity-0 scale-75"
)}
{...getFloatingProps()}
>
{MENU_ITEMS.map((item) => {
if (item.isDisabled) return null;

return (
<button
key={item.key}
type="button"
className="flex w-full items-center gap-1.5 truncate rounded px-1 py-1.5 text-xs text-custom-text-200 hover:bg-custom-background-90"
onClick={item.onClick}
disabled={item.isDisabled}
>
<item.icon className="h-3 w-3" />
{item.label}
</button>
);
})}
</div>
</FloatingPortal>
);
};
Loading
Loading