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/tailwind.css";
|
||||||
import "@/styles/editor.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 { cn } from "@/lib/utils";
|
||||||
export { getEditorClassNames } from "@/lib/utils";
|
export { getEditorClassNames } from "@/lib/utils";
|
||||||
export { EditorContainer } from "@/ui/editor-container";
|
export { startImageUpload } from "@/ui/plugins/upload-image";
|
||||||
export { EditorContentWrapper } from "@/ui/editor-content";
|
|
||||||
|
// 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 { Editor, EditorContent } from "@tiptap/react";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { ImageResizer } from "@/ui/extensions/image/image-resize";
|
import { ImageResizer } from "@/ui/extensions/image/image-resize";
|
||||||
import { TableMenu } from "./menus/table-menu";
|
import { TableMenu } from "@/ui/menus/table-menu";
|
||||||
|
|
||||||
interface EditorContentProps {
|
interface EditorContentProps {
|
||||||
editor: Editor | null;
|
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 { Markdown } from "tiptap-markdown";
|
||||||
import Gapcursor from "@tiptap/extension-gapcursor";
|
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||||
|
|
||||||
import { CustomTableCell } from "./table/table-cell";
|
import { CustomTableCell } from "@/ui/extensions/table/table-cell";
|
||||||
import { Table } from "./table/table";
|
import { Table } from "@/ui/extensions/table";
|
||||||
import { TableHeader } from "./table/table-header";
|
import { TableHeader } from "@/ui/extensions/table/table-header";
|
||||||
import { TableRow } from "@tiptap/extension-table-row";
|
import { TableRow } from "@tiptap/extension-table-row";
|
||||||
|
|
||||||
import ImageExtension from "@/ui/extensions/image";
|
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) => ({
|
export default defineConfig((options: Options) => ({
|
||||||
entry: ["src/index.ts"],
|
entry: ["src/index.ts"],
|
||||||
format: ["cjs", "esm"],
|
format: ["cjs", "esm"],
|
||||||
dts: false,
|
dts: true,
|
||||||
clean: false,
|
clean: false,
|
||||||
external: ["react"],
|
external: ["react"],
|
||||||
injectStyle: true,
|
injectStyle: true,
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export { LiteTextEditor, LiteTextEditorWithRef } from "@/ui";
|
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) => ({
|
export default defineConfig((options: Options) => ({
|
||||||
entry: ["src/index.ts"],
|
entry: ["src/index.ts"],
|
||||||
format: ["cjs", "esm"],
|
format: ["cjs", "esm"],
|
||||||
dts: false,
|
dts: true,
|
||||||
clean: false,
|
clean: false,
|
||||||
external: ["react"],
|
external: ["react"],
|
||||||
injectStyle: true,
|
injectStyle: true,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
import "@/styles/github-dark.css";
|
import "@/styles/github-dark.css";
|
||||||
|
|
||||||
export { RichTextEditor, RichTextEditorWithRef } from "@/ui";
|
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) => ({
|
export default defineConfig((options: Options) => ({
|
||||||
entry: ["src/index.ts"],
|
entry: ["src/index.ts"],
|
||||||
format: ["cjs", "esm"],
|
format: ["cjs", "esm"],
|
||||||
dts: false,
|
dts: true,
|
||||||
clean: false,
|
clean: false,
|
||||||
external: ["react"],
|
external: ["react"],
|
||||||
injectStyle: true,
|
injectStyle: true,
|
||||||
|
@ -9,7 +9,7 @@ import useUser from "hooks/use-user";
|
|||||||
// ui
|
// ui
|
||||||
import { CustomMenu, Icon } from "components/ui";
|
import { CustomMenu, Icon } from "components/ui";
|
||||||
import { CommentReaction } from "components/issues";
|
import { CommentReaction } from "components/issues";
|
||||||
import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
|
import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-text-editor";
|
||||||
// helpers
|
// helpers
|
||||||
import { timeAgo } from "helpers/date-time.helper";
|
import { timeAgo } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
@ -151,12 +151,9 @@ export const CommentCard: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<LiteTextEditorWithRef
|
<LiteReadOnlyEditorWithRef
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
|
||||||
deleteFile={fileService.deleteImage}
|
|
||||||
ref={showEditorRef}
|
ref={showEditorRef}
|
||||||
value={comment.comment_html}
|
value={comment.comment_html}
|
||||||
editable={false}
|
|
||||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||||
/>
|
/>
|
||||||
<CommentReaction projectId={comment.project} commentId={comment.id} />
|
<CommentReaction projectId={comment.project} commentId={comment.id} />
|
||||||
|
Loading…
Reference in New Issue
Block a user