From de07f6308925205f7273f2d1ef9c64ecefdb4a26 Mon Sep 17 00:00:00 2001 From: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:24:57 +0530 Subject: [PATCH] read only editor support added --- packages/editor/core/src/index.ts | 15 ++- .../ui/{ => components}/editor-container.tsx | 0 .../ui/{ => components}/editor-content.tsx | 2 +- .../ui/extensions/image/read-only-image.tsx | 17 ++++ .../editor/core/src/ui/extensions/index.tsx | 6 +- .../extensions/table/{table.ts => index.ts} | 0 .../core/src/ui/hooks/useReadOnlyEditor.tsx | 37 ++++++++ .../core/src/ui/read-only/extensions.tsx | 92 +++++++++++++++++++ .../editor/core/src/ui/read-only/props.tsx | 8 ++ packages/editor/core/tsup.config.ts | 2 +- packages/editor/lite-text-editor/src/index.ts | 1 + .../src/ui/read-only/index.tsx | 54 +++++++++++ .../editor/lite-text-editor/tsup.config.ts | 2 +- packages/editor/rich-text-editor/src/index.ts | 1 + .../src/ui/read-only/index.tsx | 54 +++++++++++ .../editor/rich-text-editor/tsup.config.ts | 2 +- .../issues/comment/comment-card.tsx | 7 +- 17 files changed, 284 insertions(+), 16 deletions(-) rename packages/editor/core/src/ui/{ => components}/editor-container.tsx (100%) rename packages/editor/core/src/ui/{ => components}/editor-content.tsx (92%) create mode 100644 packages/editor/core/src/ui/extensions/image/read-only-image.tsx rename packages/editor/core/src/ui/extensions/table/{table.ts => index.ts} (100%) create mode 100644 packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx create mode 100644 packages/editor/core/src/ui/read-only/extensions.tsx create mode 100644 packages/editor/core/src/ui/read-only/props.tsx create mode 100644 packages/editor/lite-text-editor/src/ui/read-only/index.tsx create mode 100644 packages/editor/rich-text-editor/src/ui/read-only/index.tsx diff --git a/packages/editor/core/src/index.ts b/packages/editor/core/src/index.ts index 8b72e0bf3..c46ad9336 100644 --- a/packages/editor/core/src/index.ts +++ b/packages/editor/core/src/index.ts @@ -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"; diff --git a/packages/editor/core/src/ui/editor-container.tsx b/packages/editor/core/src/ui/components/editor-container.tsx similarity index 100% rename from packages/editor/core/src/ui/editor-container.tsx rename to packages/editor/core/src/ui/components/editor-container.tsx diff --git a/packages/editor/core/src/ui/editor-content.tsx b/packages/editor/core/src/ui/components/editor-content.tsx similarity index 92% rename from packages/editor/core/src/ui/editor-content.tsx rename to packages/editor/core/src/ui/components/editor-content.tsx index 7b06944d8..1e56e98c9 100644 --- a/packages/editor/core/src/ui/editor-content.tsx +++ b/packages/editor/core/src/ui/components/editor-content.tsx @@ -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; diff --git a/packages/editor/core/src/ui/extensions/image/read-only-image.tsx b/packages/editor/core/src/ui/extensions/image/read-only-image.tsx new file mode 100644 index 000000000..73a763d04 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/image/read-only-image.tsx @@ -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; diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index b2d0a5c57..7aac1adb1 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -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"; diff --git a/packages/editor/core/src/ui/extensions/table/table.ts b/packages/editor/core/src/ui/extensions/table/index.ts similarity index 100% rename from packages/editor/core/src/ui/extensions/table/table.ts rename to packages/editor/core/src/ui/extensions/table/index.ts diff --git a/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx b/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx new file mode 100644 index 000000000..5d08b867c --- /dev/null +++ b/packages/editor/core/src/ui/hooks/useReadOnlyEditor.tsx @@ -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 : "

