From c067eaa1ed3cfc02987532b271a42169b91d6e8d Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:10:32 +0530 Subject: [PATCH] [WIKI-542] refactor: editor extensions (#7345) * refactor: editor extensions * fix: placeholder extension --- .../editor/src/core/extensions/code/index.tsx | 3 + .../core/extensions/custom-link/extension.tsx | 9 +- .../editor/src/core/extensions/extensions.ts | 100 ++---------------- .../src/core/extensions/horizontal-rule.ts | 7 +- .../editor/src/core/extensions/placeholder.ts | 43 ++++++++ .../core/extensions/read-only-extensions.ts | 62 ++--------- .../editor/src/core/extensions/starter-kit.ts | 46 ++++++++ 7 files changed, 117 insertions(+), 153 deletions(-) create mode 100644 packages/editor/src/core/extensions/placeholder.ts create mode 100644 packages/editor/src/core/extensions/starter-kit.ts diff --git a/packages/editor/src/core/extensions/code/index.tsx b/packages/editor/src/core/extensions/code/index.tsx index 15d66a6ba..7c679e92e 100644 --- a/packages/editor/src/core/extensions/code/index.tsx +++ b/packages/editor/src/core/extensions/code/index.tsx @@ -118,4 +118,7 @@ export const CustomCodeBlockExtension = CodeBlockLowlight.extend({ lowlight, defaultLanguage: "plaintext", exitOnTripleEnter: false, + HTMLAttributes: { + class: "", + }, }); diff --git a/packages/editor/src/core/extensions/custom-link/extension.tsx b/packages/editor/src/core/extensions/custom-link/extension.tsx index 182afc9f8..9be125ce6 100644 --- a/packages/editor/src/core/extensions/custom-link/extension.tsx +++ b/packages/editor/src/core/extensions/custom-link/extension.tsx @@ -3,6 +3,8 @@ import { Plugin } from "@tiptap/pm/state"; import { find, registerCustomProtocol, reset } from "linkifyjs"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; +// helpers +import { isValidHttpUrl } from "@/helpers/common"; // local imports import { autolink } from "./helpers/autolink"; import { clickHandler } from "./helpers/clickHandler"; @@ -112,13 +114,14 @@ export const CustomLinkExtension = Mark.create({ linkOnPaste: true, autolink: true, inclusive: false, - protocols: [], + protocols: ["http", "https"], HTMLAttributes: { target: "_blank", rel: "noopener noreferrer nofollow", - class: null, + class: + "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, - validate: undefined, + validate: (url: string) => isValidHttpUrl(url).isValid, }; }, diff --git a/packages/editor/src/core/extensions/extensions.ts b/packages/editor/src/core/extensions/extensions.ts index 1833dec46..27103693b 100644 --- a/packages/editor/src/core/extensions/extensions.ts +++ b/packages/editor/src/core/extensions/extensions.ts @@ -1,14 +1,10 @@ import { Extensions } from "@tiptap/core"; import CharacterCount from "@tiptap/extension-character-count"; -import Placeholder from "@tiptap/extension-placeholder"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import TextStyle from "@tiptap/extension-text-style"; import TiptapUnderline from "@tiptap/extension-underline"; -import StarterKit from "@tiptap/starter-kit"; import { Markdown } from "tiptap-markdown"; -// constants -import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions import { CustomCalloutExtension, @@ -30,9 +26,6 @@ import { TableRow, UtilityExtension, } from "@/extensions"; -// helpers -import { isValidHttpUrl } from "@/helpers/common"; -import { getExtensionStorage } from "@/helpers/get-extension-storage"; // plane editor extensions import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions"; // types @@ -40,6 +33,8 @@ import type { IEditorProps } from "@/types"; // local imports import { CustomImageExtension } from "./custom-image/extension"; import { EmojiExtension } from "./emoji/extension"; +import { CustomPlaceholderExtension } from "./placeholder"; +import { CustomStarterKitExtension } from "./starter-kit"; type TArguments = Pick< IEditorProps, @@ -62,62 +57,15 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { } = args; const extensions = [ - StarterKit.configure({ - bulletList: { - HTMLAttributes: { - class: "list-disc pl-7 space-y-[--list-spacing-y]", - }, - }, - orderedList: { - HTMLAttributes: { - class: "list-decimal pl-7 space-y-[--list-spacing-y]", - }, - }, - listItem: { - HTMLAttributes: { - class: "not-prose space-y-2", - }, - }, - code: false, - codeBlock: false, - horizontalRule: false, - blockquote: false, - paragraph: { - HTMLAttributes: { - class: "editor-paragraph-block", - }, - }, - heading: { - HTMLAttributes: { - class: "editor-heading-block", - }, - }, - dropcursor: { - class: - "text-custom-text-300 transition-all motion-reduce:transition-none motion-reduce:hover:transform-none duration-200 ease-[cubic-bezier(0.165, 0.84, 0.44, 1)]", - }, - ...(enableHistory ? {} : { history: false }), + CustomStarterKitExtension({ + enableHistory, }), EmojiExtension, CustomQuoteExtension, - CustomHorizontalRule.configure({ - HTMLAttributes: { - class: "py-4 border-custom-border-400", - }, - }), + CustomHorizontalRule, CustomKeymap, ListKeymap({ tabIndex }), - CustomLinkExtension.configure({ - openOnClick: true, - autolink: true, - linkOnPaste: true, - protocols: ["http", "https"], - validate: (url: string) => isValidHttpUrl(url).isValid, - HTMLAttributes: { - class: - "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", - }, - }), + CustomLinkExtension, CustomTypographyExtension, TiptapUnderline, TextStyle, @@ -132,11 +80,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { }, nested: true, }), - CustomCodeBlockExtension.configure({ - HTMLAttributes: { - class: "", - }, - }), + CustomCodeBlockExtension, CustomCodeInlineExtension, Markdown.configure({ html: true, @@ -149,34 +93,9 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { TableCell, TableRow, CustomMentionExtension(mentionHandler), - Placeholder.configure({ - placeholder: ({ editor, node }) => { - if (!editor.isEditable) return ""; - - if (node.type.name === CORE_EXTENSIONS.HEADING) return `Heading ${node.attrs.level}`; - - const isUploadInProgress = getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress; - - if (isUploadInProgress) return ""; - - const shouldHidePlaceholder = - editor.isActive(CORE_EXTENSIONS.TABLE) || - editor.isActive(CORE_EXTENSIONS.CODE_BLOCK) || - editor.isActive(CORE_EXTENSIONS.IMAGE) || - editor.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE); - - if (shouldHidePlaceholder) return ""; - - if (placeholder) { - if (typeof placeholder === "string") return placeholder; - else return placeholder(editor.isFocused, editor.getHTML()); - } - - return "Press '/' for commands..."; - }, - includeChildren: true, - }), + CustomPlaceholderExtension({ placeholder }), CharacterCount, + CustomColorExtension, CustomTextAlignExtension, CustomCalloutExtension, UtilityExtension({ @@ -184,7 +103,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { fileHandler, isEditable: editable, }), - CustomColorExtension, ...CoreEditorAdditionalExtensions({ disabledExtensions, flaggedExtensions, diff --git a/packages/editor/src/core/extensions/horizontal-rule.ts b/packages/editor/src/core/extensions/horizontal-rule.ts index 99a5dacc3..4ffbcbb00 100644 --- a/packages/editor/src/core/extensions/horizontal-rule.ts +++ b/packages/editor/src/core/extensions/horizontal-rule.ts @@ -20,15 +20,16 @@ declare module "@tiptap/core" { export const CustomHorizontalRule = Node.create({ name: CORE_EXTENSIONS.HORIZONTAL_RULE, + group: "block", addOptions() { return { - HTMLAttributes: {}, + HTMLAttributes: { + class: "py-4 border-custom-border-400", + }, }; }, - group: "block", - parseHTML() { return [ { diff --git a/packages/editor/src/core/extensions/placeholder.ts b/packages/editor/src/core/extensions/placeholder.ts new file mode 100644 index 000000000..9e23792a2 --- /dev/null +++ b/packages/editor/src/core/extensions/placeholder.ts @@ -0,0 +1,43 @@ +import Placeholder from "@tiptap/extension-placeholder"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// helpers +import { getExtensionStorage } from "@/helpers/get-extension-storage"; +// types +import type { IEditorProps } from "@/types"; + +type TArgs = { + placeholder: IEditorProps["placeholder"]; +}; + +export const CustomPlaceholderExtension = (args: TArgs) => { + const { placeholder } = args; + + return Placeholder.configure({ + placeholder: ({ editor, node }) => { + if (!editor.isEditable) return ""; + + if (node.type.name === CORE_EXTENSIONS.HEADING) return `Heading ${node.attrs.level}`; + + const isUploadInProgress = getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress; + + if (isUploadInProgress) return ""; + + const shouldHidePlaceholder = + editor.isActive(CORE_EXTENSIONS.TABLE) || + editor.isActive(CORE_EXTENSIONS.CODE_BLOCK) || + editor.isActive(CORE_EXTENSIONS.IMAGE) || + editor.isActive(CORE_EXTENSIONS.CUSTOM_IMAGE); + + if (shouldHidePlaceholder) return ""; + + if (placeholder) { + if (typeof placeholder === "string") return placeholder; + else return placeholder(editor.isFocused, editor.getHTML()); + } + + return "Press '/' for commands..."; + }, + includeChildren: true, + }); +}; diff --git a/packages/editor/src/core/extensions/read-only-extensions.ts b/packages/editor/src/core/extensions/read-only-extensions.ts index 25541dd3c..4d627b7f9 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.ts +++ b/packages/editor/src/core/extensions/read-only-extensions.ts @@ -4,7 +4,6 @@ import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import TextStyle from "@tiptap/extension-text-style"; import TiptapUnderline from "@tiptap/extension-underline"; -import StarterKit from "@tiptap/starter-kit"; import { Markdown } from "tiptap-markdown"; // extensions import { @@ -25,8 +24,6 @@ import { UtilityExtension, ImageExtension, } from "@/extensions"; -// helpers -import { isValidHttpUrl } from "@/helpers/common"; // plane editor extensions import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions"; // types @@ -34,6 +31,7 @@ import type { IReadOnlyEditorProps } from "@/types"; // local imports import { CustomImageExtension } from "./custom-image/extension"; import { EmojiExtension } from "./emoji/extension"; +import { CustomStarterKitExtension } from "./starter-kit"; type Props = Pick; @@ -41,57 +39,13 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { const { disabledExtensions, fileHandler, flaggedExtensions, mentionHandler } = props; const extensions = [ - StarterKit.configure({ - bulletList: { - HTMLAttributes: { - class: "list-disc pl-7 space-y-[--list-spacing-y]", - }, - }, - orderedList: { - HTMLAttributes: { - class: "list-decimal pl-7 space-y-[--list-spacing-y]", - }, - }, - listItem: { - HTMLAttributes: { - class: "not-prose space-y-2", - }, - }, - code: false, - codeBlock: false, - horizontalRule: false, - blockquote: false, - paragraph: { - HTMLAttributes: { - class: "editor-paragraph-block", - }, - }, - heading: { - HTMLAttributes: { - class: "editor-heading-block", - }, - }, - dropcursor: false, - gapcursor: false, + CustomStarterKitExtension({ + enableHistory: false, }), EmojiExtension, CustomQuoteExtension, - CustomHorizontalRule.configure({ - HTMLAttributes: { - class: "py-4 border-custom-border-400", - }, - }), - CustomLinkExtension.configure({ - openOnClick: true, - autolink: true, - linkOnPaste: true, - protocols: ["http", "https"], - validate: (url: string) => isValidHttpUrl(url).isValid, - HTMLAttributes: { - class: - "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", - }, - }), + CustomHorizontalRule, + CustomLinkExtension, CustomTypographyExtension, TiptapUnderline, TextStyle, @@ -106,11 +60,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { }, nested: true, }), - CustomCodeBlockExtension.configure({ - HTMLAttributes: { - class: "", - }, - }), + CustomCodeBlockExtension, CustomCodeInlineExtension, Markdown.configure({ html: true, diff --git a/packages/editor/src/core/extensions/starter-kit.ts b/packages/editor/src/core/extensions/starter-kit.ts new file mode 100644 index 000000000..e6a4c968d --- /dev/null +++ b/packages/editor/src/core/extensions/starter-kit.ts @@ -0,0 +1,46 @@ +import StarterKit from "@tiptap/starter-kit"; + +type TArgs = { + enableHistory: boolean; +}; + +export const CustomStarterKitExtension = (args: TArgs) => { + const { enableHistory } = args; + + return StarterKit.configure({ + bulletList: { + HTMLAttributes: { + class: "list-disc pl-7 space-y-[--list-spacing-y]", + }, + }, + orderedList: { + HTMLAttributes: { + class: "list-decimal pl-7 space-y-[--list-spacing-y]", + }, + }, + listItem: { + HTMLAttributes: { + class: "not-prose space-y-2", + }, + }, + code: false, + codeBlock: false, + horizontalRule: false, + blockquote: false, + paragraph: { + HTMLAttributes: { + class: "editor-paragraph-block", + }, + }, + heading: { + HTMLAttributes: { + class: "editor-heading-block", + }, + }, + dropcursor: { + class: + "text-custom-text-300 transition-all motion-reduce:transition-none motion-reduce:hover:transform-none duration-200 ease-[cubic-bezier(0.165, 0.84, 0.44, 1)]", + }, + ...(enableHistory ? {} : { history: false }), + }); +};