Skip to content

Commit c3273b1

Browse files
[WIKI-568] refactor: add touch device support to editor (#7439)
* refactor: add isTouchDevice prop * chore: handle event propagation in touch devices * refactor: isTouchDevice implementation * chore: misc editor updates and utility functions (#7455) * chore: misc editor updated and utility functions * fix: code review * passed isTouchDevice prop to editor-wrapper * added more props to editor-wrapper. * chore: update types --------- Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com> * fix: remove unnecessary deps --------- Co-authored-by: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com>
1 parent 7cec921 commit c3273b1

21 files changed

Lines changed: 329 additions & 81 deletions

File tree

packages/editor/src/core/components/editors/document/collaborative-editor.tsx

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Extensions } from "@tiptap/core";
2-
import React from "react";
1+
import type { Extensions } from "@tiptap/core";
2+
import React, { useMemo } from "react";
33
// plane imports
44
import { cn } from "@plane/utils";
55
// components
@@ -13,26 +13,32 @@ import { getEditorClassNames } from "@/helpers/common";
1313
// hooks
1414
import { useCollaborativeEditor } from "@/hooks/use-collaborative-editor";
1515
// types
16-
import { EditorRefApi, ICollaborativeDocumentEditorProps } from "@/types";
16+
import type { EditorRefApi, ICollaborativeDocumentEditorProps } from "@/types";
1717

1818
const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> = (props) => {
1919
const {
2020
aiHandler,
2121
bubbleMenuEnabled = true,
2222
containerClassName,
23+
documentLoaderClassName,
24+
extensions: externalExtensions = [],
2325
disabledExtensions,
2426
displayConfig = DEFAULT_DISPLAY_CONFIG,
2527
editable,
2628
editorClassName = "",
29+
editorProps,
2730
embedHandler,
2831
fileHandler,
2932
flaggedExtensions,
3033
forwardedRef,
3134
handleEditorReady,
3235
id,
36+
dragDropEnabled = true,
37+
isTouchDevice,
3338
mentionHandler,
3439
onAssetChange,
3540
onChange,
41+
onEditorFocus,
3642
onTransaction,
3743
placeholder,
3844
realtimeConfig,
@@ -41,31 +47,39 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
4147
user,
4248
} = props;
4349

44-
const extensions: Extensions = [];
50+
const extensions: Extensions = useMemo(() => {
51+
const allExtensions = [...externalExtensions];
4552

46-
if (embedHandler?.issue) {
47-
extensions.push(
48-
WorkItemEmbedExtension({
49-
widgetCallback: embedHandler.issue.widgetCallback,
50-
})
51-
);
52-
}
53+
if (embedHandler?.issue) {
54+
allExtensions.push(
55+
WorkItemEmbedExtension({
56+
widgetCallback: embedHandler.issue.widgetCallback,
57+
})
58+
);
59+
}
60+
61+
return allExtensions;
62+
}, [externalExtensions, embedHandler.issue]);
5363

5464
// use document editor
5565
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
5666
disabledExtensions,
5767
editable,
5868
editorClassName,
69+
editorProps,
5970
embedHandler,
6071
extensions,
6172
fileHandler,
6273
flaggedExtensions,
6374
forwardedRef,
6475
handleEditorReady,
6576
id,
77+
dragDropEnabled,
78+
isTouchDevice,
6679
mentionHandler,
6780
onAssetChange,
6881
onChange,
82+
onEditorFocus,
6983
onTransaction,
7084
placeholder,
7185
realtimeConfig,
@@ -87,9 +101,11 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
87101
aiHandler={aiHandler}
88102
bubbleMenuEnabled={bubbleMenuEnabled}
89103
displayConfig={displayConfig}
104+
documentLoaderClassName={documentLoaderClassName}
90105
editor={editor}
91106
editorContainerClassName={cn(editorContainerClassNames, "document-editor")}
92107
id={id}
108+
isTouchDevice={!!isTouchDevice}
93109
isLoading={!hasServerSynced && !hasServerConnectionFailed}
94110
tabIndex={tabIndex}
95111
/>

packages/editor/src/core/components/editors/document/page-renderer.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,28 @@ type Props = {
1111
aiHandler?: TAIHandler;
1212
bubbleMenuEnabled: boolean;
1313
displayConfig: TDisplayConfig;
14+
documentLoaderClassName?: string;
1415
editor: Editor;
1516
editorContainerClassName: string;
1617
id: string;
1718
isLoading?: boolean;
19+
isTouchDevice: boolean;
1820
tabIndex?: number;
1921
};
2022

2123
export const PageRenderer = (props: Props) => {
22-
const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, isLoading, tabIndex } =
23-
props;
24+
const {
25+
aiHandler,
26+
bubbleMenuEnabled,
27+
displayConfig,
28+
documentLoaderClassName,
29+
editor,
30+
editorContainerClassName,
31+
id,
32+
isLoading,
33+
isTouchDevice,
34+
tabIndex,
35+
} = props;
2436

2537
return (
2638
<div
@@ -29,16 +41,17 @@ export const PageRenderer = (props: Props) => {
2941
})}
3042
>
3143
{isLoading ? (
32-
<DocumentContentLoader />
44+
<DocumentContentLoader className={documentLoaderClassName} />
3345
) : (
3446
<EditorContainer
3547
displayConfig={displayConfig}
3648
editor={editor}
3749
editorContainerClassName={editorContainerClassName}
3850
id={id}
51+
isTouchDevice={isTouchDevice}
3952
>
4053
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
41-
{editor.isEditable && (
54+
{editor.isEditable && !isTouchDevice && (
4255
<div>
4356
{bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}
4457
<BlockMenu editor={editor} />

packages/editor/src/core/components/editors/editor-container.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Editor } from "@tiptap/react";
1+
import type { Editor } from "@tiptap/react";
22
import { FC, ReactNode, useRef } from "react";
33
// plane utils
44
import { cn } from "@plane/utils";
@@ -10,16 +10,18 @@ import { TDisplayConfig } from "@/types";
1010
// components
1111
import { LinkViewContainer } from "./link-view-container";
1212

13-
interface EditorContainerProps {
13+
type Props = {
1414
children: ReactNode;
1515
displayConfig: TDisplayConfig;
1616
editor: Editor;
1717
editorContainerClassName: string;
1818
id: string;
19-
}
19+
isTouchDevice: boolean;
20+
};
2021

21-
export const EditorContainer: FC<EditorContainerProps> = (props) => {
22-
const { children, displayConfig, editor, editorContainerClassName, id } = props;
22+
export const EditorContainer: FC<Props> = (props) => {
23+
const { children, displayConfig, editor, editorContainerClassName, id, isTouchDevice } = props;
24+
// refs
2325
const containerRef = useRef<HTMLDivElement>(null);
2426

2527
const handleContainerClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
@@ -94,7 +96,7 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
9496
)}
9597
>
9698
{children}
97-
<LinkViewContainer editor={editor} containerRef={containerRef} />
99+
{!isTouchDevice && <LinkViewContainer editor={editor} containerRef={containerRef} />}
98100
</div>
99101
</>
100102
);

packages/editor/src/core/components/editors/editor-wrapper.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@ export const EditorWrapper: React.FC<Props> = (props) => {
2424
displayConfig = DEFAULT_DISPLAY_CONFIG,
2525
editable,
2626
editorClassName = "",
27+
editorProps,
2728
extensions,
2829
id,
2930
initialValue,
31+
isTouchDevice,
3032
fileHandler,
3133
flaggedExtensions,
3234
forwardedRef,
3335
mentionHandler,
3436
onChange,
37+
onEditorFocus,
3538
onTransaction,
3639
handleEditorReady,
3740
autofocus,
@@ -44,15 +47,18 @@ export const EditorWrapper: React.FC<Props> = (props) => {
4447
editable,
4548
disabledExtensions,
4649
editorClassName,
50+
editorProps,
4751
enableHistory: true,
4852
extensions,
4953
fileHandler,
5054
flaggedExtensions,
5155
forwardedRef,
5256
id,
57+
isTouchDevice,
5358
initialValue,
5459
mentionHandler,
5560
onChange,
61+
onEditorFocus,
5662
onTransaction,
5763
handleEditorReady,
5864
autofocus,
@@ -75,6 +81,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
7581
editor={editor}
7682
editorContainerClassName={editorContainerClassName}
7783
id={id}
84+
isTouchDevice={!!isTouchDevice}
7885
>
7986
{children?.(editor)}
8087
<div className="flex flex-col">

packages/editor/src/core/components/menus/menu-items.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
MinusSquare,
2323
Palette,
2424
AlignCenter,
25+
LinkIcon,
2526
} from "lucide-react";
2627
// constants
2728
import { CORE_EXTENSIONS } from "@/constants/extension";
@@ -30,6 +31,7 @@ import {
3031
insertHorizontalRule,
3132
insertImage,
3233
insertTableCommand,
34+
setLinkEditor,
3335
setText,
3436
setTextAlign,
3537
toggleBackgroundColor,
@@ -44,6 +46,7 @@ import {
4446
toggleTaskList,
4547
toggleTextColor,
4648
toggleUnderline,
49+
unsetLinkEditor,
4750
} from "@/helpers/editor-commands";
4851
// types
4952
import { TCommandWithProps, TEditorCommands } from "@/types";
@@ -189,7 +192,7 @@ export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({
189192
icon: ImageIcon,
190193
});
191194

192-
export const HorizontalRuleItem = (editor: Editor) =>
195+
export const HorizontalRuleItem = (editor: Editor): EditorMenuItem<"divider"> =>
193196
({
194197
key: "divider",
195198
name: "Divider",
@@ -198,6 +201,19 @@ export const HorizontalRuleItem = (editor: Editor) =>
198201
icon: MinusSquare,
199202
}) as const;
200203

204+
export const LinkItem = (editor: Editor): EditorMenuItem<"link"> =>
205+
({
206+
key: "link",
207+
name: "Link",
208+
isActive: () => editor?.isActive("link"),
209+
command: (props) => {
210+
if (!props) return;
211+
if (props.url) setLinkEditor(editor, props.url, props.text);
212+
else unsetLinkEditor(editor);
213+
},
214+
icon: LinkIcon,
215+
}) as const;
216+
201217
export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ({
202218
key: "text-color",
203219
name: "Color",
@@ -254,6 +270,7 @@ export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem<TEdito
254270
TableItem(editor),
255271
ImageItem(editor),
256272
HorizontalRuleItem(editor),
273+
LinkItem(editor),
257274
TextColorItem(editor),
258275
BackgroundColorItem(editor),
259276
TextAlignItem(editor),

packages/editor/src/core/extensions/custom-image/components/block.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { NodeSelection } from "@tiptap/pm/state";
22
import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react";
33
// plane imports
44
import { cn } from "@plane/utils";
5+
// constants
6+
import { CORE_EXTENSIONS } from "@/constants/extension";
7+
// helpers
8+
import { getExtensionStorage } from "@/helpers/get-extension-storage";
59
// local imports
610
import { Pixel, TCustomImageAttributes, TCustomImageSize } from "../types";
711
import { ensurePixelString, getImageBlockId } from "../utils";
@@ -57,6 +61,8 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
5761
const imageRef = useRef<HTMLImageElement>(null);
5862
const [hasErroredOnFirstLoad, setHasErroredOnFirstLoad] = useState(false);
5963
const [hasTriedRestoringImageOnce, setHasTriedRestoringImageOnce] = useState(false);
64+
// extension options
65+
const isTouchDevice = !!getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY).isTouchDevice;
6066

6167
const updateAttributesSafely = useCallback(
6268
(attributes: Partial<TCustomImageAttributes>, errorMessage: string) => {
@@ -188,11 +194,15 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
188194
const handleImageMouseDown = useCallback(
189195
(e: React.MouseEvent) => {
190196
e.stopPropagation();
197+
if (isTouchDevice) {
198+
e.preventDefault();
199+
editor.commands.blur();
200+
}
191201
const pos = getPos();
192202
const nodeSelection = NodeSelection.create(editor.state.doc, pos);
193203
editor.view.dispatch(editor.state.tr.setSelection(nodeSelection));
194204
},
195-
[editor, getPos]
205+
[editor, getPos, isTouchDevice]
196206
);
197207

198208
// 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<CustomImageBlockProps> = (props) => {
254264
if (!resolvedImageSrc) {
255265
throw new Error("No resolved image source available");
256266
}
257-
imageRef.current.src = resolvedImageSrc;
267+
if (isTouchDevice) {
268+
const refreshedSrc = await extension.options.getImageSource?.(imgNodeSrc);
269+
imageRef.current.src = refreshedSrc;
270+
} else {
271+
imageRef.current.src = resolvedImageSrc;
272+
}
258273
} catch {
259274
// if the image failed to even restore, then show the error state
260275
setFailedToLoadImage(true);
@@ -281,14 +296,15 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
281296
<ImageToolbarRoot
282297
alignment={nodeAlignment ?? "left"}
283298
editor={editor}
284-
width={size.width}
285-
height={size.height}
286299
aspectRatio={size.aspectRatio === null ? 1 : size.aspectRatio}
287-
src={resolvedImageSrc}
288300
downloadSrc={resolvedDownloadSrc}
289301
handleAlignmentChange={(alignment) =>
290302
updateAttributesSafely({ alignment }, "Failed to update attributes while changing alignment:")
291303
}
304+
height={size.height}
305+
isTouchDevice={isTouchDevice}
306+
width={size.width}
307+
src={resolvedImageSrc}
292308
/>
293309
)}
294310
{selected && displayedImageSrc === resolvedImageSrc && (

packages/editor/src/core/extensions/custom-image/components/node-view.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const CustomImageNodeView: React.FC<CustomImageNodeViewProps> = (props) =
2424
const { editor, extension, node } = props;
2525
const { src: imgNodeSrc } = node.attrs;
2626

27-
const [isUploaded, setIsUploaded] = useState(false);
27+
const [isUploaded, setIsUploaded] = useState(!!imgNodeSrc);
2828
const [resolvedSrc, setResolvedSrc] = useState<string | undefined>(undefined);
2929
const [resolvedDownloadSrc, setResolvedDownloadSrc] = useState<string | undefined>(undefined);
3030
const [imageFromFileSystem, setImageFromFileSystem] = useState<string | undefined>(undefined);
@@ -43,13 +43,13 @@ export const CustomImageNodeView: React.FC<CustomImageNodeViewProps> = (props) =
4343
// the image is already uploaded if the image-component node has src attribute
4444
// and we need to remove the blob from our file system
4545
useEffect(() => {
46-
if (resolvedSrc) {
46+
if (resolvedSrc || imgNodeSrc) {
4747
setIsUploaded(true);
4848
setImageFromFileSystem(undefined);
4949
} else {
5050
setIsUploaded(false);
5151
}
52-
}, [resolvedSrc]);
52+
}, [resolvedSrc, imgNodeSrc]);
5353

5454
useEffect(() => {
5555
if (!imgNodeSrc) {

0 commit comments

Comments
 (0)