", + editorProps: CoreReadOnlyEditorProps, + extensions: CoreReadOnlyEditorExtensions, + }); + + const editorRef: MutableRefObject = 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; +}; diff --git a/packages/editor/core/src/ui/read-only/extensions.tsx b/packages/editor/core/src/ui/read-only/extensions.tsx new file mode 100644 index 000000000..f879b2744 --- /dev/null +++ b/packages/editor/core/src/ui/read-only/extensions.tsx @@ -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, + ]; diff --git a/packages/editor/core/src/ui/read-only/props.tsx b/packages/editor/core/src/ui/read-only/props.tsx new file mode 100644 index 000000000..25db2b68c --- /dev/null +++ b/packages/editor/core/src/ui/read-only/props.tsx @@ -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`, + }, +}; diff --git a/packages/editor/core/tsup.config.ts b/packages/editor/core/tsup.config.ts index 907f339a1..5e89e04af 100644 --- a/packages/editor/core/tsup.config.ts +++ b/packages/editor/core/tsup.config.ts @@ -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, diff --git a/packages/editor/lite-text-editor/src/index.ts b/packages/editor/lite-text-editor/src/index.ts index 9238be8b9..0a276d1c0 100644 --- a/packages/editor/lite-text-editor/src/index.ts +++ b/packages/editor/lite-text-editor/src/index.ts @@ -1 +1,2 @@ export { LiteTextEditor, LiteTextEditorWithRef } from "@/ui"; +export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "@/ui/read-only"; diff --git a/packages/editor/lite-text-editor/src/ui/read-only/index.tsx b/packages/editor/lite-text-editor/src/ui/read-only/index.tsx new file mode 100644 index 000000000..3990cb734 --- /dev/null +++ b/packages/editor/lite-text-editor/src/ui/read-only/index.tsx @@ -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; +} + +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 ( + +
+ +
+
+ ); +}; + +const LiteReadOnlyEditorWithRef = React.forwardRef((props, ref) => ( + +)); + +LiteReadOnlyEditorWithRef.displayName = "LiteReadOnlyEditorWithRef"; + +export { LiteReadOnlyEditor , LiteReadOnlyEditorWithRef }; diff --git a/packages/editor/lite-text-editor/tsup.config.ts b/packages/editor/lite-text-editor/tsup.config.ts index 907f339a1..5e89e04af 100644 --- a/packages/editor/lite-text-editor/tsup.config.ts +++ b/packages/editor/lite-text-editor/tsup.config.ts @@ -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, diff --git a/packages/editor/rich-text-editor/src/index.ts b/packages/editor/rich-text-editor/src/index.ts index b7ef6bbe4..dd8f35791 100644 --- a/packages/editor/rich-text-editor/src/index.ts +++ b/packages/editor/rich-text-editor/src/index.ts @@ -1,3 +1,4 @@ import "@/styles/github-dark.css"; export { RichTextEditor, RichTextEditorWithRef } from "@/ui"; +export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "@/ui/read-only"; diff --git a/packages/editor/rich-text-editor/src/ui/read-only/index.tsx b/packages/editor/rich-text-editor/src/ui/read-only/index.tsx new file mode 100644 index 000000000..dc058cf89 --- /dev/null +++ b/packages/editor/rich-text-editor/src/ui/read-only/index.tsx @@ -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; +} + +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 ( + +
+ +
+
+ ); +}; + +const RichReadOnlyEditorWithRef = React.forwardRef((props, ref) => ( + +)); + +RichReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef"; + +export { RichReadOnlyEditor , RichReadOnlyEditorWithRef }; diff --git a/packages/editor/rich-text-editor/tsup.config.ts b/packages/editor/rich-text-editor/tsup.config.ts index 907f339a1..5e89e04af 100644 --- a/packages/editor/rich-text-editor/tsup.config.ts +++ b/packages/editor/rich-text-editor/tsup.config.ts @@ -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, diff --git a/web/components/issues/comment/comment-card.tsx b/web/components/issues/comment/comment-card.tsx index 147c49bd1..db33189f8 100644 --- a/web/components/issues/comment/comment-card.tsx +++ b/web/components/issues/comment/comment-card.tsx @@ -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 = ({ /> )} -