[WEB-2047] refactor: editor side menu (#5329)
* refactor: editor side menu * chore: change editor side menu selector to be id based
This commit is contained in:
parent
943dd593fa
commit
e805c49e69
16 changed files with 356 additions and 293 deletions
13
packages/editor/src/ce/extensions/ai-features/handle.ts
Normal file
13
packages/editor/src/ce/extensions/ai-features/handle.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// extensions
|
||||||
|
import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export const AIHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => {
|
||||||
|
const view = () => {};
|
||||||
|
const domEvents = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
view,
|
||||||
|
domEvents,
|
||||||
|
};
|
||||||
|
};
|
||||||
1
packages/editor/src/ce/extensions/ai-features/index.ts
Normal file
1
packages/editor/src/ce/extensions/ai-features/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./handle";
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
export * from "./ai-features";
|
||||||
export * from "./document-extensions";
|
export * from "./document-extensions";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
// components
|
// components
|
||||||
import { PageRenderer } from "@/components/editors";
|
import { PageRenderer } from "@/components/editors";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -46,13 +46,6 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
||||||
tabIndex,
|
tabIndex,
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
|
||||||
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = useState<() => void>(() => {});
|
|
||||||
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
|
|
||||||
// loads such that we can invoke it from react when the cursor leaves the container
|
|
||||||
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
|
|
||||||
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
|
|
||||||
};
|
|
||||||
|
|
||||||
// use document editor
|
// use document editor
|
||||||
const { editor, isIndexedDbSynced } = useDocumentEditor({
|
const { editor, isIndexedDbSynced } = useDocumentEditor({
|
||||||
|
|
@ -67,7 +60,6 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
placeholder,
|
placeholder,
|
||||||
setHideDragHandleFunction,
|
|
||||||
tabIndex,
|
tabIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -80,13 +72,7 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
||||||
if (!editor || !isIndexedDbSynced) return null;
|
if (!editor || !isIndexedDbSynced) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageRenderer
|
<PageRenderer editor={editor} editorContainerClassName={editorContainerClassNames} id={id} tabIndex={tabIndex} />
|
||||||
editor={editor}
|
|
||||||
editorContainerClassName={editorContainerClassNames}
|
|
||||||
hideDragHandle={hideDragHandleOnMouseLeave}
|
|
||||||
id={id}
|
|
||||||
tabIndex={tabIndex}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,12 @@ import { BlockMenu } from "@/components/menus";
|
||||||
type IPageRenderer = {
|
type IPageRenderer = {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
editorContainerClassName: string;
|
editorContainerClassName: string;
|
||||||
hideDragHandle?: () => void;
|
|
||||||
id: string;
|
id: string;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageRenderer = (props: IPageRenderer) => {
|
export const PageRenderer = (props: IPageRenderer) => {
|
||||||
const { editor, editorContainerClassName, hideDragHandle, id, tabIndex } = props;
|
const { editor, editorContainerClassName, id, tabIndex } = props;
|
||||||
// states
|
// states
|
||||||
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
@ -129,14 +128,9 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="frame-renderer flex-grow w-full -mx-5" onMouseOver={handleLinkHover}>
|
<div className="frame-renderer flex-grow w-full -mx-5" onMouseOver={handleLinkHover}>
|
||||||
<EditorContainer
|
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName} id={id}>
|
||||||
editor={editor}
|
|
||||||
editorContainerClassName={editorContainerClassName}
|
|
||||||
hideDragHandle={hideDragHandle}
|
|
||||||
id={id}
|
|
||||||
>
|
|
||||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||||
{editor && editor.isEditable && <BlockMenu editor={editor} />}
|
{editor.isEditable && <BlockMenu editor={editor} />}
|
||||||
</EditorContainer>
|
</EditorContainer>
|
||||||
</div>
|
</div>
|
||||||
{isOpen && linkViewProps && coordinates && (
|
{isOpen && linkViewProps && coordinates && (
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,11 @@ interface EditorContainerProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
editor: Editor | null;
|
editor: Editor | null;
|
||||||
editorContainerClassName: string;
|
editorContainerClassName: string;
|
||||||
hideDragHandle?: () => void;
|
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||||
const { children, editor, editorContainerClassName, hideDragHandle, id } = props;
|
const { children, editor, editorContainerClassName, id } = props;
|
||||||
|
|
||||||
const handleContainerClick = () => {
|
const handleContainerClick = () => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
|
|
@ -53,11 +52,18 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContainerMouseLeave = () => {
|
||||||
|
const dragHandleElement = document.querySelector("#editor-side-menu");
|
||||||
|
if (!dragHandleElement?.classList.contains("side-menu-hidden")) {
|
||||||
|
dragHandleElement?.classList.add("side-menu-hidden");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`editor-container-${id}`}
|
id={`editor-container-${id}`}
|
||||||
onClick={handleContainerClick}
|
onClick={handleContainerClick}
|
||||||
onMouseLeave={hideDragHandle}
|
onMouseLeave={handleContainerMouseLeave}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-text relative",
|
"cursor-text relative",
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import { EditorContentWrapper } from "./editor-content";
|
||||||
type Props = IEditorProps & {
|
type Props = IEditorProps & {
|
||||||
children?: (editor: Editor) => React.ReactNode;
|
children?: (editor: Editor) => React.ReactNode;
|
||||||
extensions: Extension<any, any>[];
|
extensions: Extension<any, any>[];
|
||||||
hideDragHandleOnMouseLeave: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditorWrapper: React.FC<Props> = (props) => {
|
export const EditorWrapper: React.FC<Props> = (props) => {
|
||||||
|
|
@ -20,7 +19,6 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||||
containerClassName,
|
containerClassName,
|
||||||
editorClassName = "",
|
editorClassName = "",
|
||||||
extensions,
|
extensions,
|
||||||
hideDragHandleOnMouseLeave,
|
|
||||||
id,
|
id,
|
||||||
initialValue,
|
initialValue,
|
||||||
fileHandler,
|
fileHandler,
|
||||||
|
|
@ -56,12 +54,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorContainer
|
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName} id={id}>
|
||||||
editor={editor}
|
|
||||||
editorContainerClassName={editorContainerClassName}
|
|
||||||
id={id}
|
|
||||||
hideDragHandle={hideDragHandleOnMouseLeave}
|
|
||||||
>
|
|
||||||
{children?.(editor)}
|
{children?.(editor)}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const LiteTextEditor = (props: ILiteTextEditor) => {
|
||||||
|
|
||||||
const extensions = [EnterKeyExtension(onEnterKeyPress)];
|
const extensions = [EnterKeyExtension(onEnterKeyPress)];
|
||||||
|
|
||||||
return <EditorWrapper {...props} extensions={extensions} hideDragHandleOnMouseLeave={() => {}} />;
|
return <EditorWrapper {...props} extensions={extensions} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LiteTextEditorWithRef = forwardRef<EditorRefApi, ILiteTextEditor>((props, ref) => (
|
const LiteTextEditorWithRef = forwardRef<EditorRefApi, ILiteTextEditor>((props, ref) => (
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,30 @@
|
||||||
import { forwardRef, useCallback, useState } from "react";
|
import { forwardRef, useCallback } from "react";
|
||||||
// components
|
// components
|
||||||
import { EditorWrapper } from "@/components/editors";
|
import { EditorWrapper } from "@/components/editors";
|
||||||
import { EditorBubbleMenu } from "@/components/menus";
|
import { EditorBubbleMenu } from "@/components/menus";
|
||||||
// extensions
|
// extensions
|
||||||
import { DragAndDrop, SlashCommand } from "@/extensions";
|
import { SideMenuExtension, SlashCommand } from "@/extensions";
|
||||||
// types
|
// types
|
||||||
import { EditorRefApi, IRichTextEditor } from "@/types";
|
import { EditorRefApi, IRichTextEditor } from "@/types";
|
||||||
|
|
||||||
const RichTextEditor = (props: IRichTextEditor) => {
|
const RichTextEditor = (props: IRichTextEditor) => {
|
||||||
const { dragDropEnabled, fileHandler } = props;
|
const { dragDropEnabled, fileHandler } = props;
|
||||||
// states
|
|
||||||
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = useState<() => void>(() => {});
|
|
||||||
|
|
||||||
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
|
|
||||||
// loads such that we can invoke it from react when the cursor leaves the container
|
|
||||||
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
|
|
||||||
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getExtensions = useCallback(() => {
|
const getExtensions = useCallback(() => {
|
||||||
const extensions = [SlashCommand(fileHandler.upload)];
|
const extensions = [SlashCommand(fileHandler.upload)];
|
||||||
|
|
||||||
if (dragDropEnabled) extensions.push(DragAndDrop(setHideDragHandleFunction));
|
extensions.push(
|
||||||
|
SideMenuExtension({
|
||||||
|
aiEnabled: false,
|
||||||
|
dragDropEnabled: !!dragDropEnabled,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return extensions;
|
return extensions;
|
||||||
}, [dragDropEnabled, fileHandler.upload]);
|
}, [dragDropEnabled, fileHandler.upload]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorWrapper {...props} extensions={getExtensions()} hideDragHandleOnMouseLeave={hideDragHandleOnMouseLeave}>
|
<EditorWrapper {...props} extensions={getExtensions()}>
|
||||||
{(editor) => <>{editor && <EditorBubbleMenu editor={editor} />}</>}
|
{(editor) => <>{editor && <EditorBubbleMenu editor={editor} />}</>}
|
||||||
</EditorWrapper>
|
</EditorWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ export * from "./typography";
|
||||||
export * from "./core-without-props";
|
export * from "./core-without-props";
|
||||||
export * from "./document-without-props";
|
export * from "./document-without-props";
|
||||||
export * from "./custom-code-inline";
|
export * from "./custom-code-inline";
|
||||||
export * from "./drag-drop";
|
|
||||||
export * from "./drop";
|
export * from "./drop";
|
||||||
export * from "./enter-key-extension";
|
export * from "./enter-key-extension";
|
||||||
export * from "./extensions";
|
export * from "./extensions";
|
||||||
|
|
@ -18,4 +17,5 @@ export * from "./horizontal-rule";
|
||||||
export * from "./keymap";
|
export * from "./keymap";
|
||||||
export * from "./quote";
|
export * from "./quote";
|
||||||
export * from "./read-only-extensions";
|
export * from "./read-only-extensions";
|
||||||
|
export * from "./side-menu";
|
||||||
export * from "./slash-commands";
|
export * from "./slash-commands";
|
||||||
|
|
|
||||||
199
packages/editor/src/core/extensions/side-menu.tsx
Normal file
199
packages/editor/src/core/extensions/side-menu.tsx
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
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";
|
||||||
|
import { DragHandlePlugin } from "@/plugins/drag-handle";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
aiEnabled: boolean;
|
||||||
|
dragDropEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SideMenuPluginProps = {
|
||||||
|
dragHandleWidth: number;
|
||||||
|
handlesConfig: {
|
||||||
|
ai: boolean;
|
||||||
|
dragDrop: boolean;
|
||||||
|
};
|
||||||
|
scrollThreshold: {
|
||||||
|
up: number;
|
||||||
|
down: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SideMenuHandleOptions = {
|
||||||
|
view: (view: EditorView, sideMenu: HTMLDivElement | null) => void;
|
||||||
|
domEvents?: {
|
||||||
|
[key: string]: (...args: any) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SideMenuExtension = (props: Props) => {
|
||||||
|
const { aiEnabled, dragDropEnabled } = props;
|
||||||
|
|
||||||
|
return Extension.create({
|
||||||
|
name: "editorSideMenu",
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
SideMenu({
|
||||||
|
dragHandleWidth: 24,
|
||||||
|
handlesConfig: {
|
||||||
|
ai: aiEnabled,
|
||||||
|
dragDrop: dragDropEnabled,
|
||||||
|
},
|
||||||
|
scrollThreshold: { up: 300, down: 100 },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const absoluteRect = (node: Element) => {
|
||||||
|
const data = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: data.top,
|
||||||
|
left: data.left,
|
||||||
|
width: data.width,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
||||||
|
const elements = document.elementsFromPoint(coords.x, coords.y);
|
||||||
|
const generalSelectors = [
|
||||||
|
"li",
|
||||||
|
"p:not(:first-child)",
|
||||||
|
".code-block",
|
||||||
|
"blockquote",
|
||||||
|
"img",
|
||||||
|
"h1, h2, h3, h4, h5, h6",
|
||||||
|
"[data-type=horizontalRule]",
|
||||||
|
".table-wrapper",
|
||||||
|
".issue-embed",
|
||||||
|
].join(", ");
|
||||||
|
|
||||||
|
for (const elem of elements) {
|
||||||
|
if (elem.matches("p:first-child") && elem.parentElement?.matches(".ProseMirror")) {
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the element is a <p> tag that is the first child of a td or th
|
||||||
|
if (
|
||||||
|
(elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) &&
|
||||||
|
elem?.textContent?.trim() !== ""
|
||||||
|
) {
|
||||||
|
return elem; // Return only if p tag is not empty in td or th
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply general selector
|
||||||
|
if (elem.matches(generalSelectors)) {
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SideMenu = (options: SideMenuPluginProps) => {
|
||||||
|
const { handlesConfig } = options;
|
||||||
|
const editorSideMenu: HTMLDivElement | null = document.createElement("div");
|
||||||
|
editorSideMenu.id = "editor-side-menu";
|
||||||
|
// side menu view actions
|
||||||
|
const hideSideMenu = () => {
|
||||||
|
if (!editorSideMenu?.classList.contains("side-menu-hidden")) editorSideMenu?.classList.add("side-menu-hidden");
|
||||||
|
};
|
||||||
|
const showSideMenu = () => editorSideMenu?.classList.remove("side-menu-hidden");
|
||||||
|
// side menu elements
|
||||||
|
const { view: dragHandleView, domEvents: dragHandleDOMEvents } = DragHandlePlugin(options);
|
||||||
|
const { view: aiHandleView } = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy: () => hideSideMenu(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
handleDOMEvents: {
|
||||||
|
mousemove: (view, event) => {
|
||||||
|
if (!view.editable) return;
|
||||||
|
|
||||||
|
const node = nodeDOMAtCoords({
|
||||||
|
x: event.clientX + 50 + options.dragHandleWidth,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!(node instanceof Element) || node.matches("ul, ol")) {
|
||||||
|
hideSideMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compStyle = window.getComputedStyle(node);
|
||||||
|
const lineHeight = parseInt(compStyle.lineHeight, 10);
|
||||||
|
const paddingTop = parseInt(compStyle.paddingTop, 10);
|
||||||
|
|
||||||
|
const rect = absoluteRect(node);
|
||||||
|
|
||||||
|
rect.top += (lineHeight - 20) / 2;
|
||||||
|
rect.top += paddingTop;
|
||||||
|
|
||||||
|
if (node.parentElement?.parentElement?.matches("td") || node.parentElement?.parentElement?.matches("th")) {
|
||||||
|
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
||||||
|
rect.left -= 5;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Li markers
|
||||||
|
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
||||||
|
rect.left -= 18;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.matches(".table-wrapper")) {
|
||||||
|
rect.top += 8;
|
||||||
|
rect.left -= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) {
|
||||||
|
rect.left += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
rect.width = options.dragHandleWidth;
|
||||||
|
|
||||||
|
if (!editorSideMenu) return;
|
||||||
|
|
||||||
|
editorSideMenu.style.left = `${rect.left - rect.width}px`;
|
||||||
|
editorSideMenu.style.top = `${rect.top}px`;
|
||||||
|
showSideMenu();
|
||||||
|
},
|
||||||
|
keydown: () => hideSideMenu(),
|
||||||
|
mousewheel: () => hideSideMenu(),
|
||||||
|
dragenter: (view) => {
|
||||||
|
if (handlesConfig.dragDrop) {
|
||||||
|
dragHandleDOMEvents?.dragenter?.(view);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drop: (view, event) => {
|
||||||
|
if (handlesConfig.dragDrop) {
|
||||||
|
dragHandleDOMEvents?.drop?.(view, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dragend: (view) => {
|
||||||
|
if (handlesConfig.dragDrop) {
|
||||||
|
dragHandleDOMEvents?.dragend?.(view);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -3,7 +3,7 @@ import Collaboration from "@tiptap/extension-collaboration";
|
||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
// extensions
|
// extensions
|
||||||
import { DragAndDrop, IssueWidget } from "@/extensions";
|
import { IssueWidget, SideMenuExtension } from "@/extensions";
|
||||||
// hooks
|
// hooks
|
||||||
import { TFileHandler, useEditor } from "@/hooks/use-editor";
|
import { TFileHandler, useEditor } from "@/hooks/use-editor";
|
||||||
// plane editor extensions
|
// plane editor extensions
|
||||||
|
|
@ -30,7 +30,6 @@ type DocumentEditorProps = {
|
||||||
};
|
};
|
||||||
onChange: (updates: Uint8Array) => void;
|
onChange: (updates: Uint8Array) => void;
|
||||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||||
setHideDragHandleFunction: (hideDragHandlerFromDragDrop: () => void) => void;
|
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
value: Uint8Array;
|
value: Uint8Array;
|
||||||
};
|
};
|
||||||
|
|
@ -48,7 +47,6 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
setHideDragHandleFunction,
|
|
||||||
tabIndex,
|
tabIndex,
|
||||||
value,
|
value,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
@ -95,7 +93,10 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
mentionHandler,
|
mentionHandler,
|
||||||
extensions: [
|
extensions: [
|
||||||
DragAndDrop(setHideDragHandleFunction),
|
SideMenuExtension({
|
||||||
|
aiEnabled: !disabledExtensions?.includes("ai"),
|
||||||
|
dragDropEnabled: true,
|
||||||
|
}),
|
||||||
embedHandler?.issue &&
|
embedHandler?.issue &&
|
||||||
IssueWidget({
|
IssueWidget({
|
||||||
widgetCallback: embedHandler.issue.widgetCallback,
|
widgetCallback: embedHandler.issue.widgetCallback,
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,33 @@
|
||||||
import { Extension } from "@tiptap/core";
|
|
||||||
import { Fragment, Slice, Node } from "@tiptap/pm/model";
|
import { Fragment, Slice, Node } from "@tiptap/pm/model";
|
||||||
import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
import { NodeSelection, TextSelection } from "@tiptap/pm/state";
|
||||||
// @ts-expect-error __serializeForClipboard's is not exported
|
// @ts-expect-error __serializeForClipboard's is not exported
|
||||||
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
|
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
|
||||||
|
// extensions
|
||||||
|
import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions";
|
||||||
|
|
||||||
export interface DragHandleOptions {
|
const dragHandleClassName =
|
||||||
dragHandleWidth: number;
|
"hidden sm:grid place-items-center size-5 aspect-square rounded-sm cursor-grab outline-none hover:bg-custom-background-80 active:bg-custom-background-80 active:cursor-grabbing transition-colors duration-200 ease-linear";
|
||||||
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
|
const dragHandleContainerClassName = "size-[15px] grid place-items-center";
|
||||||
scrollThreshold: {
|
const dragHandleDotsClassName = "h-full w-3 grid grid-cols-2 place-items-center";
|
||||||
up: number;
|
const dragHandleDotClassName = "size-[2.5px] bg-custom-text-300 rounded-[50%]";
|
||||||
down: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DragAndDrop = (setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void) =>
|
|
||||||
Extension.create({
|
|
||||||
name: "dragAndDrop",
|
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
|
||||||
return [
|
|
||||||
DragHandle({
|
|
||||||
dragHandleWidth: 24,
|
|
||||||
scrollThreshold: { up: 300, down: 100 },
|
|
||||||
setHideDragHandle,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createDragHandleElement = (): HTMLElement => {
|
const createDragHandleElement = (): HTMLElement => {
|
||||||
const dragHandleElement = document.createElement("button");
|
const dragHandleElement = document.createElement("button");
|
||||||
dragHandleElement.type = "button";
|
dragHandleElement.type = "button";
|
||||||
dragHandleElement.draggable = true;
|
dragHandleElement.draggable = true;
|
||||||
dragHandleElement.dataset.dragHandle = "";
|
dragHandleElement.dataset.dragHandle = "";
|
||||||
dragHandleElement.classList.add("drag-handle");
|
dragHandleElement.classList.value = dragHandleClassName;
|
||||||
|
|
||||||
const dragHandleContainer = document.createElement("span");
|
const dragHandleContainer = document.createElement("span");
|
||||||
dragHandleContainer.classList.add("drag-handle-container");
|
dragHandleContainer.classList.value = dragHandleContainerClassName;
|
||||||
dragHandleElement.appendChild(dragHandleContainer);
|
dragHandleElement.appendChild(dragHandleContainer);
|
||||||
|
|
||||||
const dotsContainer = document.createElement("span");
|
const dotsContainer = document.createElement("span");
|
||||||
dotsContainer.classList.add("drag-handle-dots");
|
dotsContainer.classList.value = dragHandleDotsClassName;
|
||||||
|
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
const spanElement = document.createElement("span");
|
const spanElement = document.createElement("span");
|
||||||
spanElement.classList.add("drag-handle-dot");
|
spanElement.classList.value = dragHandleDotClassName;
|
||||||
dotsContainer.appendChild(spanElement);
|
dotsContainer.appendChild(spanElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,16 +36,6 @@ const createDragHandleElement = (): HTMLElement => {
|
||||||
return dragHandleElement;
|
return dragHandleElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const absoluteRect = (node: Element) => {
|
|
||||||
const data = node.getBoundingClientRect();
|
|
||||||
|
|
||||||
return {
|
|
||||||
top: data.top,
|
|
||||||
left: data.left,
|
|
||||||
width: data.width,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
||||||
const elements = document.elementsFromPoint(coords.x, coords.y);
|
const elements = document.elementsFromPoint(coords.x, coords.y);
|
||||||
const generalSelectors = [
|
const generalSelectors = [
|
||||||
|
|
@ -98,7 +71,7 @@ const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodePosAtDOM = (node: Element, view: EditorView, options: DragHandleOptions) => {
|
const nodePosAtDOM = (node: Element, view: EditorView, options: SideMenuPluginProps) => {
|
||||||
const boundingRect = node.getBoundingClientRect();
|
const boundingRect = node.getBoundingClientRect();
|
||||||
|
|
||||||
return view.posAtCoords({
|
return view.posAtCoords({
|
||||||
|
|
@ -132,7 +105,7 @@ const calcNodePos = (pos: number, view: EditorView, node: Element) => {
|
||||||
return safePos;
|
return safePos;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DragHandle = (options: DragHandleOptions) => {
|
export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => {
|
||||||
let listType = "";
|
let listType = "";
|
||||||
const handleDragStart = (event: DragEvent, view: EditorView) => {
|
const handleDragStart = (event: DragEvent, view: EditorView) => {
|
||||||
view.focus();
|
view.focus();
|
||||||
|
|
@ -222,15 +195,15 @@ const DragHandle = (options: DragHandleOptions) => {
|
||||||
if (!(node instanceof Element)) return;
|
if (!(node instanceof Element)) return;
|
||||||
|
|
||||||
if (node.matches("blockquote")) {
|
if (node.matches("blockquote")) {
|
||||||
let nodePosForBlockquotes = nodePosAtDOMForBlockQuotes(node, view);
|
let nodePosForBlockQuotes = nodePosAtDOMForBlockQuotes(node, view);
|
||||||
if (nodePosForBlockquotes === null || nodePosForBlockquotes === undefined) return;
|
if (nodePosForBlockQuotes === null || nodePosForBlockQuotes === undefined) return;
|
||||||
|
|
||||||
const docSize = view.state.doc.content.size;
|
const docSize = view.state.doc.content.size;
|
||||||
nodePosForBlockquotes = Math.max(0, Math.min(nodePosForBlockquotes, docSize));
|
nodePosForBlockQuotes = Math.max(0, Math.min(nodePosForBlockQuotes, docSize));
|
||||||
|
|
||||||
if (nodePosForBlockquotes >= 0 && nodePosForBlockquotes <= docSize) {
|
if (nodePosForBlockQuotes >= 0 && nodePosForBlockQuotes <= docSize) {
|
||||||
// TODO FIX ERROR
|
// TODO FIX ERROR
|
||||||
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockquotes);
|
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockQuotes);
|
||||||
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -253,14 +226,12 @@ const DragHandle = (options: DragHandleOptions) => {
|
||||||
|
|
||||||
let dragHandleElement: HTMLElement | null = null;
|
let dragHandleElement: HTMLElement | null = null;
|
||||||
// drag handle view actions
|
// drag handle view actions
|
||||||
const hideDragHandle = () => dragHandleElement?.classList.add("drag-handle-hidden");
|
const hideDragHandle = () => {
|
||||||
const showDragHandle = () => dragHandleElement?.classList.remove("drag-handle-hidden");
|
if (!dragHandleElement?.classList.contains("drag-handle-hidden"))
|
||||||
|
dragHandleElement?.classList.add("drag-handle-hidden");
|
||||||
|
};
|
||||||
|
|
||||||
options.setHideDragHandle?.(hideDragHandle);
|
const view = (view: EditorView, sideMenu: HTMLDivElement | null) => {
|
||||||
|
|
||||||
return new Plugin({
|
|
||||||
key: new PluginKey("dragHandle"),
|
|
||||||
view: (view) => {
|
|
||||||
dragHandleElement = createDragHandleElement();
|
dragHandleElement = createDragHandleElement();
|
||||||
dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view));
|
dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view));
|
||||||
dragHandleElement.addEventListener("click", (e) => handleClick(e, view));
|
dragHandleElement.addEventListener("click", (e) => handleClick(e, view));
|
||||||
|
|
@ -279,7 +250,7 @@ const DragHandle = (options: DragHandleOptions) => {
|
||||||
|
|
||||||
hideDragHandle();
|
hideDragHandle();
|
||||||
|
|
||||||
view?.dom?.parentElement?.appendChild(dragHandleElement);
|
sideMenu?.appendChild(dragHandleElement);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
|
|
@ -287,70 +258,13 @@ const DragHandle = (options: DragHandleOptions) => {
|
||||||
dragHandleElement = null;
|
dragHandleElement = null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
};
|
||||||
props: {
|
const domEvents = {
|
||||||
handleDOMEvents: {
|
dragenter: (view: EditorView) => {
|
||||||
mousemove: (view, event) => {
|
|
||||||
if (!view.editable) return;
|
|
||||||
|
|
||||||
const node = nodeDOMAtCoords({
|
|
||||||
x: event.clientX + 50 + options.dragHandleWidth,
|
|
||||||
y: event.clientY,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!(node instanceof Element) || node.matches("ul, ol")) {
|
|
||||||
hideDragHandle();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const compStyle = window.getComputedStyle(node);
|
|
||||||
const lineHeight = parseInt(compStyle.lineHeight, 10);
|
|
||||||
const paddingTop = parseInt(compStyle.paddingTop, 10);
|
|
||||||
|
|
||||||
const rect = absoluteRect(node);
|
|
||||||
|
|
||||||
rect.top += (lineHeight - 20) / 2;
|
|
||||||
rect.top += paddingTop;
|
|
||||||
|
|
||||||
if (node.parentElement?.parentElement?.matches("td") || node.parentElement?.parentElement?.matches("th")) {
|
|
||||||
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
|
||||||
rect.left -= 5;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Li markers
|
|
||||||
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
|
||||||
rect.left -= 18;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.matches(".table-wrapper")) {
|
|
||||||
rect.top += 8;
|
|
||||||
rect.left -= 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.parentElement?.matches("td") || node.parentElement?.matches("th")) {
|
|
||||||
rect.left += 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
rect.width = options.dragHandleWidth;
|
|
||||||
|
|
||||||
if (!dragHandleElement) return;
|
|
||||||
|
|
||||||
dragHandleElement.style.left = `${rect.left - rect.width}px`;
|
|
||||||
dragHandleElement.style.top = `${rect.top}px`;
|
|
||||||
showDragHandle();
|
|
||||||
},
|
|
||||||
keydown: () => {
|
|
||||||
hideDragHandle();
|
|
||||||
},
|
|
||||||
mousewheel: () => {
|
|
||||||
hideDragHandle();
|
|
||||||
},
|
|
||||||
dragenter: (view) => {
|
|
||||||
view.dom.classList.add("dragging");
|
view.dom.classList.add("dragging");
|
||||||
hideDragHandle();
|
hideDragHandle();
|
||||||
},
|
},
|
||||||
drop: (view, event) => {
|
drop: (view: EditorView, event: DragEvent) => {
|
||||||
view.dom.classList.remove("dragging");
|
view.dom.classList.remove("dragging");
|
||||||
hideDragHandle();
|
hideDragHandle();
|
||||||
let droppedNode: Node | null = null;
|
let droppedNode: Node | null = null;
|
||||||
|
|
@ -395,10 +309,13 @@ const DragHandle = (options: DragHandleOptions) => {
|
||||||
view.dragging = { slice, move: event.ctrlKey };
|
view.dragging = { slice, move: event.ctrlKey };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dragend: (view) => {
|
dragend: (view: EditorView) => {
|
||||||
view.dom.classList.remove("dragging");
|
view.dom.classList.remove("dragging");
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
},
|
|
||||||
});
|
return {
|
||||||
|
view,
|
||||||
|
domEvents,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -1 +1 @@
|
||||||
export type TExtensions = "issue-embed";
|
export type TExtensions = "ai" | "issue-embed";
|
||||||
|
|
|
||||||
1
packages/editor/src/ee/extensions/ai-features/index.ts
Normal file
1
packages/editor/src/ee/extensions/ai-features/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "src/ce/extensions/ai-features";
|
||||||
|
|
@ -1,66 +1,20 @@
|
||||||
/* drag handle */
|
/* side menu */
|
||||||
.drag-handle {
|
#editor-side-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
opacity: 1;
|
display: flex;
|
||||||
height: 20px;
|
align-items: center;
|
||||||
width: 20px;
|
opacity: 100;
|
||||||
aspect-ratio: 1 / 1;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
z-index: 5;
|
|
||||||
cursor: grab;
|
|
||||||
border-radius: 2px;
|
|
||||||
outline: none !important;
|
|
||||||
transition:
|
transition:
|
||||||
opacity 0.2s ease 0.2s,
|
opacity 0.2s ease 0.2s,
|
||||||
background-color 0.2s ease,
|
|
||||||
top 0.2s ease,
|
top 0.2s ease,
|
||||||
left 0.2s ease;
|
left 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&.side-menu-hidden {
|
||||||
background-color: rgba(var(--color-background-80));
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: rgba(var(--color-background-80));
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.drag-handle-hidden {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* end side menu */
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
.drag-handle {
|
|
||||||
display: none;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle-container {
|
|
||||||
height: 15px;
|
|
||||||
width: 15px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle-dots {
|
|
||||||
height: 100%;
|
|
||||||
width: 12px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle-dot {
|
|
||||||
height: 2.5px;
|
|
||||||
width: 2.5px;
|
|
||||||
background-color: rgba(var(--color-text-300));
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
/* end drag handle */
|
|
||||||
|
|
||||||
.ProseMirror:not(.dragging) .ProseMirror-selectednode {
|
.ProseMirror:not(.dragging) .ProseMirror-selectednode {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue