diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx index daaa5617737..120ade7127e 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx @@ -1,5 +1,5 @@ -import { Extensions } from "@tiptap/core"; -import React from "react"; +import type { Extensions } from "@tiptap/core"; +import React, { useMemo } from "react"; // plane imports import { cn } from "@plane/utils"; // components @@ -13,26 +13,32 @@ import { getEditorClassNames } from "@/helpers/common"; // hooks import { useCollaborativeEditor } from "@/hooks/use-collaborative-editor"; // types -import { EditorRefApi, ICollaborativeDocumentEditorProps } from "@/types"; +import type { EditorRefApi, ICollaborativeDocumentEditorProps } from "@/types"; const CollaborativeDocumentEditor: React.FC = (props) => { const { aiHandler, bubbleMenuEnabled = true, containerClassName, + documentLoaderClassName, + extensions: externalExtensions = [], disabledExtensions, displayConfig = DEFAULT_DISPLAY_CONFIG, editable, editorClassName = "", + editorProps, embedHandler, fileHandler, flaggedExtensions, forwardedRef, handleEditorReady, id, + dragDropEnabled = true, + isTouchDevice, mentionHandler, onAssetChange, onChange, + onEditorFocus, onTransaction, placeholder, realtimeConfig, @@ -41,21 +47,26 @@ const CollaborativeDocumentEditor: React.FC = user, } = props; - const extensions: Extensions = []; + const extensions: Extensions = useMemo(() => { + const allExtensions = [...externalExtensions]; - if (embedHandler?.issue) { - extensions.push( - WorkItemEmbedExtension({ - widgetCallback: embedHandler.issue.widgetCallback, - }) - ); - } + if (embedHandler?.issue) { + allExtensions.push( + WorkItemEmbedExtension({ + widgetCallback: embedHandler.issue.widgetCallback, + }) + ); + } + + return allExtensions; + }, [externalExtensions, embedHandler.issue]); // use document editor const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({ disabledExtensions, editable, editorClassName, + editorProps, embedHandler, extensions, fileHandler, @@ -63,9 +74,12 @@ const CollaborativeDocumentEditor: React.FC = forwardedRef, handleEditorReady, id, + dragDropEnabled, + isTouchDevice, mentionHandler, onAssetChange, onChange, + onEditorFocus, onTransaction, placeholder, realtimeConfig, @@ -87,9 +101,11 @@ const CollaborativeDocumentEditor: React.FC = aiHandler={aiHandler} bubbleMenuEnabled={bubbleMenuEnabled} displayConfig={displayConfig} + documentLoaderClassName={documentLoaderClassName} editor={editor} editorContainerClassName={cn(editorContainerClassNames, "document-editor")} id={id} + isTouchDevice={!!isTouchDevice} isLoading={!hasServerSynced && !hasServerConnectionFailed} tabIndex={tabIndex} /> 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 1316d2d7249..266476788c1 100644 --- a/packages/editor/src/core/components/editors/document/page-renderer.tsx +++ b/packages/editor/src/core/components/editors/document/page-renderer.tsx @@ -11,16 +11,28 @@ type Props = { aiHandler?: TAIHandler; bubbleMenuEnabled: boolean; displayConfig: TDisplayConfig; + documentLoaderClassName?: string; editor: Editor; editorContainerClassName: string; id: string; isLoading?: boolean; + isTouchDevice: boolean; tabIndex?: number; }; export const PageRenderer = (props: Props) => { - const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, isLoading, tabIndex } = - props; + const { + aiHandler, + bubbleMenuEnabled, + displayConfig, + documentLoaderClassName, + editor, + editorContainerClassName, + id, + isLoading, + isTouchDevice, + tabIndex, + } = props; return (
{ })} > {isLoading ? ( - + ) : ( - {editor.isEditable && ( + {editor.isEditable && !isTouchDevice && (
{bubbleMenuEnabled && } diff --git a/packages/editor/src/core/components/editors/editor-container.tsx b/packages/editor/src/core/components/editors/editor-container.tsx index f189bde9850..bf8bf36579c 100644 --- a/packages/editor/src/core/components/editors/editor-container.tsx +++ b/packages/editor/src/core/components/editors/editor-container.tsx @@ -1,4 +1,4 @@ -import { Editor } from "@tiptap/react"; +import type { Editor } from "@tiptap/react"; import { FC, ReactNode, useRef } from "react"; // plane utils import { cn } from "@plane/utils"; @@ -10,16 +10,18 @@ import { TDisplayConfig } from "@/types"; // components import { LinkViewContainer } from "./link-view-container"; -interface EditorContainerProps { +type Props = { children: ReactNode; displayConfig: TDisplayConfig; editor: Editor; editorContainerClassName: string; id: string; -} + isTouchDevice: boolean; +}; -export const EditorContainer: FC = (props) => { - const { children, displayConfig, editor, editorContainerClassName, id } = props; +export const EditorContainer: FC = (props) => { + const { children, displayConfig, editor, editorContainerClassName, id, isTouchDevice } = props; + // refs const containerRef = useRef(null); const handleContainerClick = (event: React.MouseEvent) => { @@ -94,7 +96,7 @@ export const EditorContainer: FC = (props) => { )} > {children} - + {!isTouchDevice && }
); diff --git a/packages/editor/src/core/components/editors/editor-wrapper.tsx b/packages/editor/src/core/components/editors/editor-wrapper.tsx index 0ca626683dc..51c3ea156dd 100644 --- a/packages/editor/src/core/components/editors/editor-wrapper.tsx +++ b/packages/editor/src/core/components/editors/editor-wrapper.tsx @@ -24,14 +24,17 @@ export const EditorWrapper: React.FC = (props) => { displayConfig = DEFAULT_DISPLAY_CONFIG, editable, editorClassName = "", + editorProps, extensions, id, initialValue, + isTouchDevice, fileHandler, flaggedExtensions, forwardedRef, mentionHandler, onChange, + onEditorFocus, onTransaction, handleEditorReady, autofocus, @@ -44,15 +47,18 @@ export const EditorWrapper: React.FC = (props) => { editable, disabledExtensions, editorClassName, + editorProps, enableHistory: true, extensions, fileHandler, flaggedExtensions, forwardedRef, id, + isTouchDevice, initialValue, mentionHandler, onChange, + onEditorFocus, onTransaction, handleEditorReady, autofocus, @@ -75,6 +81,7 @@ export const EditorWrapper: React.FC = (props) => { editor={editor} editorContainerClassName={editorContainerClassName} id={id} + isTouchDevice={!!isTouchDevice} > {children?.(editor)}
diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index c3aa4d414a7..cfa1213c1a1 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -22,6 +22,7 @@ import { MinusSquare, Palette, AlignCenter, + LinkIcon, } from "lucide-react"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; @@ -30,6 +31,7 @@ import { insertHorizontalRule, insertImage, insertTableCommand, + setLinkEditor, setText, setTextAlign, toggleBackgroundColor, @@ -44,6 +46,7 @@ import { toggleTaskList, toggleTextColor, toggleUnderline, + unsetLinkEditor, } from "@/helpers/editor-commands"; // types import { TCommandWithProps, TEditorCommands } from "@/types"; @@ -189,7 +192,7 @@ export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({ icon: ImageIcon, }); -export const HorizontalRuleItem = (editor: Editor) => +export const HorizontalRuleItem = (editor: Editor): EditorMenuItem<"divider"> => ({ key: "divider", name: "Divider", @@ -198,6 +201,19 @@ export const HorizontalRuleItem = (editor: Editor) => icon: MinusSquare, }) as const; +export const LinkItem = (editor: Editor): EditorMenuItem<"link"> => + ({ + key: "link", + name: "Link", + isActive: () => editor?.isActive("link"), + command: (props) => { + if (!props) return; + if (props.url) setLinkEditor(editor, props.url, props.text); + else unsetLinkEditor(editor); + }, + icon: LinkIcon, + }) as const; + export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ({ key: "text-color", name: "Color", @@ -254,6 +270,7 @@ export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem = (props) => { const imageRef = useRef(null); const [hasErroredOnFirstLoad, setHasErroredOnFirstLoad] = useState(false); const [hasTriedRestoringImageOnce, setHasTriedRestoringImageOnce] = useState(false); + // extension options + const isTouchDevice = !!getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY).isTouchDevice; const updateAttributesSafely = useCallback( (attributes: Partial, errorMessage: string) => { @@ -188,11 +194,15 @@ export const CustomImageBlock: React.FC = (props) => { const handleImageMouseDown = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); + if (isTouchDevice) { + e.preventDefault(); + editor.commands.blur(); + } const pos = getPos(); const nodeSelection = NodeSelection.create(editor.state.doc, pos); editor.view.dispatch(editor.state.tr.setSelection(nodeSelection)); }, - [editor, getPos] + [editor, getPos, isTouchDevice] ); // show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or) @@ -254,7 +264,12 @@ export const CustomImageBlock: React.FC = (props) => { if (!resolvedImageSrc) { throw new Error("No resolved image source available"); } - imageRef.current.src = resolvedImageSrc; + if (isTouchDevice) { + const refreshedSrc = await extension.options.getImageSource?.(imgNodeSrc); + imageRef.current.src = refreshedSrc; + } else { + imageRef.current.src = resolvedImageSrc; + } } catch { // if the image failed to even restore, then show the error state setFailedToLoadImage(true); @@ -281,14 +296,15 @@ export const CustomImageBlock: React.FC = (props) => { updateAttributesSafely({ alignment }, "Failed to update attributes while changing alignment:") } + height={size.height} + isTouchDevice={isTouchDevice} + width={size.width} + src={resolvedImageSrc} /> )} {selected && displayedImageSrc === resolvedImageSrc && ( diff --git a/packages/editor/src/core/extensions/custom-image/components/node-view.tsx b/packages/editor/src/core/extensions/custom-image/components/node-view.tsx index eb38ab40542..48a8ee5f76f 100644 --- a/packages/editor/src/core/extensions/custom-image/components/node-view.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/node-view.tsx @@ -24,7 +24,7 @@ export const CustomImageNodeView: React.FC = (props) = const { editor, extension, node } = props; const { src: imgNodeSrc } = node.attrs; - const [isUploaded, setIsUploaded] = useState(false); + const [isUploaded, setIsUploaded] = useState(!!imgNodeSrc); const [resolvedSrc, setResolvedSrc] = useState(undefined); const [resolvedDownloadSrc, setResolvedDownloadSrc] = useState(undefined); const [imageFromFileSystem, setImageFromFileSystem] = useState(undefined); @@ -43,13 +43,13 @@ export const CustomImageNodeView: React.FC = (props) = // the image is already uploaded if the image-component node has src attribute // and we need to remove the blob from our file system useEffect(() => { - if (resolvedSrc) { + if (resolvedSrc || imgNodeSrc) { setIsUploaded(true); setImageFromFileSystem(undefined); } else { setIsUploaded(false); } - }, [resolvedSrc]); + }, [resolvedSrc, imgNodeSrc]); useEffect(() => { if (!imgNodeSrc) { diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx index d7e4be9caa5..65f42ccfd04 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/modal.tsx @@ -11,15 +11,16 @@ const ZOOM_STEPS = [0.5, 1, 1.5, 2]; type Props = { aspectRatio: number; - isFullScreenEnabled: boolean; downloadSrc: string; + isFullScreenEnabled: boolean; + isTouchDevice: boolean; src: string; toggleFullScreenMode: (val: boolean) => void; width: string; }; const ImageFullScreenModalWithoutPortal = (props: Props) => { - const { aspectRatio, isFullScreenEnabled, downloadSrc, src, toggleFullScreenMode, width } = props; + const { aspectRatio, isFullScreenEnabled, isTouchDevice, downloadSrc, src, toggleFullScreenMode, width } = props; // refs const dragStart = useRef({ x: 0, y: 0 }); const dragOffset = useRef({ x: 0, y: 0 }); @@ -233,7 +234,13 @@ const ImageFullScreenModalWithoutPortal = (props: Props) => {
- - + {!isTouchDevice && ( + + )} + {!isTouchDevice && ( + + )}
@@ -279,7 +296,10 @@ export const ImageFullScreenModal: React.FC = (props) => { if (portal) { modal = ReactDOM.createPortal(modal, portal); } else { - console.warn("Portal element #editor-portal not found. Rendering inline."); + console.warn("Portal element #editor-portal not found. Rendering in document.body"); + if (typeof document !== "undefined" && document.body) { + modal = ReactDOM.createPortal(modal, document.body); + } } return modal; }; diff --git a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx index 2108bfeaaee..2d8e7360614 100644 --- a/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/toolbar/full-screen/root.tsx @@ -7,17 +7,18 @@ import { ImageFullScreenModal } from "./modal"; type Props = { image: { + aspectRatio: number; downloadSrc: string; - src: string; height: string; + src: string; width: string; - aspectRatio: number; }; + isTouchDevice: boolean; toggleToolbarViewStatus: (val: boolean) => void; }; export const ImageFullScreenActionRoot: React.FC = (props) => { - const { image, toggleToolbarViewStatus } = props; + const { image, isTouchDevice, toggleToolbarViewStatus } = props; // states const [isFullScreenEnabled, setIsFullScreenEnabled] = useState(false); // derived values @@ -31,13 +32,14 @@ export const ImageFullScreenActionRoot: React.FC = (props) => { <> - +