[WEB-2730] chore: core/editor updates to support mobile editor (#5910)
* added editor changes w.r.t mobile-editor * added external extensions option * fix: type errors in image block * added on transaction method * fix: optional prop fixed * fix: memoize the extensions array * fix: added missing deps * fix: image component types * fix: remove range prop * fix: type fixes and better names of img src * fix: image load blinking * fix: code review * fix: props code review * fix: coderabbit review --------- Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
This commit is contained in:
parent
8ea34b5995
commit
3696062372
26 changed files with 164 additions and 61 deletions
|
|
@ -14,7 +14,8 @@ type Props = {
|
|||
};
|
||||
|
||||
export const DocumentEditorAdditionalExtensions = (_props: Props) => {
|
||||
const extensions: Extensions = [SlashCommands()];
|
||||
const { disabledExtensions } = _props;
|
||||
const extensions: Extensions = disabledExtensions?.includes("slash-commands") ? [] : [SlashCommands()];
|
||||
|
||||
return extensions;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { EditorRefApi, ICollaborativeDocumentEditor } from "@/types";
|
|||
|
||||
const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
|
||||
const {
|
||||
onTransaction,
|
||||
aiHandler,
|
||||
containerClassName,
|
||||
disabledExtensions,
|
||||
|
|
@ -43,6 +44,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
|
|||
|
||||
// use document editor
|
||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
|
||||
onTransaction,
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
embedHandler,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
|||
forwardedRef,
|
||||
mentionHandler,
|
||||
onChange,
|
||||
onTransaction,
|
||||
handleEditorReady,
|
||||
autofocus,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
value,
|
||||
|
|
@ -43,6 +46,9 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
|||
initialValue,
|
||||
mentionHandler,
|
||||
onChange,
|
||||
onTransaction,
|
||||
handleEditorReady,
|
||||
autofocus,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
value,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { forwardRef } from "react";
|
||||
import { forwardRef, useMemo } from "react";
|
||||
// components
|
||||
import { EditorWrapper } from "@/components/editors/editor-wrapper";
|
||||
// extensions
|
||||
|
|
@ -7,9 +7,15 @@ import { EnterKeyExtension } from "@/extensions";
|
|||
import { EditorRefApi, ILiteTextEditor } from "@/types";
|
||||
|
||||
const LiteTextEditor = (props: ILiteTextEditor) => {
|
||||
const { onEnterKeyPress } = props;
|
||||
const { onEnterKeyPress, disabledExtensions, extensions: externalExtensions = [] } = props;
|
||||
|
||||
const extensions = [EnterKeyExtension(onEnterKeyPress)];
|
||||
const extensions = useMemo(
|
||||
() => [
|
||||
...externalExtensions,
|
||||
...(disabledExtensions?.includes("enter-key") ? [] : [EnterKeyExtension(onEnterKeyPress)]),
|
||||
],
|
||||
[externalExtensions, disabledExtensions, onEnterKeyPress]
|
||||
);
|
||||
|
||||
return <EditorWrapper {...props} extensions={extensions} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,24 +8,31 @@ import { SideMenuExtension, SlashCommands } from "@/extensions";
|
|||
import { EditorRefApi, IRichTextEditor } from "@/types";
|
||||
|
||||
const RichTextEditor = (props: IRichTextEditor) => {
|
||||
const { dragDropEnabled } = props;
|
||||
const {
|
||||
disabledExtensions,
|
||||
dragDropEnabled,
|
||||
bubbleMenuEnabled = true,
|
||||
extensions: externalExtensions = [],
|
||||
} = props;
|
||||
|
||||
const getExtensions = useCallback(() => {
|
||||
const extensions = [SlashCommands()];
|
||||
|
||||
extensions.push(
|
||||
const extensions = [
|
||||
...externalExtensions,
|
||||
SideMenuExtension({
|
||||
aiEnabled: false,
|
||||
dragDropEnabled: !!dragDropEnabled,
|
||||
})
|
||||
);
|
||||
}),
|
||||
];
|
||||
if (!disabledExtensions?.includes("slash-commands")) {
|
||||
extensions.push(SlashCommands());
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}, [dragDropEnabled]);
|
||||
}, [dragDropEnabled, disabledExtensions, externalExtensions]);
|
||||
|
||||
return (
|
||||
<EditorWrapper {...props} extensions={getExtensions()}>
|
||||
{(editor) => <>{editor && <EditorBubbleMenu editor={editor} />}</>}
|
||||
{(editor) => <>{editor && bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}</>}
|
||||
</EditorWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ import {
|
|||
Heading6,
|
||||
CaseSensitive,
|
||||
LucideIcon,
|
||||
MinusSquare,
|
||||
Palette,
|
||||
} from "lucide-react";
|
||||
// helpers
|
||||
import {
|
||||
insertHorizontalRule,
|
||||
insertImage,
|
||||
insertTableCommand,
|
||||
setText,
|
||||
|
|
@ -208,6 +210,15 @@ export const ImageItem = (editor: Editor) =>
|
|||
icon: ImageIcon,
|
||||
}) as const;
|
||||
|
||||
export const HorizontalRuleItem = (editor: Editor) =>
|
||||
({
|
||||
key: "divider",
|
||||
name: "Divider",
|
||||
isActive: () => editor?.isActive("horizontalRule"),
|
||||
command: () => insertHorizontalRule(editor),
|
||||
icon: MinusSquare,
|
||||
}) as const;
|
||||
|
||||
export const TextColorItem = (editor: Editor): EditorMenuItem => ({
|
||||
key: "text-color",
|
||||
name: "Color",
|
||||
|
|
@ -246,6 +257,7 @@ export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem[] => {
|
|||
QuoteItem(editor),
|
||||
TableItem(editor),
|
||||
ImageItem(editor),
|
||||
HorizontalRuleItem(editor),
|
||||
TextColorItem(editor),
|
||||
BackgroundColorItem(editor),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from "react";
|
||||
import { NodeSelection } from "@tiptap/pm/state";
|
||||
// extensions
|
||||
import { CustomImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image";
|
||||
import { CustoBaseImageNodeViewProps, ImageToolbarRoot } from "@/extensions/custom-image";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ const ensurePixelString = <TDefault,>(value: Pixel | TDefault | number | undefin
|
|||
return value;
|
||||
};
|
||||
|
||||
type CustomImageBlockProps = CustomImageNodeViewProps & {
|
||||
type CustomImageBlockProps = CustoBaseImageNodeViewProps & {
|
||||
imageFromFileSystem: string;
|
||||
setFailedToLoadImage: (isError: boolean) => void;
|
||||
editorContainer: HTMLDivElement | null;
|
||||
|
|
@ -56,10 +56,10 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
getPos,
|
||||
editor,
|
||||
editorContainer,
|
||||
src: remoteImageSrc,
|
||||
src: resolvedImageSrc,
|
||||
setEditorContainer,
|
||||
} = props;
|
||||
const { width: nodeWidth, height: nodeHeight, aspectRatio: nodeAspectRatio } = node.attrs;
|
||||
const { width: nodeWidth, height: nodeHeight, aspectRatio: nodeAspectRatio, src: imgNodeSrc } = node.attrs;
|
||||
// states
|
||||
const [size, setSize] = useState<Size>({
|
||||
width: ensurePixelString(nodeWidth, "35%"),
|
||||
|
|
@ -210,13 +210,13 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
|
||||
// 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)
|
||||
// if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete
|
||||
const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete || hasErroredOnFirstLoad;
|
||||
const showImageLoader = !(resolvedImageSrc || imageFromFileSystem) || !initialResizeComplete || hasErroredOnFirstLoad;
|
||||
// show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
|
||||
const showImageUtils = remoteImageSrc && initialResizeComplete;
|
||||
const showImageUtils = resolvedImageSrc && initialResizeComplete;
|
||||
// show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem)
|
||||
const showImageResizer = editor.isEditable && remoteImageSrc && initialResizeComplete;
|
||||
const showImageResizer = editor.isEditable && resolvedImageSrc && initialResizeComplete;
|
||||
// show the preview image from the file system if the remote image's src is not set
|
||||
const displayedImageSrc = remoteImageSrc || imageFromFileSystem;
|
||||
const displayedImageSrc = resolvedImageSrc || imageFromFileSystem;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -248,8 +248,8 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
try {
|
||||
setHasErroredOnFirstLoad(true);
|
||||
// this is a type error from tiptap, don't remove await until it's fixed
|
||||
await editor?.commands.restoreImage?.(node.attrs.src);
|
||||
imageRef.current.src = remoteImageSrc;
|
||||
await editor?.commands.restoreImage?.(imgNodeSrc);
|
||||
imageRef.current.src = resolvedImageSrc;
|
||||
} catch {
|
||||
// if the image failed to even restore, then show the error state
|
||||
setFailedToLoadImage(true);
|
||||
|
|
@ -264,7 +264,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
// hide the image while the background calculations of the image loader are in progress (to avoid flickering) and show the loader until then
|
||||
hidden: showImageLoader,
|
||||
"read-only-image": !editor.isEditable,
|
||||
"blur-sm opacity-80 loading-image": !remoteImageSrc,
|
||||
"blur-sm opacity-80 loading-image": !resolvedImageSrc,
|
||||
})}
|
||||
style={{
|
||||
width: size.width,
|
||||
|
|
@ -277,14 +277,14 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
|||
"absolute top-1 right-1 z-20 bg-black/40 rounded opacity-0 pointer-events-none group-hover/image-component:opacity-100 group-hover/image-component:pointer-events-auto transition-opacity"
|
||||
}
|
||||
image={{
|
||||
src: remoteImageSrc,
|
||||
src: resolvedImageSrc,
|
||||
aspectRatio: size.aspectRatio,
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{selected && displayedImageSrc === remoteImageSrc && (
|
||||
{selected && displayedImageSrc === resolvedImageSrc && (
|
||||
<div className="absolute inset-0 size-full bg-custom-primary-500/30" />
|
||||
)}
|
||||
{showImageResizer && (
|
||||
|
|
|
|||
|
|
@ -3,23 +3,24 @@ import { Editor, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
|||
// extensions
|
||||
import { CustomImageBlock, CustomImageUploader, ImageAttributes } from "@/extensions/custom-image";
|
||||
|
||||
export type CustomImageComponentProps = {
|
||||
export type CustoBaseImageNodeViewProps = {
|
||||
getPos: () => number;
|
||||
editor: Editor;
|
||||
node: NodeViewProps["node"] & {
|
||||
attrs: ImageAttributes;
|
||||
};
|
||||
updateAttributes: (attrs: ImageAttributes) => void;
|
||||
updateAttributes: (attrs: Partial<ImageAttributes>) => void;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
export type CustomImageNodeViewProps = NodeViewProps & CustomImageComponentProps;
|
||||
export type CustomImageNodeProps = NodeViewProps & CustoBaseImageNodeViewProps;
|
||||
|
||||
export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
||||
export const CustomImageNode = (props: CustomImageNodeProps) => {
|
||||
const { getPos, editor, node, updateAttributes, selected } = props;
|
||||
const { src: remoteImageSrc } = node.attrs;
|
||||
const { src: imgNodeSrc } = node.attrs;
|
||||
|
||||
const [isUploaded, setIsUploaded] = useState(false);
|
||||
const [resolvedSrc, setResolvedSrc] = useState<string | undefined>(undefined);
|
||||
const [imageFromFileSystem, setImageFromFileSystem] = useState<string | undefined>(undefined);
|
||||
const [failedToLoadImage, setFailedToLoadImage] = useState(false);
|
||||
|
||||
|
|
@ -39,13 +40,22 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
|||
// 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 (remoteImageSrc) {
|
||||
if (resolvedSrc) {
|
||||
setIsUploaded(true);
|
||||
setImageFromFileSystem(undefined);
|
||||
} else {
|
||||
setIsUploaded(false);
|
||||
}
|
||||
}, [remoteImageSrc]);
|
||||
}, [resolvedSrc]);
|
||||
|
||||
useEffect(() => {
|
||||
const getImageSource = async () => {
|
||||
// @ts-expect-error function not expected here, but will still work and don't remove await
|
||||
const url: string = await editor?.commands?.getImageSource?.(imgNodeSrc);
|
||||
setResolvedSrc(url as string);
|
||||
};
|
||||
getImageSource();
|
||||
}, [imageFromFileSystem, node.attrs.src]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
|
|
@ -55,8 +65,7 @@ export const CustomImageNode = (props: CustomImageNodeViewProps) => {
|
|||
imageFromFileSystem={imageFromFileSystem}
|
||||
editorContainer={editorContainer}
|
||||
editor={editor}
|
||||
// @ts-expect-error function not expected here, but will still work
|
||||
src={editor?.commands?.getImageSource?.(remoteImageSrc)}
|
||||
src={resolvedSrc}
|
||||
getPos={getPos}
|
||||
node={node}
|
||||
setEditorContainer={setEditorContainer}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { cn } from "@/helpers/common";
|
|||
// hooks
|
||||
import { useUploader, useDropZone, uploadFirstImageAndInsertRemaining } from "@/hooks/use-file-upload";
|
||||
// extensions
|
||||
import { type CustomImageComponentProps, getImageComponentImageFileMap } from "@/extensions/custom-image";
|
||||
import { CustoBaseImageNodeViewProps, getImageComponentImageFileMap } from "@/extensions/custom-image";
|
||||
|
||||
type CustomImageUploaderProps = CustomImageComponentProps & {
|
||||
type CustomImageUploaderProps = CustoBaseImageNodeViewProps & {
|
||||
maxFileSize: number;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
failedToLoadImage: boolean;
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ declare module "@tiptap/core" {
|
|||
imageComponent: {
|
||||
insertImageComponent: ({ file, pos, event }: InsertImageComponentProps) => ReturnType;
|
||||
uploadImage: (file: File) => () => Promise<string> | undefined;
|
||||
getImageSource?: (path: string) => () => Promise<string>;
|
||||
restoreImage: (src: string) => () => Promise<void>;
|
||||
getImageSource?: (path: string) => () => string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -193,10 +193,10 @@ export const CustomImageExtension = (props: TFileHandler) => {
|
|||
const fileUrl = await upload(file);
|
||||
return fileUrl;
|
||||
},
|
||||
getImageSource: (path: string) => async () => await getAssetSrc(path),
|
||||
restoreImage: (src: string) => async () => {
|
||||
await restoreImageFn(src);
|
||||
},
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export const CustomReadOnlyImageExtension = (props: Pick<TFileHandler, "getAsset
|
|||
|
||||
addCommands() {
|
||||
return {
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
getImageSource: (path: string) => async () => await getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export const ImageExtension = (fileHandler: TFileHandler) => {
|
|||
|
||||
addCommands() {
|
||||
return {
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
getImageSource: (path: string) => async () => await getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const ReadOnlyImageExtension = (props: Pick<TFileHandler, "getAssetSrc">)
|
|||
|
||||
addCommands() {
|
||||
return {
|
||||
getImageSource: (path: string) => () => getAssetSrc(path),
|
||||
getImageSource: (path: string) => async () => await getAssetSrc(path),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -18,16 +18,22 @@ export const MentionNodeView = (props) => {
|
|||
useEffect(() => {
|
||||
if (!props.extension.options.mentionHighlights) return;
|
||||
const hightlights = async () => {
|
||||
const userId = await props.extension.options.mentionHighlights();
|
||||
const userId = await props.extension.options.mentionHighlights?.();
|
||||
setHighlightsState(userId);
|
||||
};
|
||||
hightlights();
|
||||
}, [props.extension.options]);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (!props.node.attrs.redirect_uri) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="mention-component inline w-fit">
|
||||
<a
|
||||
href={props.node.attrs.redirect_uri}
|
||||
href={props.node.attrs.redirect_uri || "#"}
|
||||
target="_blank"
|
||||
className={cn("mention rounded bg-custom-primary-100/20 px-1 py-0.5 font-medium text-custom-primary-100", {
|
||||
"bg-yellow-500/20 text-yellow-500": highlightsState
|
||||
|
|
|
|||
|
|
@ -181,6 +181,10 @@ export const toggleBackgroundColor = (color: string | undefined, editor: Editor,
|
|||
}
|
||||
};
|
||||
|
||||
export const insertHorizontalRule = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||
else editor.chain().focus().setHorizontalRule().run();
|
||||
}
|
||||
export const insertCallout = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).insertCallout().run();
|
||||
else editor.chain().focus().insertCallout().run();
|
||||
|
|
|
|||
|
|
@ -32,6 +32,26 @@ function scrollToNode(editor: Editor, pos: number): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function scrollToNodeViaDOMCoordinates(editor: Editor, pos: number, behavior?: ScrollBehavior): void {
|
||||
const view = editor.view;
|
||||
|
||||
// Get the coordinates of the position
|
||||
const coords = view.coordsAtPos(pos);
|
||||
|
||||
if (coords) {
|
||||
// Scroll to the coordinates
|
||||
window.scrollTo({
|
||||
top: coords.top + window.scrollY - window.innerHeight / 2,
|
||||
behavior: behavior,
|
||||
});
|
||||
|
||||
// Optionally, you can also focus the editor
|
||||
view.focus();
|
||||
} else {
|
||||
console.warn("Unable to find coordinates for the given position");
|
||||
}
|
||||
}
|
||||
|
||||
export function scrollSummary(editor: Editor, marking: IMarking) {
|
||||
if (editor) {
|
||||
const pos = findNthH1(editor, marking.sequence, marking.level);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { TCollaborativeEditorProps } from "@/types";
|
|||
|
||||
export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
||||
const {
|
||||
onTransaction,
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
editorProps = {},
|
||||
|
|
@ -75,6 +76,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
|||
|
||||
const editor = useEditor({
|
||||
id,
|
||||
onTransaction,
|
||||
editorProps,
|
||||
editorClassName,
|
||||
enableHistory: false,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { CoreEditorExtensions } from "@/extensions";
|
|||
// helpers
|
||||
import { getParagraphCount } from "@/helpers/common";
|
||||
import { insertContentAtSavedSelection } from "@/helpers/insert-content-at-cursor-position";
|
||||
import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
|
||||
import { IMarking, scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node";
|
||||
// props
|
||||
import { CoreEditorProps } from "@/props";
|
||||
// types
|
||||
|
|
@ -33,6 +33,8 @@ export interface CustomEditorProps {
|
|||
suggestions?: () => Promise<IMentionSuggestion[]>;
|
||||
};
|
||||
onChange?: (json: object, html: string) => void;
|
||||
onTransaction?: () => void;
|
||||
autofocus?: boolean;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
provider?: HocuspocusProvider;
|
||||
tabIndex?: number;
|
||||
|
|
@ -54,10 +56,12 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
initialValue,
|
||||
mentionHandler,
|
||||
onChange,
|
||||
onTransaction,
|
||||
placeholder,
|
||||
provider,
|
||||
tabIndex,
|
||||
value,
|
||||
autofocus = false,
|
||||
} = props;
|
||||
// states
|
||||
|
||||
|
|
@ -66,6 +70,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
const savedSelectionRef = useRef(savedSelection);
|
||||
const editor = useTiptapEditor({
|
||||
autofocus,
|
||||
editorProps: {
|
||||
...CoreEditorProps({
|
||||
editorClassName,
|
||||
|
|
@ -87,7 +92,10 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
],
|
||||
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
||||
onCreate: () => handleEditorReady?.(true),
|
||||
onTransaction: ({ editor }) => setSavedSelection(editor.state.selection),
|
||||
onTransaction: ({ editor }) => {
|
||||
setSavedSelection(editor.state.selection);
|
||||
onTransaction?.();
|
||||
},
|
||||
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
|
||||
onDestroy: () => handleEditorReady?.(false),
|
||||
});
|
||||
|
|
@ -120,6 +128,13 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
useImperativeHandle(
|
||||
forwardedRef,
|
||||
() => ({
|
||||
blur: () => editorRef.current?.commands.blur(),
|
||||
scrollToNodeViaDOMCoordinates(behavior?: ScrollBehavior, pos?: number) {
|
||||
const resolvedPos = pos ?? savedSelection?.from;
|
||||
if (!editorRef.current || !resolvedPos) return;
|
||||
scrollToNodeViaDOMCoordinates(editorRef.current, resolvedPos, behavior);
|
||||
},
|
||||
getCurrentCursorPosition: () => savedSelection?.from,
|
||||
clearEditor: (emitUpdate = false) => {
|
||||
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type TCollaborativeEditorHookProps = {
|
|||
};
|
||||
|
||||
export type TCollaborativeEditorProps = TCollaborativeEditorHookProps & {
|
||||
onTransaction?: () => void;
|
||||
embedHandler?: TEmbedConfig;
|
||||
fileHandler: TFileHandler;
|
||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { DeleteImage, RestoreImage, UploadImage } from "@/types";
|
||||
|
||||
export type TFileHandler = {
|
||||
getAssetSrc: (path: string) => string;
|
||||
getAssetSrc: (path: string) => Promise<string>;
|
||||
cancel: () => void;
|
||||
delete: DeleteImage;
|
||||
upload: UploadImage;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ export type EditorReadOnlyRefApi = {
|
|||
};
|
||||
|
||||
export interface EditorRefApi extends EditorReadOnlyRefApi {
|
||||
blur: () => void;
|
||||
scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void;
|
||||
getCurrentCursorPosition: () => number | undefined;
|
||||
setEditorValueAtCursorPosition: (content: string) => void;
|
||||
executeMenuItemCommand: (
|
||||
props:
|
||||
|
|
@ -68,6 +71,7 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
|
|||
export interface IEditorProps {
|
||||
containerClassName?: string;
|
||||
displayConfig?: TDisplayConfig;
|
||||
disabledExtensions?: TExtensions[];
|
||||
editorClassName?: string;
|
||||
fileHandler: TFileHandler;
|
||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
|
|
@ -78,22 +82,26 @@ export interface IEditorProps {
|
|||
suggestions?: () => Promise<IMentionSuggestion[]>;
|
||||
};
|
||||
onChange?: (json: object, html: string) => void;
|
||||
onTransaction?: () => void;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
autofocus?: boolean;
|
||||
onEnterKeyPress?: (e?: any) => void;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
tabIndex?: number;
|
||||
value?: string | null;
|
||||
value?: string | null;
|
||||
}
|
||||
export interface ILiteTextEditor extends IEditorProps {
|
||||
extensions?: any[];
|
||||
}
|
||||
|
||||
export type ILiteTextEditor = IEditorProps;
|
||||
|
||||
export interface IRichTextEditor extends IEditorProps {
|
||||
extensions?: any[];
|
||||
bubbleMenuEnabled?: boolean;
|
||||
dragDropEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ICollaborativeDocumentEditor
|
||||
extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> {
|
||||
aiHandler?: TAIHandler;
|
||||
disabledExtensions: TExtensions[];
|
||||
embedHandler: TEmbedConfig;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed";
|
||||
export type TExtensions = "ai" | "collaboration-cursor" | "issue-embed" | "slash-commands"| "enter-key";
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// styles
|
||||
// import "./styles/tailwind.css";
|
||||
import "src/styles/variables.css";
|
||||
import "src/styles/editor.css";
|
||||
import "src/styles/table.css";
|
||||
import "src/styles/github-dark.css";
|
||||
import "src/styles/drag-drop.css";
|
||||
import "./styles/variables.css";
|
||||
import "./styles/editor.css";
|
||||
import "./styles/table.css";
|
||||
import "./styles/github-dark.css";
|
||||
import "./styles/drag-drop.css";
|
||||
|
||||
// editors
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.ProseMirror {
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
|||
const { anchor, uploadFile, workspaceId } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
getAssetSrc: async (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
|
|
@ -70,7 +70,7 @@ export const getReadOnlyEditorFileHandlers = (
|
|||
const { anchor } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
getAssetSrc: async (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
|||
const { maxFileSize, projectId, uploadFile, workspaceId, workspaceSlug } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
getAssetSrc: async (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
|
|
@ -94,7 +94,7 @@ export const getReadOnlyEditorFileHandlers = (
|
|||
const { projectId, workspaceSlug } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: (path) => {
|
||||
getAssetSrc: async (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue