[WIKI-412] chore: improved rich text editor extensions handling (#7158)
* chore: code split for rich text editor extensions * chore: update type * chore: add missing prop
This commit is contained in:
parent
b2ccca0567
commit
2792d48288
12 changed files with 159 additions and 38 deletions
|
|
@ -2,30 +2,31 @@ import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||||
import { AnyExtension } from "@tiptap/core";
|
import { AnyExtension } from "@tiptap/core";
|
||||||
import { SlashCommands } from "@/extensions";
|
import { SlashCommands } from "@/extensions";
|
||||||
// plane editor types
|
// plane editor types
|
||||||
import { TIssueEmbedConfig } from "@/plane-editor/types";
|
import { TEmbedConfig } from "@/plane-editor/types";
|
||||||
// types
|
// types
|
||||||
import { TExtensions, TUserDetails } from "@/types";
|
import { TExtensions, TFileHandler, TUserDetails } from "@/types";
|
||||||
|
|
||||||
type Props = {
|
export type TDocumentEditorAdditionalExtensionsProps = {
|
||||||
disabledExtensions?: TExtensions[];
|
disabledExtensions: TExtensions[];
|
||||||
issueEmbedConfig: TIssueEmbedConfig | undefined;
|
embedConfig: TEmbedConfig | undefined;
|
||||||
provider: HocuspocusProvider;
|
fileHandler: TFileHandler;
|
||||||
|
provider?: HocuspocusProvider;
|
||||||
userDetails: TUserDetails;
|
userDetails: TUserDetails;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ExtensionConfig = {
|
export type TDocumentEditorAdditionalExtensionsRegistry = {
|
||||||
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||||
getExtension: (props: Props) => AnyExtension;
|
getExtension: (props: TDocumentEditorAdditionalExtensionsProps) => AnyExtension;
|
||||||
};
|
};
|
||||||
|
|
||||||
const extensionRegistry: ExtensionConfig[] = [
|
const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [
|
||||||
{
|
{
|
||||||
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
|
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
|
||||||
getExtension: () => SlashCommands({}),
|
getExtension: ({ disabledExtensions }) => SlashCommands({ disabledExtensions }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DocumentEditorAdditionalExtensions = (_props: Props) => {
|
export const DocumentEditorAdditionalExtensions = (_props: TDocumentEditorAdditionalExtensionsProps) => {
|
||||||
const { disabledExtensions = [] } = _props;
|
const { disabledExtensions = [] } = _props;
|
||||||
|
|
||||||
const documentExtensions = extensionRegistry
|
const documentExtensions = extensionRegistry
|
||||||
|
|
|
||||||
41
packages/editor/src/ce/extensions/rich-text/extensions.tsx
Normal file
41
packages/editor/src/ce/extensions/rich-text/extensions.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { AnyExtension, Extensions } from "@tiptap/core";
|
||||||
|
// extensions
|
||||||
|
import { SlashCommands } from "@/extensions/slash-commands/root";
|
||||||
|
// types
|
||||||
|
import { TExtensions, TFileHandler } from "@/types";
|
||||||
|
|
||||||
|
export type TRichTextEditorAdditionalExtensionsProps = {
|
||||||
|
disabledExtensions: TExtensions[];
|
||||||
|
fileHandler: TFileHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry entry configuration for extensions
|
||||||
|
*/
|
||||||
|
export type TRichTextEditorAdditionalExtensionsRegistry = {
|
||||||
|
/** Determines if the extension should be enabled based on disabled extensions */
|
||||||
|
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||||
|
/** Returns the extension instance(s) when enabled */
|
||||||
|
getExtension: (props: TRichTextEditorAdditionalExtensionsProps) => AnyExtension | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extensionRegistry: TRichTextEditorAdditionalExtensionsRegistry[] = [
|
||||||
|
{
|
||||||
|
isEnabled: (disabledExtensions) => !disabledExtensions.includes("slash-commands"),
|
||||||
|
getExtension: ({ disabledExtensions }) =>
|
||||||
|
SlashCommands({
|
||||||
|
disabledExtensions,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RichTextEditorAdditionalExtensions = (props: TRichTextEditorAdditionalExtensionsProps) => {
|
||||||
|
const { disabledExtensions } = props;
|
||||||
|
|
||||||
|
const extensions: Extensions = extensionRegistry
|
||||||
|
.filter((config) => config.isEnabled(disabledExtensions))
|
||||||
|
.map((config) => config.getExtension(props))
|
||||||
|
.filter((extension): extension is AnyExtension => extension !== undefined);
|
||||||
|
|
||||||
|
return extensions;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { AnyExtension, Extensions } from "@tiptap/core";
|
||||||
|
// types
|
||||||
|
import { TExtensions, TReadOnlyFileHandler } from "@/types";
|
||||||
|
|
||||||
|
export type TRichTextReadOnlyEditorAdditionalExtensionsProps = {
|
||||||
|
disabledExtensions: TExtensions[];
|
||||||
|
fileHandler: TReadOnlyFileHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry entry configuration for extensions
|
||||||
|
*/
|
||||||
|
export type TRichTextReadOnlyEditorAdditionalExtensionsRegistry = {
|
||||||
|
/** Determines if the extension should be enabled based on disabled extensions */
|
||||||
|
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||||
|
/** Returns the extension instance(s) when enabled */
|
||||||
|
getExtension: (props: TRichTextReadOnlyEditorAdditionalExtensionsProps) => AnyExtension | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extensionRegistry: TRichTextReadOnlyEditorAdditionalExtensionsRegistry[] = [];
|
||||||
|
|
||||||
|
export const RichTextReadOnlyEditorAdditionalExtensions = (props: TRichTextReadOnlyEditorAdditionalExtensionsProps) => {
|
||||||
|
const { disabledExtensions } = props;
|
||||||
|
|
||||||
|
const extensions: Extensions = extensionRegistry
|
||||||
|
.filter((config) => config.isEnabled(disabledExtensions))
|
||||||
|
.map((config) => config.getExtension(props))
|
||||||
|
.filter((extension): extension is AnyExtension => extension !== undefined);
|
||||||
|
|
||||||
|
return extensions;
|
||||||
|
};
|
||||||
|
|
@ -15,6 +15,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||||
disabledExtensions,
|
disabledExtensions,
|
||||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||||
editorClassName = "",
|
editorClassName = "",
|
||||||
|
extensions,
|
||||||
fileHandler,
|
fileHandler,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
id,
|
id,
|
||||||
|
|
@ -25,6 +26,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||||
const editor = useReadOnlyEditor({
|
const editor = useReadOnlyEditor({
|
||||||
disabledExtensions,
|
disabledExtensions,
|
||||||
editorClassName,
|
editorClassName,
|
||||||
|
extensions,
|
||||||
fileHandler,
|
fileHandler,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
initialValue,
|
initialValue,
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,20 @@ import { forwardRef, useCallback } from "react";
|
||||||
import { EditorWrapper } from "@/components/editors";
|
import { EditorWrapper } from "@/components/editors";
|
||||||
import { EditorBubbleMenu } from "@/components/menus";
|
import { EditorBubbleMenu } from "@/components/menus";
|
||||||
// extensions
|
// extensions
|
||||||
import { SideMenuExtension, SlashCommands } from "@/extensions";
|
import { SideMenuExtension } from "@/extensions";
|
||||||
|
// plane editor imports
|
||||||
|
import { RichTextEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/extensions";
|
||||||
// types
|
// types
|
||||||
import { EditorRefApi, IRichTextEditor } from "@/types";
|
import { EditorRefApi, IRichTextEditor } from "@/types";
|
||||||
|
|
||||||
const RichTextEditor = (props: IRichTextEditor) => {
|
const RichTextEditor = (props: IRichTextEditor) => {
|
||||||
const { disabledExtensions, dragDropEnabled, bubbleMenuEnabled = true, extensions: externalExtensions = [] } = props;
|
const {
|
||||||
|
disabledExtensions,
|
||||||
|
dragDropEnabled,
|
||||||
|
fileHandler,
|
||||||
|
bubbleMenuEnabled = true,
|
||||||
|
extensions: externalExtensions = [],
|
||||||
|
} = props;
|
||||||
|
|
||||||
const getExtensions = useCallback(() => {
|
const getExtensions = useCallback(() => {
|
||||||
const extensions = [
|
const extensions = [
|
||||||
|
|
@ -17,17 +25,14 @@ const RichTextEditor = (props: IRichTextEditor) => {
|
||||||
aiEnabled: false,
|
aiEnabled: false,
|
||||||
dragDropEnabled: !!dragDropEnabled,
|
dragDropEnabled: !!dragDropEnabled,
|
||||||
}),
|
}),
|
||||||
];
|
...RichTextEditorAdditionalExtensions({
|
||||||
if (!disabledExtensions?.includes("slash-commands")) {
|
|
||||||
extensions.push(
|
|
||||||
SlashCommands({
|
|
||||||
disabledExtensions,
|
disabledExtensions,
|
||||||
})
|
fileHandler,
|
||||||
);
|
}),
|
||||||
}
|
];
|
||||||
|
|
||||||
return extensions;
|
return extensions;
|
||||||
}, [dragDropEnabled, disabledExtensions, externalExtensions]);
|
}, [dragDropEnabled, disabledExtensions, externalExtensions, fileHandler]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorWrapper {...props} extensions={getExtensions()}>
|
<EditorWrapper {...props} extensions={getExtensions()}>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,33 @@
|
||||||
import { forwardRef } from "react";
|
import { forwardRef, useCallback } from "react";
|
||||||
|
// plane editor extensions
|
||||||
|
import { RichTextReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/read-only-extensions";
|
||||||
// types
|
// types
|
||||||
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor } from "@/types";
|
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor } from "@/types";
|
||||||
|
// local imports
|
||||||
import { ReadOnlyEditorWrapper } from "../read-only-editor-wrapper";
|
import { ReadOnlyEditorWrapper } from "../read-only-editor-wrapper";
|
||||||
|
|
||||||
const RichTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => (
|
const RichTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => {
|
||||||
<ReadOnlyEditorWrapper {...props} forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>} />
|
const { disabledExtensions, fileHandler } = props;
|
||||||
));
|
|
||||||
|
const getExtensions = useCallback(() => {
|
||||||
|
const extensions = [
|
||||||
|
...RichTextReadOnlyEditorAdditionalExtensions({
|
||||||
|
disabledExtensions,
|
||||||
|
fileHandler,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return extensions;
|
||||||
|
}, [disabledExtensions, fileHandler]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReadOnlyEditorWrapper
|
||||||
|
{...props}
|
||||||
|
extensions={getExtensions()}
|
||||||
|
forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
RichTextReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef";
|
RichTextReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -170,8 +170,9 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||||
CustomTextAlignExtension,
|
CustomTextAlignExtension,
|
||||||
CustomCalloutExtension,
|
CustomCalloutExtension,
|
||||||
UtilityExtension({
|
UtilityExtension({
|
||||||
isEditable: editable,
|
disabledExtensions,
|
||||||
fileHandler,
|
fileHandler,
|
||||||
|
isEditable: editable,
|
||||||
}),
|
}),
|
||||||
CustomColorExtension,
|
CustomColorExtension,
|
||||||
...CoreEditorAdditionalExtensions({
|
...CoreEditorAdditionalExtensions({
|
||||||
|
|
|
||||||
|
|
@ -127,8 +127,9 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||||
CustomTextAlignExtension,
|
CustomTextAlignExtension,
|
||||||
CustomCalloutReadOnlyExtension,
|
CustomCalloutReadOnlyExtension,
|
||||||
UtilityExtension({
|
UtilityExtension({
|
||||||
isEditable: false,
|
disabledExtensions,
|
||||||
fileHandler,
|
fileHandler,
|
||||||
|
isEditable: false,
|
||||||
}),
|
}),
|
||||||
...CoreReadOnlyEditorAdditionalExtensions({
|
...CoreReadOnlyEditorAdditionalExtensions({
|
||||||
disabledExtensions,
|
disabledExtensions,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { DropHandlerPlugin } from "@/plugins/drop";
|
||||||
import { FilePlugins } from "@/plugins/file/root";
|
import { FilePlugins } from "@/plugins/file/root";
|
||||||
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
|
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
|
||||||
// types
|
// types
|
||||||
import { TFileHandler, TReadOnlyFileHandler } from "@/types";
|
import { TExtensions, TFileHandler, TReadOnlyFileHandler } from "@/types";
|
||||||
|
|
||||||
declare module "@tiptap/core" {
|
declare module "@tiptap/core" {
|
||||||
interface Commands {
|
interface Commands {
|
||||||
|
|
@ -24,13 +24,14 @@ export interface UtilityExtensionStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
disabledExtensions: TExtensions[];
|
||||||
fileHandler: TFileHandler | TReadOnlyFileHandler;
|
fileHandler: TFileHandler | TReadOnlyFileHandler;
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UtilityExtension = (props: Props) => {
|
export const UtilityExtension = (props: Props) => {
|
||||||
const { fileHandler, isEditable } = props;
|
const { disabledExtensions, fileHandler, isEditable } = props;
|
||||||
const { restore: restoreImageFn } = fileHandler;
|
const { restore } = fileHandler;
|
||||||
|
|
||||||
return Extension.create<Record<string, unknown>, UtilityExtensionStorage>({
|
return Extension.create<Record<string, unknown>, UtilityExtensionStorage>({
|
||||||
name: "utility",
|
name: "utility",
|
||||||
|
|
@ -45,12 +46,15 @@ export const UtilityExtension = (props: Props) => {
|
||||||
}),
|
}),
|
||||||
...codemark({ markType: this.editor.schema.marks.code }),
|
...codemark({ markType: this.editor.schema.marks.code }),
|
||||||
MarkdownClipboardPlugin(this.editor),
|
MarkdownClipboardPlugin(this.editor),
|
||||||
DropHandlerPlugin(this.editor),
|
DropHandlerPlugin({
|
||||||
|
disabledExtensions,
|
||||||
|
editor: this.editor,
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
onCreate() {
|
onCreate() {
|
||||||
restorePublicImages(this.editor, restoreImageFn);
|
restorePublicImages(this.editor, restore);
|
||||||
},
|
},
|
||||||
|
|
||||||
addStorage() {
|
addStorage() {
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,8 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
||||||
...(extensions ?? []),
|
...(extensions ?? []),
|
||||||
...DocumentEditorAdditionalExtensions({
|
...DocumentEditorAdditionalExtensions({
|
||||||
disabledExtensions,
|
disabledExtensions,
|
||||||
issueEmbedConfig: embedHandler?.issue,
|
embedConfig: embedHandler,
|
||||||
|
fileHandler,
|
||||||
provider,
|
provider,
|
||||||
userDetails: user,
|
userDetails: user,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,17 @@ import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
// constants
|
// constants
|
||||||
import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
|
import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
|
||||||
// types
|
// types
|
||||||
import { TEditorCommands } from "@/types";
|
import { TEditorCommands, TExtensions } from "@/types";
|
||||||
|
|
||||||
export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
type Props = {
|
||||||
new Plugin({
|
disabledExtensions: TExtensions[];
|
||||||
|
editor: Editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DropHandlerPlugin = (props: Props): Plugin => {
|
||||||
|
const { disabledExtensions, editor } = props;
|
||||||
|
|
||||||
|
return new Plugin({
|
||||||
key: new PluginKey("drop-handler-plugin"),
|
key: new PluginKey("drop-handler-plugin"),
|
||||||
props: {
|
props: {
|
||||||
handlePaste: (view, event) => {
|
handlePaste: (view, event) => {
|
||||||
|
|
@ -25,6 +32,7 @@ export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
||||||
if (acceptedFiles.length) {
|
if (acceptedFiles.length) {
|
||||||
const pos = view.state.selection.from;
|
const pos = view.state.selection.from;
|
||||||
insertFilesSafely({
|
insertFilesSafely({
|
||||||
|
disabledExtensions,
|
||||||
editor,
|
editor,
|
||||||
files: acceptedFiles,
|
files: acceptedFiles,
|
||||||
initialPos: pos,
|
initialPos: pos,
|
||||||
|
|
@ -58,6 +66,7 @@ export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
||||||
if (coordinates) {
|
if (coordinates) {
|
||||||
const pos = coordinates.pos;
|
const pos = coordinates.pos;
|
||||||
insertFilesSafely({
|
insertFilesSafely({
|
||||||
|
disabledExtensions,
|
||||||
editor,
|
editor,
|
||||||
files: acceptedFiles,
|
files: acceptedFiles,
|
||||||
initialPos: pos,
|
initialPos: pos,
|
||||||
|
|
@ -71,8 +80,10 @@ export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
type InsertFilesSafelyArgs = {
|
type InsertFilesSafelyArgs = {
|
||||||
|
disabledExtensions: TExtensions[];
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
event: "insert" | "drop";
|
event: "insert" | "drop";
|
||||||
files: File[];
|
files: File[];
|
||||||
|
|
@ -81,7 +92,7 @@ type InsertFilesSafelyArgs = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => {
|
export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => {
|
||||||
const { editor, event, files, initialPos, type } = args;
|
const { disabledExtensions, editor, event, files, initialPos, type } = args;
|
||||||
let pos = initialPos;
|
let pos = initialPos;
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
|
@ -100,7 +111,7 @@ export const insertFilesSafely = async (args: InsertFilesSafelyArgs) => {
|
||||||
else if (ACCEPTED_ATTACHMENT_MIME_TYPES.includes(file.type)) fileType = "attachment";
|
else if (ACCEPTED_ATTACHMENT_MIME_TYPES.includes(file.type)) fileType = "attachment";
|
||||||
}
|
}
|
||||||
// insert file depending on the type at the current position
|
// insert file depending on the type at the current position
|
||||||
if (fileType === "image") {
|
if (fileType === "image" && !disabledExtensions.includes("image")) {
|
||||||
editor.commands.insertImageComponent({
|
editor.commands.insertImageComponent({
|
||||||
file,
|
file,
|
||||||
pos,
|
pos,
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@ export interface IReadOnlyEditorProps {
|
||||||
disabledExtensions: TExtensions[];
|
disabledExtensions: TExtensions[];
|
||||||
displayConfig?: TDisplayConfig;
|
displayConfig?: TDisplayConfig;
|
||||||
editorClassName?: string;
|
editorClassName?: string;
|
||||||
|
extensions?: Extensions;
|
||||||
fileHandler: TReadOnlyFileHandler;
|
fileHandler: TReadOnlyFileHandler;
|
||||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue