diff --git a/packages/editor/src/ce/extensions/ai-features/handle.ts b/packages/editor/src/ce/extensions/ai-features/handle.ts new file mode 100644 index 00000000000..d477d228ac9 --- /dev/null +++ b/packages/editor/src/ce/extensions/ai-features/handle.ts @@ -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, + }; +}; diff --git a/packages/editor/src/ce/extensions/ai-features/index.ts b/packages/editor/src/ce/extensions/ai-features/index.ts new file mode 100644 index 00000000000..af0faafca65 --- /dev/null +++ b/packages/editor/src/ce/extensions/ai-features/index.ts @@ -0,0 +1 @@ +export * from "./handle"; diff --git a/packages/editor/src/ce/extensions/index.ts b/packages/editor/src/ce/extensions/index.ts index 4a975b8c5a1..172d9ee1a76 100644 --- a/packages/editor/src/ce/extensions/index.ts +++ b/packages/editor/src/ce/extensions/index.ts @@ -1 +1,2 @@ +export * from "./ai-features"; export * from "./document-extensions"; diff --git a/packages/editor/src/core/components/editors/document/editor.tsx b/packages/editor/src/core/components/editors/document/editor.tsx index 503dc76e0a1..2ec49acc0c7 100644 --- a/packages/editor/src/core/components/editors/document/editor.tsx +++ b/packages/editor/src/core/components/editors/document/editor.tsx @@ -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 ( - + ); }; diff --git a/packages/editor/src/core/components/editors/document/page-renderer.tsx b/packages/editor/src/core/components/editors/document/page-renderer.tsx index 9254d52c654..2de5f6c571e 100644 --- a/packages/editor/src/core/components/editors/document/page-renderer.tsx +++ b/packages/editor/src/core/components/editors/document/page-renderer.tsx @@ -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(); const [isOpen, setIsOpen] = useState(false); @@ -129,14 +128,9 @@ export const PageRenderer = (props: IPageRenderer) => { return ( <>
- + - {editor && editor.isEditable && } + {editor.isEditable && }
{isOpen && linkViewProps && coordinates && ( diff --git a/packages/editor/src/core/components/editors/editor-container.tsx b/packages/editor/src/core/components/editors/editor-container.tsx index 5c09f42e56f..5102cf9ac3a 100644 --- a/packages/editor/src/core/components/editors/editor-container.tsx +++ b/packages/editor/src/core/components/editors/editor-container.tsx @@ -7,12 +7,11 @@ interface EditorContainerProps { children: ReactNode; editor: Editor | null; editorContainerClassName: string; - hideDragHandle?: () => void; id: string; } export const EditorContainer: FC = (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 = (props) => { } }; + const handleContainerMouseLeave = () => { + const dragHandleElement = document.querySelector("#editor-side-menu"); + if (!dragHandleElement?.classList.contains("side-menu-hidden")) { + dragHandleElement?.classList.add("side-menu-hidden"); + } + }; + return (
React.ReactNode; extensions: Extension[]; - hideDragHandleOnMouseLeave: () => void; }; export const EditorWrapper: React.FC = (props) => { @@ -20,7 +19,6 @@ export const EditorWrapper: React.FC = (props) => { containerClassName, editorClassName = "", extensions, - hideDragHandleOnMouseLeave, id, initialValue, fileHandler, @@ -56,12 +54,7 @@ export const EditorWrapper: React.FC = (props) => { if (!editor) return null; return ( - + {children?.(editor)}
diff --git a/packages/editor/src/core/components/editors/lite-text/editor.tsx b/packages/editor/src/core/components/editors/lite-text/editor.tsx index 0ef7080220a..924706aae87 100644 --- a/packages/editor/src/core/components/editors/lite-text/editor.tsx +++ b/packages/editor/src/core/components/editors/lite-text/editor.tsx @@ -11,7 +11,7 @@ const LiteTextEditor = (props: ILiteTextEditor) => { const extensions = [EnterKeyExtension(onEnterKeyPress)]; - return {}} />; + return ; }; const LiteTextEditorWithRef = forwardRef((props, ref) => ( diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index ead73d99d90..28204237275 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -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 ( - + {(editor) => <>{editor && }} ); diff --git a/packages/editor/src/core/extensions/index.ts b/packages/editor/src/core/extensions/index.ts index 220a11757ce..9c9e74ff94f 100644 --- a/packages/editor/src/core/extensions/index.ts +++ b/packages/editor/src/core/extensions/index.ts @@ -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"; diff --git a/packages/editor/src/core/extensions/side-menu.tsx b/packages/editor/src/core/extensions/side-menu.tsx new file mode 100644 index 00000000000..c382469a3b5 --- /dev/null +++ b/packages/editor/src/core/extensions/side-menu.tsx @@ -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

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); + } + }, + }, + }, + }); +}; diff --git a/packages/editor/src/core/hooks/use-document-editor.ts b/packages/editor/src/core/hooks/use-document-editor.ts index 58a45b74730..21b224b9d42 100644 --- a/packages/editor/src/core/hooks/use-document-editor.ts +++ b/packages/editor/src/core/hooks/use-document-editor.ts @@ -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, diff --git a/packages/editor/src/core/extensions/drag-drop.tsx b/packages/editor/src/core/plugins/drag-handle.ts similarity index 52% rename from packages/editor/src/core/extensions/drag-drop.tsx rename to packages/editor/src/core/plugins/drag-handle.ts index 458ccbf70d1..32b6301d53a 100644 --- a/packages/editor/src/core/extensions/drag-drop.tsx +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -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"); - - options.setHideDragHandle?.(hideDragHandle); - - 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" }); - } - }); + const hideDragHandle = () => { + if (!dragHandleElement?.classList.contains("drag-handle-hidden")) + dragHandleElement?.classList.add("drag-handle-hidden"); + }; + 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)); + + 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" }); + } + }); - view?.dom?.parentElement?.appendChild(dragHandleElement); + hideDragHandle(); - return { - destroy: () => { - dragHandleElement?.remove?.(); - dragHandleElement = null; - }, - }; - }, - 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

    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"); - }, + 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, + }); + + 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
      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: EditorView) => { + view.dom.classList.remove("dragging"); + }, + }; + + return { + view, + domEvents, + }; }; diff --git a/packages/editor/src/core/types/extensions.ts b/packages/editor/src/core/types/extensions.ts index d4a26e803d3..4f86b412007 100644 --- a/packages/editor/src/core/types/extensions.ts +++ b/packages/editor/src/core/types/extensions.ts @@ -1 +1 @@ -export type TExtensions = "issue-embed"; +export type TExtensions = "ai" | "issue-embed"; diff --git a/packages/editor/src/ee/extensions/ai-features/index.ts b/packages/editor/src/ee/extensions/ai-features/index.ts new file mode 100644 index 00000000000..5e22830460d --- /dev/null +++ b/packages/editor/src/ee/extensions/ai-features/index.ts @@ -0,0 +1 @@ +export * from "src/ce/extensions/ai-features"; diff --git a/packages/editor/src/styles/drag-drop.css b/packages/editor/src/styles/drag-drop.css index c0970630bd1..c205c0699b7 100644 --- a/packages/editor/src/styles/drag-drop.css +++ b/packages/editor/src/styles/drag-drop.css @@ -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;