mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
read only editor support added
This commit is contained in:
parent
3d87a56e3b
commit
de07f63089
@ -1,9 +1,16 @@
|
||||
// styles
|
||||
import "@/styles/tailwind.css";
|
||||
import "@/styles/editor.css";
|
||||
export { startImageUpload } from "@/ui/plugins/upload-image";
|
||||
|
||||
export { useEditor } from "@/ui/hooks/useEditor";
|
||||
// utils
|
||||
export { cn } from "@/lib/utils";
|
||||
export { getEditorClassNames } from "@/lib/utils";
|
||||
export { EditorContainer } from "@/ui/editor-container";
|
||||
export { EditorContentWrapper } from "@/ui/editor-content";
|
||||
export { startImageUpload } from "@/ui/plugins/upload-image";
|
||||
|
||||
// components
|
||||
export { EditorContainer } from "@/ui/components/editor-container";
|
||||
export { EditorContentWrapper } from "@/ui/components/editor-content";
|
||||
|
||||
// hooks
|
||||
export { useEditor } from "@/ui/hooks/useEditor";
|
||||
export { useReadOnlyEditor } from "@/ui/hooks/useReadOnlyEditor";
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Editor, EditorContent } from "@tiptap/react";
|
||||
import { ReactNode } from "react";
|
||||
import { ImageResizer } from "@/ui/extensions/image/image-resize";
|
||||
import { TableMenu } from "./menus/table-menu";
|
||||
import { TableMenu } from "@/ui/menus/table-menu";
|
||||
|
||||
interface EditorContentProps {
|
||||
editor: Editor | null;
|
@ -0,0 +1,17 @@
|
||||
import Image from "@tiptap/extension-image";
|
||||
|
||||
const ReadOnlyImageExtension = Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
width: {
|
||||
default: "35%",
|
||||
},
|
||||
height: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default ReadOnlyImageExtension;
|
@ -8,9 +8,9 @@ import TaskList from "@tiptap/extension-task-list";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||
|
||||
import { CustomTableCell } from "./table/table-cell";
|
||||
import { Table } from "./table/table";
|
||||
import { TableHeader } from "./table/table-header";
|
||||
import { CustomTableCell } from "@/ui/extensions/table/table-cell";
|
||||
import { Table } from "@/ui/extensions/table";
|
||||
import { TableHeader } from "@/ui/extensions/table/table-header";
|
||||
import { TableRow } from "@tiptap/extension-table-row";
|
||||
|
||||
import ImageExtension from "@/ui/extensions/image";
|
||||
|
37
packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx
Normal file
37
packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { useEditor as useCustomEditor, Editor } from "@tiptap/react";
|
||||
import { useImperativeHandle, useRef, MutableRefObject } from "react";
|
||||
import { CoreReadOnlyEditorExtensions } from "@/ui/read-only/extensions";
|
||||
import { CoreReadOnlyEditorProps } from "@/ui/read-only/props";
|
||||
|
||||
interface CustomReadOnlyEditorProps {
|
||||
value: string;
|
||||
forwardedRef?: any;
|
||||
}
|
||||
|
||||
export const useReadOnlyEditor = ({ value, forwardedRef }: CustomReadOnlyEditorProps) => {
|
||||
const editor = useCustomEditor({
|
||||
editable: false,
|
||||
content: (typeof value === "string" && value.trim() !== "") ? value : "<p></p>",
|
||||
editorProps: CoreReadOnlyEditorProps,
|
||||
extensions: CoreReadOnlyEditorExtensions,
|
||||
});
|
||||
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
editorRef.current = editor;
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
clearEditor: () => {
|
||||
editorRef.current?.commands.clearContent();
|
||||
},
|
||||
setEditorValue: (content: string) => {
|
||||
editorRef.current?.commands.setContent(content);
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return editor;
|
||||
};
|
92
packages/editor/core/src/ui/read-only/extensions.tsx
Normal file
92
packages/editor/core/src/ui/read-only/extensions.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import TiptapLink from "@tiptap/extension-link";
|
||||
import TiptapUnderline from "@tiptap/extension-underline";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
import TaskItem from "@tiptap/extension-task-item";
|
||||
import TaskList from "@tiptap/extension-task-list";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||
|
||||
import { CustomTableCell } from "@/ui/extensions/table/table-cell";
|
||||
import { Table } from "@/ui/extensions/table";
|
||||
import { TableHeader } from "@/ui/extensions/table/table-header";
|
||||
import { TableRow } from "@tiptap/extension-table-row";
|
||||
|
||||
import isValidHttpUrl from "@/ui/menus/bubble-menu/utils";
|
||||
import ReadOnlyImageExtension from "@/ui/extensions/image/read-only-image";
|
||||
|
||||
export const CoreReadOnlyEditorExtensions = [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc list-outside leading-3 -mt-2",
|
||||
},
|
||||
},
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal list-outside leading-3 -mt-2",
|
||||
},
|
||||
},
|
||||
listItem: {
|
||||
HTMLAttributes: {
|
||||
class: "leading-normal -mb-2",
|
||||
},
|
||||
},
|
||||
blockquote: {
|
||||
HTMLAttributes: {
|
||||
class: "border-l-4 border-custom-border-300",
|
||||
},
|
||||
},
|
||||
code: {
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
||||
spellcheck: "false",
|
||||
},
|
||||
},
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
dropcursor: {
|
||||
color: "rgba(var(--color-text-100))",
|
||||
width: 2,
|
||||
},
|
||||
gapcursor: false,
|
||||
}),
|
||||
Gapcursor,
|
||||
TiptapLink.configure({
|
||||
protocols: ["http", "https"],
|
||||
validate: (url) => isValidHttpUrl(url),
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
ReadOnlyImageExtension.configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-lg border border-custom-border-300",
|
||||
},
|
||||
}),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
Color,
|
||||
TaskList.configure({
|
||||
HTMLAttributes: {
|
||||
class: "not-prose pl-2",
|
||||
},
|
||||
}),
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "flex items-start my-4",
|
||||
},
|
||||
nested: true,
|
||||
}),
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformCopiedText: true,
|
||||
}),
|
||||
Table,
|
||||
TableHeader,
|
||||
CustomTableCell,
|
||||
TableRow,
|
||||
];
|
8
packages/editor/core/src/ui/read-only/props.tsx
Normal file
8
packages/editor/core/src/ui/read-only/props.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
|
||||
export const CoreReadOnlyEditorProps: EditorProps =
|
||||
{
|
||||
attributes: {
|
||||
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
|
||||
},
|
||||
};
|
@ -3,7 +3,7 @@ import { defineConfig, Options } from "tsup";
|
||||
export default defineConfig((options: Options) => ({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["cjs", "esm"],
|
||||
dts: false,
|
||||
dts: true,
|
||||
clean: false,
|
||||
external: ["react"],
|
||||
injectStyle: true,
|
||||
|
@ -1 +1,2 @@
|
||||
export { LiteTextEditor, LiteTextEditorWithRef } from "@/ui";
|
||||
export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "@/ui/read-only";
|
||||
|
54
packages/editor/lite-text-editor/src/ui/read-only/index.tsx
Normal file
54
packages/editor/lite-text-editor/src/ui/read-only/index.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
import { EditorContainer, EditorContentWrapper, getEditorClassNames, useReadOnlyEditor } from '@plane/editor-core';
|
||||
import * as React from 'react';
|
||||
|
||||
interface ICoreReadOnlyEditor {
|
||||
value: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
noBorder?: boolean;
|
||||
borderOnFocus?: boolean;
|
||||
customClassName?: string;
|
||||
}
|
||||
|
||||
interface EditorCoreProps extends ICoreReadOnlyEditor {
|
||||
forwardedRef?: React.Ref<EditorHandle>;
|
||||
}
|
||||
|
||||
interface EditorHandle {
|
||||
clearEditor: () => void;
|
||||
setEditorValue: (content: string) => void;
|
||||
}
|
||||
|
||||
const LiteReadOnlyEditor = ({
|
||||
editorContentCustomClassNames,
|
||||
noBorder,
|
||||
borderOnFocus,
|
||||
customClassName,
|
||||
value,
|
||||
forwardedRef,
|
||||
}: EditorCoreProps) => {
|
||||
const editor = useReadOnlyEditor({
|
||||
value,
|
||||
forwardedRef,
|
||||
});
|
||||
|
||||
const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName });
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
</div>
|
||||
</EditorContainer >
|
||||
);
|
||||
};
|
||||
|
||||
const LiteReadOnlyEditorWithRef = React.forwardRef<EditorHandle, ICoreReadOnlyEditor>((props, ref) => (
|
||||
<LiteReadOnlyEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
LiteReadOnlyEditorWithRef.displayName = "LiteReadOnlyEditorWithRef";
|
||||
|
||||
export { LiteReadOnlyEditor , LiteReadOnlyEditorWithRef };
|
@ -3,7 +3,7 @@ import { defineConfig, Options } from "tsup";
|
||||
export default defineConfig((options: Options) => ({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["cjs", "esm"],
|
||||
dts: false,
|
||||
dts: true,
|
||||
clean: false,
|
||||
external: ["react"],
|
||||
injectStyle: true,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import "@/styles/github-dark.css";
|
||||
|
||||
export { RichTextEditor, RichTextEditorWithRef } from "@/ui";
|
||||
export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "@/ui/read-only";
|
||||
|
54
packages/editor/rich-text-editor/src/ui/read-only/index.tsx
Normal file
54
packages/editor/rich-text-editor/src/ui/read-only/index.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
import { EditorContainer, EditorContentWrapper, getEditorClassNames, useReadOnlyEditor } from '@plane/editor-core';
|
||||
import * as React from 'react';
|
||||
|
||||
interface IRichTextReadOnlyEditor {
|
||||
value: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
noBorder?: boolean;
|
||||
borderOnFocus?: boolean;
|
||||
customClassName?: string;
|
||||
}
|
||||
|
||||
interface RichTextReadOnlyEditorProps extends IRichTextReadOnlyEditor {
|
||||
forwardedRef?: React.Ref<EditorHandle>;
|
||||
}
|
||||
|
||||
interface EditorHandle {
|
||||
clearEditor: () => void;
|
||||
setEditorValue: (content: string) => void;
|
||||
}
|
||||
|
||||
const RichReadOnlyEditor = ({
|
||||
editorContentCustomClassNames,
|
||||
noBorder,
|
||||
borderOnFocus,
|
||||
customClassName,
|
||||
value,
|
||||
forwardedRef,
|
||||
}: RichTextReadOnlyEditorProps) => {
|
||||
const editor = useReadOnlyEditor({
|
||||
value,
|
||||
forwardedRef,
|
||||
});
|
||||
|
||||
const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName });
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
</div>
|
||||
</EditorContainer >
|
||||
);
|
||||
};
|
||||
|
||||
const RichReadOnlyEditorWithRef = React.forwardRef<EditorHandle, IRichTextReadOnlyEditor>((props, ref) => (
|
||||
<RichReadOnlyEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
RichReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef";
|
||||
|
||||
export { RichReadOnlyEditor , RichReadOnlyEditorWithRef };
|
@ -3,7 +3,7 @@ import { defineConfig, Options } from "tsup";
|
||||
export default defineConfig((options: Options) => ({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["cjs", "esm"],
|
||||
dts: false,
|
||||
dts: true,
|
||||
clean: false,
|
||||
external: ["react"],
|
||||
injectStyle: true,
|
||||
|
@ -9,7 +9,7 @@ import useUser from "hooks/use-user";
|
||||
// ui
|
||||
import { CustomMenu, Icon } from "components/ui";
|
||||
import { CommentReaction } from "components/issues";
|
||||
import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
|
||||
import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-text-editor";
|
||||
// helpers
|
||||
import { timeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
@ -151,12 +151,9 @@ export const CommentCard: React.FC<Props> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<LiteTextEditorWithRef
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.deleteImage}
|
||||
<LiteReadOnlyEditorWithRef
|
||||
ref={showEditorRef}
|
||||
value={comment.comment_html}
|
||||
editable={false}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
/>
|
||||
<CommentReaction projectId={comment.project} commentId={comment.id} />
|
||||
|
Loading…
Reference in New Issue
Block a user