[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 { SlashCommands } from "@/extensions";
|
||||
// plane editor types
|
||||
import { TIssueEmbedConfig } from "@/plane-editor/types";
|
||||
import { TEmbedConfig } from "@/plane-editor/types";
|
||||
// types
|
||||
import { TExtensions, TUserDetails } from "@/types";
|
||||
import { TExtensions, TFileHandler, TUserDetails } from "@/types";
|
||||
|
||||
type Props = {
|
||||
disabledExtensions?: TExtensions[];
|
||||
issueEmbedConfig: TIssueEmbedConfig | undefined;
|
||||
provider: HocuspocusProvider;
|
||||
export type TDocumentEditorAdditionalExtensionsProps = {
|
||||
disabledExtensions: TExtensions[];
|
||||
embedConfig: TEmbedConfig | undefined;
|
||||
fileHandler: TFileHandler;
|
||||
provider?: HocuspocusProvider;
|
||||
userDetails: TUserDetails;
|
||||
};
|
||||
|
||||
type ExtensionConfig = {
|
||||
export type TDocumentEditorAdditionalExtensionsRegistry = {
|
||||
isEnabled: (disabledExtensions: TExtensions[]) => boolean;
|
||||
getExtension: (props: Props) => AnyExtension;
|
||||
getExtension: (props: TDocumentEditorAdditionalExtensionsProps) => AnyExtension;
|
||||
};
|
||||
|
||||
const extensionRegistry: ExtensionConfig[] = [
|
||||
const extensionRegistry: TDocumentEditorAdditionalExtensionsRegistry[] = [
|
||||
{
|
||||
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 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,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editorClassName = "",
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
id,
|
||||
|
|
@ -25,6 +26,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
|||
const editor = useReadOnlyEditor({
|
||||
disabledExtensions,
|
||||
editorClassName,
|
||||
extensions,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
initialValue,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,20 @@ import { forwardRef, useCallback } from "react";
|
|||
import { EditorWrapper } from "@/components/editors";
|
||||
import { EditorBubbleMenu } from "@/components/menus";
|
||||
// extensions
|
||||
import { SideMenuExtension, SlashCommands } from "@/extensions";
|
||||
import { SideMenuExtension } from "@/extensions";
|
||||
// plane editor imports
|
||||
import { RichTextEditorAdditionalExtensions } from "@/plane-editor/extensions/rich-text/extensions";
|
||||
// types
|
||||
import { EditorRefApi, IRichTextEditor } from "@/types";
|
||||
|
||||
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 extensions = [
|
||||
|
|
@ -17,17 +25,14 @@ const RichTextEditor = (props: IRichTextEditor) => {
|
|||
aiEnabled: false,
|
||||
dragDropEnabled: !!dragDropEnabled,
|
||||
}),
|
||||
...RichTextEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
fileHandler,
|
||||
}),
|
||||
];
|
||||
if (!disabledExtensions?.includes("slash-commands")) {
|
||||
extensions.push(
|
||||
SlashCommands({
|
||||
disabledExtensions,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}, [dragDropEnabled, disabledExtensions, externalExtensions]);
|
||||
}, [dragDropEnabled, disabledExtensions, externalExtensions, fileHandler]);
|
||||
|
||||
return (
|
||||
<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
|
||||
import { EditorReadOnlyRefApi, IRichTextReadOnlyEditor } from "@/types";
|
||||
// local imports
|
||||
import { ReadOnlyEditorWrapper } from "../read-only-editor-wrapper";
|
||||
|
||||
const RichTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => (
|
||||
<ReadOnlyEditorWrapper {...props} forwardedRef={ref as React.MutableRefObject<EditorReadOnlyRefApi | null>} />
|
||||
));
|
||||
const RichTextReadOnlyEditorWithRef = forwardRef<EditorReadOnlyRefApi, IRichTextReadOnlyEditor>((props, ref) => {
|
||||
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";
|
||||
|
||||
|
|
|
|||
|
|
@ -170,8 +170,9 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
|||
CustomTextAlignExtension,
|
||||
CustomCalloutExtension,
|
||||
UtilityExtension({
|
||||
isEditable: editable,
|
||||
disabledExtensions,
|
||||
fileHandler,
|
||||
isEditable: editable,
|
||||
}),
|
||||
CustomColorExtension,
|
||||
...CoreEditorAdditionalExtensions({
|
||||
|
|
|
|||
|
|
@ -127,8 +127,9 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
|||
CustomTextAlignExtension,
|
||||
CustomCalloutReadOnlyExtension,
|
||||
UtilityExtension({
|
||||
isEditable: false,
|
||||
disabledExtensions,
|
||||
fileHandler,
|
||||
isEditable: false,
|
||||
}),
|
||||
...CoreReadOnlyEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { DropHandlerPlugin } from "@/plugins/drop";
|
|||
import { FilePlugins } from "@/plugins/file/root";
|
||||
import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard";
|
||||
// types
|
||||
import { TFileHandler, TReadOnlyFileHandler } from "@/types";
|
||||
import { TExtensions, TFileHandler, TReadOnlyFileHandler } from "@/types";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands {
|
||||
|
|
@ -24,13 +24,14 @@ export interface UtilityExtensionStorage {
|
|||
}
|
||||
|
||||
type Props = {
|
||||
disabledExtensions: TExtensions[];
|
||||
fileHandler: TFileHandler | TReadOnlyFileHandler;
|
||||
isEditable: boolean;
|
||||
};
|
||||
|
||||
export const UtilityExtension = (props: Props) => {
|
||||
const { fileHandler, isEditable } = props;
|
||||
const { restore: restoreImageFn } = fileHandler;
|
||||
const { disabledExtensions, fileHandler, isEditable } = props;
|
||||
const { restore } = fileHandler;
|
||||
|
||||
return Extension.create<Record<string, unknown>, UtilityExtensionStorage>({
|
||||
name: "utility",
|
||||
|
|
@ -45,12 +46,15 @@ export const UtilityExtension = (props: Props) => {
|
|||
}),
|
||||
...codemark({ markType: this.editor.schema.marks.code }),
|
||||
MarkdownClipboardPlugin(this.editor),
|
||||
DropHandlerPlugin(this.editor),
|
||||
DropHandlerPlugin({
|
||||
disabledExtensions,
|
||||
editor: this.editor,
|
||||
}),
|
||||
];
|
||||
},
|
||||
|
||||
onCreate() {
|
||||
restorePublicImages(this.editor, restoreImageFn);
|
||||
restorePublicImages(this.editor, restore);
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,8 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorProps) => {
|
|||
...(extensions ?? []),
|
||||
...DocumentEditorAdditionalExtensions({
|
||||
disabledExtensions,
|
||||
issueEmbedConfig: embedHandler?.issue,
|
||||
embedConfig: embedHandler,
|
||||
fileHandler,
|
||||
provider,
|
||||
userDetails: user,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -3,10 +3,17 @@ import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|||
// constants
|
||||
import { ACCEPTED_ATTACHMENT_MIME_TYPES, ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
|
||||
// types
|
||||
import { TEditorCommands } from "@/types";
|
||||
import { TEditorCommands, TExtensions } from "@/types";
|
||||
|
||||
export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
||||
new Plugin({
|
||||
type Props = {
|
||||
disabledExtensions: TExtensions[];
|
||||
editor: Editor;
|
||||
};
|
||||
|
||||
export const DropHandlerPlugin = (props: Props): Plugin => {
|
||||
const { disabledExtensions, editor } = props;
|
||||
|
||||
return new Plugin({
|
||||
key: new PluginKey("drop-handler-plugin"),
|
||||
props: {
|
||||
handlePaste: (view, event) => {
|
||||
|
|
@ -25,6 +32,7 @@ export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
|||
if (acceptedFiles.length) {
|
||||
const pos = view.state.selection.from;
|
||||
insertFilesSafely({
|
||||
disabledExtensions,
|
||||
editor,
|
||||
files: acceptedFiles,
|
||||
initialPos: pos,
|
||||
|
|
@ -58,6 +66,7 @@ export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
|||
if (coordinates) {
|
||||
const pos = coordinates.pos;
|
||||
insertFilesSafely({
|
||||
disabledExtensions,
|
||||
editor,
|
||||
files: acceptedFiles,
|
||||
initialPos: pos,
|
||||
|
|
@ -71,8 +80,10 @@ export const DropHandlerPlugin = (editor: Editor): Plugin =>
|
|||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
type InsertFilesSafelyArgs = {
|
||||
disabledExtensions: TExtensions[];
|
||||
editor: Editor;
|
||||
event: "insert" | "drop";
|
||||
files: File[];
|
||||
|
|
@ -81,7 +92,7 @@ type 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;
|
||||
|
||||
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";
|
||||
}
|
||||
// insert file depending on the type at the current position
|
||||
if (fileType === "image") {
|
||||
if (fileType === "image" && !disabledExtensions.includes("image")) {
|
||||
editor.commands.insertImageComponent({
|
||||
file,
|
||||
pos,
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ export interface IReadOnlyEditorProps {
|
|||
disabledExtensions: TExtensions[];
|
||||
displayConfig?: TDisplayConfig;
|
||||
editorClassName?: string;
|
||||
extensions?: Extensions;
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
id: string;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue