[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:
Aaryan Khandelwal 2024-08-08 14:48:05 +05:30 committed by GitHub
parent 943dd593fa
commit e805c49e69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 356 additions and 293 deletions

View 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,
};
};

View file

@ -0,0 +1 @@
export * from "./handle";

View file

@ -1 +1,2 @@
export * from "./ai-features";
export * from "./document-extensions";

View file

@ -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} />
);
};

View file

@ -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 && (

View file

@ -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",
{

View file

@ -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} />

View file

@ -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) => (

View file

@ -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>
);

View file

@ -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";

View 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);
}
},
},
},
});
};

View file

@ -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,

View file

@ -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,
};
};

View file

@ -1 +1 @@
export type TExtensions = "issue-embed";
export type TExtensions = "ai" | "issue-embed";

View file

@ -0,0 +1 @@
export * from "src/ce/extensions/ai-features";

View file

@ -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;