Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 0 additions & 13 deletions packages/editor/src/ce/extensions/ai-features/handle.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/editor/src/ce/extensions/ai-features/index.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/editor/src/ce/extensions/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from "./ai-features";
export * from "./document-extensions";
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import {
EditorRefApi,
IMentionHighlight,
IMentionSuggestion,
TAIHandler,
TDisplayConfig,
TExtensions,
TFileHandler,
} from "@/types";

interface IDocumentEditor {
aiHandler?: TAIHandler;
containerClassName?: string;
disabledExtensions?: TExtensions[];
displayConfig?: TDisplayConfig;
Expand All @@ -41,6 +43,7 @@ interface IDocumentEditor {

const DocumentEditor = (props: IDocumentEditor) => {
const {
aiHandler,
containerClassName,
disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG,
Expand Down Expand Up @@ -84,6 +87,7 @@ const DocumentEditor = (props: IDocumentEditor) => {
return (
<PageRenderer
displayConfig={displayConfig}
aiHandler={aiHandler}
editor={editor}
editorContainerClassName={editorContainerClassNames}
id={id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import { Editor, ReactRenderer } from "@tiptap/react";
// components
import { EditorContainer, EditorContentWrapper } from "@/components/editors";
import { LinkView, LinkViewProps } from "@/components/links";
import { BlockMenu } from "@/components/menus";
import { AIFeaturesMenu, BlockMenu } from "@/components/menus";
// types
import { TDisplayConfig } from "@/types";
import { TAIHandler, TDisplayConfig } from "@/types";

type IPageRenderer = {
aiHandler?: TAIHandler;
displayConfig: TDisplayConfig;
editor: Editor;
editorContainerClassName: string;
Expand All @@ -28,7 +29,7 @@ type IPageRenderer = {
};

export const PageRenderer = (props: IPageRenderer) => {
const { displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
const { aiHandler, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
// states
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
const [isOpen, setIsOpen] = useState(false);
Expand Down Expand Up @@ -138,7 +139,12 @@ export const PageRenderer = (props: IPageRenderer) => {
id={id}
>
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
{editor.isEditable && <BlockMenu editor={editor} />}
{editor.isEditable && (
<>
<BlockMenu editor={editor} />
<AIFeaturesMenu menu={aiHandler?.menu} />
</>
)}
</EditorContainer>
</div>
{isOpen && linkViewProps && coordinates && (
Expand Down
95 changes: 95 additions & 0 deletions packages/editor/src/core/components/menus/ai-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useCallback, useEffect, useRef, useState } from "react";
import tippy, { Instance } from "tippy.js";
// helpers
import { cn } from "@/helpers/common";
// types
import { TAIHandler } from "@/types";

type Props = {
menu: TAIHandler["menu"];
};

export const AIFeaturesMenu: React.FC<Props> = (props) => {
const { menu } = props;
// states
const [isPopupVisible, setIsPopupVisible] = useState(false);
// refs
const menuRef = useRef<HTMLDivElement>(null);
const popup = useRef<Instance | null>(null);

useEffect(() => {
if (!menuRef.current) return;

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: "bottom-start",
animation: "shift-away",
hideOnClick: true,
onShown: () => menuRef.current?.focus(),
});

return () => {
popup.current?.destroy();
popup.current = null;
};
}, []);

const hidePopup = useCallback(() => {
popup.current?.hide();
setIsPopupVisible(false);
}, []);

useEffect(() => {
const handleClickAIHandle = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (target.matches("#ai-handle") || menuRef.current?.contains(e.target as Node)) {
e.preventDefault();

if (!isPopupVisible) {
popup.current?.setProps({
getReferenceClientRect: () => target.getBoundingClientRect(),
});
popup.current?.show();
setIsPopupVisible(true);
}
return;
}

hidePopup();
return;
};

document.addEventListener("click", handleClickAIHandle);
document.addEventListener("contextmenu", handleClickAIHandle);
document.addEventListener("keydown", hidePopup);

return () => {
document.removeEventListener("click", handleClickAIHandle);
document.removeEventListener("contextmenu", handleClickAIHandle);
document.removeEventListener("keydown", hidePopup);
};
}, [hidePopup, isPopupVisible]);

return (
<div
className={cn("opacity-0 pointer-events-none fixed inset-0 size-full z-10 transition-opacity", {
"opacity-100 pointer-events-auto": isPopupVisible,
})}
>
<div ref={menuRef} className="z-10">
{menu?.({
onClose: hidePopup,
})}
</div>
</div>
);
};
1 change: 1 addition & 0 deletions packages/editor/src/core/components/menus/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./bubble-menu";
export * from "./ai-menu";
export * from "./block-menu";
export * from "./menu-items";
19 changes: 12 additions & 7 deletions packages/editor/src/core/extensions/side-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Extension } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { EditorView } from "@tiptap/pm/view";
// plane editor extensions
import { AIHandlePlugin } from "@/plane-editor/extensions";
// plugins
import { AIHandlePlugin } from "@/plugins/ai-handle";
import { DragHandlePlugin } from "@/plugins/drag-handle";

type Props = {
Expand Down Expand Up @@ -105,20 +105,20 @@ const SideMenu = (options: SideMenuPluginProps) => {
const showSideMenu = () => editorSideMenu?.classList.remove("side-menu-hidden");
// side menu elements
const { view: dragHandleView, domEvents: dragHandleDOMEvents } = DragHandlePlugin(options);
const { view: aiHandleView } = AIHandlePlugin(options);
const { view: aiHandleView, domEvents: aiHandleDOMEvents } = AIHandlePlugin(options);

return new Plugin({
key: new PluginKey("sideMenu"),
view: (view) => {
hideSideMenu();
view?.dom.parentElement?.appendChild(editorSideMenu);
// side menu elements' initialization
if (handlesConfig.dragDrop) {
dragHandleView(view, editorSideMenu);
}
if (handlesConfig.ai) {
aiHandleView(view, editorSideMenu);
}
if (handlesConfig.dragDrop) {
dragHandleView(view, editorSideMenu);
}

return {
destroy: () => hideSideMenu(),
Expand Down Expand Up @@ -175,7 +175,12 @@ const SideMenu = (options: SideMenuPluginProps) => {
editorSideMenu.style.left = `${rect.left - rect.width}px`;
editorSideMenu.style.top = `${rect.top}px`;
showSideMenu();
dragHandleDOMEvents?.mousemove();
if (handlesConfig.dragDrop) {
dragHandleDOMEvents?.mousemove();
}
if (handlesConfig.ai) {
aiHandleDOMEvents?.mousemove?.();
}
},
keydown: () => hideSideMenu(),
mousewheel: () => hideSideMenu(),
Expand Down
36 changes: 36 additions & 0 deletions packages/editor/src/core/hooks/use-editor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
import { DOMSerializer } from "@tiptap/pm/model";
import { Selection } from "@tiptap/pm/state";
import { EditorProps } from "@tiptap/pm/view";
import { useEditor as useTiptapEditor, Editor } from "@tiptap/react";
Expand Down Expand Up @@ -213,6 +214,41 @@ export const useEditor = (props: CustomEditorProps) => {
console.error("An error occurred while setting focus at position:", error);
}
},
getSelectedText: () => {
if (!editorRef.current) return null;

const { state } = editorRef.current;
const { from, to, empty } = state.selection;

if (empty) return null;

const nodesArray: string[] = [];
state.doc.nodesBetween(from, to, (node, pos, parent) => {
if (parent === state.doc && editorRef.current) {
const serializer = DOMSerializer.fromSchema(editorRef.current?.schema);
const dom = serializer.serializeNode(node);
const tempDiv = document.createElement("div");
tempDiv.appendChild(dom);
nodesArray.push(tempDiv.innerHTML);
}
});
const selection = nodesArray.join("");
console.log(selection);
return selection;
},
insertText: (contentHTML, insertOnNextLine) => {
if (!editor) return;
// get selection
const { from, to, empty } = editor.state.selection;
if (empty) return;
if (insertOnNextLine) {
// move cursor to the end of the selection and insert a new line
editor.chain().focus().setTextSelection(to).insertContent("<br />").insertContent(contentHTML).run();
} else {
// replace selected text with the content provided
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
}
},
}),
[editorRef, savedSelection, fileHandler.upload]
);
Expand Down
Loading