[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";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
// components
|
||||
import { PageRenderer } from "@/components/editors";
|
||||
// helpers
|
||||
|
|
@ -46,13 +46,6 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
|||
tabIndex,
|
||||
value,
|
||||
} = 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
|
||||
const { editor, isIndexedDbSynced } = useDocumentEditor({
|
||||
|
|
@ -67,7 +60,6 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
|||
forwardedRef,
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
setHideDragHandleFunction,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
|
|
@ -80,13 +72,7 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
|||
if (!editor || !isIndexedDbSynced) return null;
|
||||
|
||||
return (
|
||||
<PageRenderer
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassNames}
|
||||
hideDragHandle={hideDragHandleOnMouseLeave}
|
||||
id={id}
|
||||
tabIndex={tabIndex}
|
||||
/>
|
||||
<PageRenderer editor={editor} editorContainerClassName={editorContainerClassNames} id={id} tabIndex={tabIndex} />
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,12 @@ import { BlockMenu } from "@/components/menus";
|
|||
type IPageRenderer = {
|
||||
editor: Editor;
|
||||
editorContainerClassName: string;
|
||||
hideDragHandle?: () => void;
|
||||
id: string;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export const PageRenderer = (props: IPageRenderer) => {
|
||||
const { editor, editorContainerClassName, hideDragHandle, id, tabIndex } = props;
|
||||
const { editor, editorContainerClassName, id, tabIndex } = props;
|
||||
// states
|
||||
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
|
@ -129,14 +128,9 @@ export const PageRenderer = (props: IPageRenderer) => {
|
|||
return (
|
||||
<>
|
||||
<div className="frame-renderer flex-grow w-full -mx-5" onMouseOver={handleLinkHover}>
|
||||
<EditorContainer
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
hideDragHandle={hideDragHandle}
|
||||
id={id}
|
||||
>
|
||||
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName} id={id}>
|
||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||
{editor && editor.isEditable && <BlockMenu editor={editor} />}
|
||||
{editor.isEditable && <BlockMenu editor={editor} />}
|
||||
</EditorContainer>
|
||||
</div>
|
||||
{isOpen && linkViewProps && coordinates && (
|
||||
|
|
|
|||
|
|
@ -7,12 +7,11 @@ interface EditorContainerProps {
|
|||
children: ReactNode;
|
||||
editor: Editor | null;
|
||||
editorContainerClassName: string;
|
||||
hideDragHandle?: () => void;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
const { children, editor, editorContainerClassName, hideDragHandle, id } = props;
|
||||
const { children, editor, editorContainerClassName, id } = props;
|
||||
|
||||
const handleContainerClick = () => {
|
||||
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 (
|
||||
<div
|
||||
id={`editor-container-${id}`}
|
||||
onClick={handleContainerClick}
|
||||
onMouseLeave={hideDragHandle}
|
||||
onMouseLeave={handleContainerMouseLeave}
|
||||
className={cn(
|
||||
"cursor-text relative",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { EditorContentWrapper } from "./editor-content";
|
|||
type Props = IEditorProps & {
|
||||
children?: (editor: Editor) => React.ReactNode;
|
||||
extensions: Extension<any, any>[];
|
||||
hideDragHandleOnMouseLeave: () => void;
|
||||
};
|
||||
|
||||
export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
|
|
@ -20,7 +19,6 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
|||
containerClassName,
|
||||
editorClassName = "",
|
||||
extensions,
|
||||
hideDragHandleOnMouseLeave,
|
||||
id,
|
||||
initialValue,
|
||||
fileHandler,
|
||||
|
|
@ -56,12 +54,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
|||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<EditorContainer
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
id={id}
|
||||
hideDragHandle={hideDragHandleOnMouseLeave}
|
||||
>
|
||||
<EditorContainer editor={editor} editorContainerClassName={editorContainerClassName} id={id}>
|
||||
{children?.(editor)}
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const LiteTextEditor = (props: ILiteTextEditor) => {
|
|||
|
||||
const extensions = [EnterKeyExtension(onEnterKeyPress)];
|
||||
|
||||
return <EditorWrapper {...props} extensions={extensions} hideDragHandleOnMouseLeave={() => {}} />;
|
||||
return <EditorWrapper {...props} extensions={extensions} />;
|
||||
};
|
||||
|
||||
const LiteTextEditorWithRef = forwardRef<EditorRefApi, ILiteTextEditor>((props, ref) => (
|
||||
|
|
|
|||
|
|
@ -1,33 +1,30 @@
|
|||
import { forwardRef, useCallback, useState } from "react";
|
||||
import { forwardRef, useCallback } from "react";
|
||||
// components
|
||||
import { EditorWrapper } from "@/components/editors";
|
||||
import { EditorBubbleMenu } from "@/components/menus";
|
||||
// extensions
|
||||
import { DragAndDrop, SlashCommand } from "@/extensions";
|
||||
import { SideMenuExtension, SlashCommand } from "@/extensions";
|
||||
// types
|
||||
import { EditorRefApi, IRichTextEditor } from "@/types";
|
||||
|
||||
const RichTextEditor = (props: IRichTextEditor) => {
|
||||
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 extensions = [SlashCommand(fileHandler.upload)];
|
||||
|
||||
if (dragDropEnabled) extensions.push(DragAndDrop(setHideDragHandleFunction));
|
||||
extensions.push(
|
||||
SideMenuExtension({
|
||||
aiEnabled: false,
|
||||
dragDropEnabled: !!dragDropEnabled,
|
||||
})
|
||||
);
|
||||
|
||||
return extensions;
|
||||
}, [dragDropEnabled, fileHandler.upload]);
|
||||
|
||||
return (
|
||||
<EditorWrapper {...props} extensions={getExtensions()} hideDragHandleOnMouseLeave={hideDragHandleOnMouseLeave}>
|
||||
<EditorWrapper {...props} extensions={getExtensions()}>
|
||||
{(editor) => <>{editor && <EditorBubbleMenu editor={editor} />}</>}
|
||||
</EditorWrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ export * from "./typography";
|
|||
export * from "./core-without-props";
|
||||
export * from "./document-without-props";
|
||||
export * from "./custom-code-inline";
|
||||
export * from "./drag-drop";
|
||||
export * from "./drop";
|
||||
export * from "./enter-key-extension";
|
||||
export * from "./extensions";
|
||||
|
|
@ -18,4 +17,5 @@ export * from "./horizontal-rule";
|
|||
export * from "./keymap";
|
||||
export * from "./quote";
|
||||
export * from "./read-only-extensions";
|
||||
export * from "./side-menu";
|
||||
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 * as Y from "yjs";
|
||||
// extensions
|
||||
import { DragAndDrop, IssueWidget } from "@/extensions";
|
||||
import { IssueWidget, SideMenuExtension } from "@/extensions";
|
||||
// hooks
|
||||
import { TFileHandler, useEditor } from "@/hooks/use-editor";
|
||||
// plane editor extensions
|
||||
|
|
@ -30,7 +30,6 @@ type DocumentEditorProps = {
|
|||
};
|
||||
onChange: (updates: Uint8Array) => void;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
setHideDragHandleFunction: (hideDragHandlerFromDragDrop: () => void) => void;
|
||||
tabIndex?: number;
|
||||
value: Uint8Array;
|
||||
};
|
||||
|
|
@ -48,7 +47,6 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
|
|||
mentionHandler,
|
||||
onChange,
|
||||
placeholder,
|
||||
setHideDragHandleFunction,
|
||||
tabIndex,
|
||||
value,
|
||||
} = props;
|
||||
|
|
@ -95,7 +93,10 @@ export const useDocumentEditor = (props: DocumentEditorProps) => {
|
|||
forwardedRef,
|
||||
mentionHandler,
|
||||
extensions: [
|
||||
DragAndDrop(setHideDragHandleFunction),
|
||||
SideMenuExtension({
|
||||
aiEnabled: !disabledExtensions?.includes("ai"),
|
||||
dragDropEnabled: true,
|
||||
}),
|
||||
embedHandler?.issue &&
|
||||
IssueWidget({
|
||||
widgetCallback: embedHandler.issue.widgetCallback,
|
||||
|
|
|
|||
|
|
@ -1,50 +1,33 @@
|
|||
import { Extension } from "@tiptap/core";
|
||||
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
|
||||
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
|
||||
// extensions
|
||||
import { SideMenuHandleOptions, SideMenuPluginProps } from "@/extensions";
|
||||
|
||||
export interface DragHandleOptions {
|
||||
dragHandleWidth: number;
|
||||
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
|
||||
scrollThreshold: {
|
||||
up: number;
|
||||
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 dragHandleClassName =
|
||||
"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";
|
||||
const dragHandleContainerClassName = "size-[15px] grid place-items-center";
|
||||
const dragHandleDotsClassName = "h-full w-3 grid grid-cols-2 place-items-center";
|
||||
const dragHandleDotClassName = "size-[2.5px] bg-custom-text-300 rounded-[50%]";
|
||||
|
||||
const createDragHandleElement = (): HTMLElement => {
|
||||
const dragHandleElement = document.createElement("button");
|
||||
dragHandleElement.type = "button";
|
||||
dragHandleElement.draggable = true;
|
||||
dragHandleElement.dataset.dragHandle = "";
|
||||
dragHandleElement.classList.add("drag-handle");
|
||||
dragHandleElement.classList.value = dragHandleClassName;
|
||||
|
||||
const dragHandleContainer = document.createElement("span");
|
||||
dragHandleContainer.classList.add("drag-handle-container");
|
||||
dragHandleContainer.classList.value = dragHandleContainerClassName;
|
||||
dragHandleElement.appendChild(dragHandleContainer);
|
||||
|
||||
const dotsContainer = document.createElement("span");
|
||||
dotsContainer.classList.add("drag-handle-dots");
|
||||
dotsContainer.classList.value = dragHandleDotsClassName;
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const spanElement = document.createElement("span");
|
||||
spanElement.classList.add("drag-handle-dot");
|
||||
spanElement.classList.value = dragHandleDotClassName;
|
||||
dotsContainer.appendChild(spanElement);
|
||||
}
|
||||
|
||||
|
|
@ -53,16 +36,6 @@ const createDragHandleElement = (): HTMLElement => {
|
|||
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 elements = document.elementsFromPoint(coords.x, coords.y);
|
||||
const generalSelectors = [
|
||||
|
|
@ -98,7 +71,7 @@ const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
const nodePosAtDOM = (node: Element, view: EditorView, options: DragHandleOptions) => {
|
||||
const nodePosAtDOM = (node: Element, view: EditorView, options: SideMenuPluginProps) => {
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
|
||||
return view.posAtCoords({
|
||||
|
|
@ -132,7 +105,7 @@ const calcNodePos = (pos: number, view: EditorView, node: Element) => {
|
|||
return safePos;
|
||||
};
|
||||
|
||||
const DragHandle = (options: DragHandleOptions) => {
|
||||
export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOptions => {
|
||||
let listType = "";
|
||||
const handleDragStart = (event: DragEvent, view: EditorView) => {
|
||||
view.focus();
|
||||
|
|
@ -222,15 +195,15 @@ const DragHandle = (options: DragHandleOptions) => {
|
|||
if (!(node instanceof Element)) return;
|
||||
|
||||
if (node.matches("blockquote")) {
|
||||
let nodePosForBlockquotes = nodePosAtDOMForBlockQuotes(node, view);
|
||||
if (nodePosForBlockquotes === null || nodePosForBlockquotes === undefined) return;
|
||||
let nodePosForBlockQuotes = nodePosAtDOMForBlockQuotes(node, view);
|
||||
if (nodePosForBlockQuotes === null || nodePosForBlockQuotes === undefined) return;
|
||||
|
||||
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
|
||||
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockquotes);
|
||||
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockQuotes);
|
||||
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
||||
}
|
||||
return;
|
||||
|
|
@ -253,152 +226,96 @@ const DragHandle = (options: DragHandleOptions) => {
|
|||
|
||||
let dragHandleElement: HTMLElement | null = null;
|
||||
// drag handle view actions
|
||||
const hideDragHandle = () => dragHandleElement?.classList.add("drag-handle-hidden");
|
||||
const showDragHandle = () => dragHandleElement?.classList.remove("drag-handle-hidden");
|
||||
const hideDragHandle = () => {
|
||||
if (!dragHandleElement?.classList.contains("drag-handle-hidden"))
|
||||
dragHandleElement?.classList.add("drag-handle-hidden");
|
||||
};
|
||||
|
||||
options.setHideDragHandle?.(hideDragHandle);
|
||||
const view = (view: EditorView, sideMenu: HTMLDivElement | null) => {
|
||||
dragHandleElement = createDragHandleElement();
|
||||
dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view));
|
||||
dragHandleElement.addEventListener("click", (e) => handleClick(e, view));
|
||||
dragHandleElement.addEventListener("contextmenu", (e) => handleClick(e, view));
|
||||
|
||||
return new Plugin({
|
||||
key: new PluginKey("dragHandle"),
|
||||
view: (view) => {
|
||||
dragHandleElement = createDragHandleElement();
|
||||
dragHandleElement.addEventListener("dragstart", (e) => handleDragStart(e, view));
|
||||
dragHandleElement.addEventListener("click", (e) => handleClick(e, view));
|
||||
dragHandleElement.addEventListener("contextmenu", (e) => handleClick(e, view));
|
||||
dragHandleElement.addEventListener("drag", (e) => {
|
||||
hideDragHandle();
|
||||
const frameRenderer = document.querySelector(".frame-renderer");
|
||||
if (!frameRenderer) return;
|
||||
if (e.clientY < options.scrollThreshold.up) {
|
||||
frameRenderer.scrollBy({ top: -70, behavior: "smooth" });
|
||||
} else if (window.innerHeight - e.clientY < options.scrollThreshold.down) {
|
||||
frameRenderer.scrollBy({ top: 70, behavior: "smooth" });
|
||||
}
|
||||
});
|
||||
|
||||
dragHandleElement.addEventListener("drag", (e) => {
|
||||
hideDragHandle();
|
||||
const frameRenderer = document.querySelector(".frame-renderer");
|
||||
if (!frameRenderer) return;
|
||||
if (e.clientY < options.scrollThreshold.up) {
|
||||
frameRenderer.scrollBy({ top: -70, behavior: "smooth" });
|
||||
} else if (window.innerHeight - e.clientY < options.scrollThreshold.down) {
|
||||
frameRenderer.scrollBy({ top: 70, behavior: "smooth" });
|
||||
}
|
||||
hideDragHandle();
|
||||
|
||||
sideMenu?.appendChild(dragHandleElement);
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
dragHandleElement?.remove?.();
|
||||
dragHandleElement = null;
|
||||
},
|
||||
};
|
||||
};
|
||||
const domEvents = {
|
||||
dragenter: (view: EditorView) => {
|
||||
view.dom.classList.add("dragging");
|
||||
hideDragHandle();
|
||||
},
|
||||
drop: (view: EditorView, event: DragEvent) => {
|
||||
view.dom.classList.remove("dragging");
|
||||
hideDragHandle();
|
||||
let droppedNode: Node | null = null;
|
||||
const dropPos = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
|
||||
hideDragHandle();
|
||||
if (!dropPos) return;
|
||||
|
||||
view?.dom?.parentElement?.appendChild(dragHandleElement);
|
||||
if (view.state.selection instanceof NodeSelection) {
|
||||
droppedNode = view.state.selection.node;
|
||||
}
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
dragHandleElement?.remove?.();
|
||||
dragHandleElement = null;
|
||||
},
|
||||
};
|
||||
if (!droppedNode) return;
|
||||
|
||||
const resolvedPos = view.state.doc.resolve(dropPos.pos);
|
||||
let isDroppedInsideList = false;
|
||||
|
||||
// Traverse up the document tree to find if we're inside a list item
|
||||
for (let i = resolvedPos.depth; i > 0; i--) {
|
||||
if (resolvedPos.node(i).type.name === "listItem") {
|
||||
isDroppedInsideList = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the selected node is a list item and is not dropped inside a list, we need to wrap it inside <ol> tag otherwise ol list items will be transformed into ul list item when dropped
|
||||
if (
|
||||
view.state.selection instanceof NodeSelection &&
|
||||
view.state.selection.node.type.name === "listItem" &&
|
||||
!isDroppedInsideList &&
|
||||
listType == "OL"
|
||||
) {
|
||||
const text = droppedNode.textContent;
|
||||
if (!text) return;
|
||||
const paragraph = view.state.schema.nodes.paragraph?.createAndFill({}, view.state.schema.text(text));
|
||||
const listItem = view.state.schema.nodes.listItem?.createAndFill({}, paragraph);
|
||||
|
||||
const newList = view.state.schema.nodes.orderedList?.createAndFill(null, listItem);
|
||||
const slice = new Slice(Fragment.from(newList), 0, 0);
|
||||
view.dragging = { slice, move: event.ctrlKey };
|
||||
}
|
||||
},
|
||||
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")) {
|
||||
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");
|
||||
hideDragHandle();
|
||||
},
|
||||
drop: (view, event) => {
|
||||
view.dom.classList.remove("dragging");
|
||||
hideDragHandle();
|
||||
let droppedNode: Node | null = null;
|
||||
const dropPos = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
|
||||
if (!dropPos) return;
|
||||
|
||||
if (view.state.selection instanceof NodeSelection) {
|
||||
droppedNode = view.state.selection.node;
|
||||
}
|
||||
|
||||
if (!droppedNode) return;
|
||||
|
||||
const resolvedPos = view.state.doc.resolve(dropPos.pos);
|
||||
let isDroppedInsideList = false;
|
||||
|
||||
// Traverse up the document tree to find if we're inside a list item
|
||||
for (let i = resolvedPos.depth; i > 0; i--) {
|
||||
if (resolvedPos.node(i).type.name === "listItem") {
|
||||
isDroppedInsideList = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the selected node is a list item and is not dropped inside a list, we need to wrap it inside <ol> tag otherwise ol list items will be transformed into ul list item when dropped
|
||||
if (
|
||||
view.state.selection instanceof NodeSelection &&
|
||||
view.state.selection.node.type.name === "listItem" &&
|
||||
!isDroppedInsideList &&
|
||||
listType == "OL"
|
||||
) {
|
||||
const text = droppedNode.textContent;
|
||||
if (!text) return;
|
||||
const paragraph = view.state.schema.nodes.paragraph?.createAndFill({}, view.state.schema.text(text));
|
||||
const listItem = view.state.schema.nodes.listItem?.createAndFill({}, paragraph);
|
||||
|
||||
const newList = view.state.schema.nodes.orderedList?.createAndFill(null, listItem);
|
||||
const slice = new Slice(Fragment.from(newList), 0, 0);
|
||||
view.dragging = { slice, move: event.ctrlKey };
|
||||
}
|
||||
},
|
||||
dragend: (view) => {
|
||||
view.dom.classList.remove("dragging");
|
||||
},
|
||||
},
|
||||
dragend: (view: EditorView) => {
|
||||
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 */
|
||||
.drag-handle {
|
||||
/* side menu */
|
||||
#editor-side-menu {
|
||||
position: fixed;
|
||||
opacity: 1;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
aspect-ratio: 1 / 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
z-index: 5;
|
||||
cursor: grab;
|
||||
border-radius: 2px;
|
||||
outline: none !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 100;
|
||||
transition:
|
||||
opacity 0.2s ease 0.2s,
|
||||
background-color 0.2s ease,
|
||||
top 0.2s ease,
|
||||
left 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--color-background-80));
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(var(--color-background-80));
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&.drag-handle-hidden {
|
||||
&.side-menu-hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
@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 */
|
||||
/* end side menu */
|
||||
|
||||
.ProseMirror:not(.dragging) .ProseMirror-selectednode {
|
||||
position: relative;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue