[WIKI-542] refactor: editor extensions (#7345)
* refactor: editor extensions * fix: placeholder extension
This commit is contained in:
parent
2c70c1aaa8
commit
c067eaa1ed
7 changed files with 117 additions and 153 deletions
|
|
@ -118,4 +118,7 @@ export const CustomCodeBlockExtension = CodeBlockLowlight.extend({
|
||||||
lowlight,
|
lowlight,
|
||||||
defaultLanguage: "plaintext",
|
defaultLanguage: "plaintext",
|
||||||
exitOnTripleEnter: false,
|
exitOnTripleEnter: false,
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { Plugin } from "@tiptap/pm/state";
|
||||||
import { find, registerCustomProtocol, reset } from "linkifyjs";
|
import { find, registerCustomProtocol, reset } from "linkifyjs";
|
||||||
// constants
|
// constants
|
||||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||||
|
// helpers
|
||||||
|
import { isValidHttpUrl } from "@/helpers/common";
|
||||||
// local imports
|
// local imports
|
||||||
import { autolink } from "./helpers/autolink";
|
import { autolink } from "./helpers/autolink";
|
||||||
import { clickHandler } from "./helpers/clickHandler";
|
import { clickHandler } from "./helpers/clickHandler";
|
||||||
|
|
@ -112,13 +114,14 @@ export const CustomLinkExtension = Mark.create<LinkOptions, CustomLinkStorage>({
|
||||||
linkOnPaste: true,
|
linkOnPaste: true,
|
||||||
autolink: true,
|
autolink: true,
|
||||||
inclusive: false,
|
inclusive: false,
|
||||||
protocols: [],
|
protocols: ["http", "https"],
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
rel: "noopener noreferrer nofollow",
|
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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
import { Extensions } from "@tiptap/core";
|
import { Extensions } from "@tiptap/core";
|
||||||
import CharacterCount from "@tiptap/extension-character-count";
|
import CharacterCount from "@tiptap/extension-character-count";
|
||||||
import Placeholder from "@tiptap/extension-placeholder";
|
|
||||||
import TaskItem from "@tiptap/extension-task-item";
|
import TaskItem from "@tiptap/extension-task-item";
|
||||||
import TaskList from "@tiptap/extension-task-list";
|
import TaskList from "@tiptap/extension-task-list";
|
||||||
import TextStyle from "@tiptap/extension-text-style";
|
import TextStyle from "@tiptap/extension-text-style";
|
||||||
import TiptapUnderline from "@tiptap/extension-underline";
|
import TiptapUnderline from "@tiptap/extension-underline";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
|
||||||
import { Markdown } from "tiptap-markdown";
|
import { Markdown } from "tiptap-markdown";
|
||||||
// constants
|
|
||||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
|
||||||
// extensions
|
// extensions
|
||||||
import {
|
import {
|
||||||
CustomCalloutExtension,
|
CustomCalloutExtension,
|
||||||
|
|
@ -30,9 +26,6 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
UtilityExtension,
|
UtilityExtension,
|
||||||
} from "@/extensions";
|
} from "@/extensions";
|
||||||
// helpers
|
|
||||||
import { isValidHttpUrl } from "@/helpers/common";
|
|
||||||
import { getExtensionStorage } from "@/helpers/get-extension-storage";
|
|
||||||
// plane editor extensions
|
// plane editor extensions
|
||||||
import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
||||||
// types
|
// types
|
||||||
|
|
@ -40,6 +33,8 @@ import type { IEditorProps } from "@/types";
|
||||||
// local imports
|
// local imports
|
||||||
import { CustomImageExtension } from "./custom-image/extension";
|
import { CustomImageExtension } from "./custom-image/extension";
|
||||||
import { EmojiExtension } from "./emoji/extension";
|
import { EmojiExtension } from "./emoji/extension";
|
||||||
|
import { CustomPlaceholderExtension } from "./placeholder";
|
||||||
|
import { CustomStarterKitExtension } from "./starter-kit";
|
||||||
|
|
||||||
type TArguments = Pick<
|
type TArguments = Pick<
|
||||||
IEditorProps,
|
IEditorProps,
|
||||||
|
|
@ -62,62 +57,15 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
StarterKit.configure({
|
CustomStarterKitExtension({
|
||||||
bulletList: {
|
enableHistory,
|
||||||
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 }),
|
|
||||||
}),
|
}),
|
||||||
EmojiExtension,
|
EmojiExtension,
|
||||||
CustomQuoteExtension,
|
CustomQuoteExtension,
|
||||||
CustomHorizontalRule.configure({
|
CustomHorizontalRule,
|
||||||
HTMLAttributes: {
|
|
||||||
class: "py-4 border-custom-border-400",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
CustomKeymap,
|
CustomKeymap,
|
||||||
ListKeymap({ tabIndex }),
|
ListKeymap({ tabIndex }),
|
||||||
CustomLinkExtension.configure({
|
CustomLinkExtension,
|
||||||
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",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
CustomTypographyExtension,
|
CustomTypographyExtension,
|
||||||
TiptapUnderline,
|
TiptapUnderline,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
|
|
@ -132,11 +80,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||||
},
|
},
|
||||||
nested: true,
|
nested: true,
|
||||||
}),
|
}),
|
||||||
CustomCodeBlockExtension.configure({
|
CustomCodeBlockExtension,
|
||||||
HTMLAttributes: {
|
|
||||||
class: "",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
CustomCodeInlineExtension,
|
CustomCodeInlineExtension,
|
||||||
Markdown.configure({
|
Markdown.configure({
|
||||||
html: true,
|
html: true,
|
||||||
|
|
@ -149,34 +93,9 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||||
TableCell,
|
TableCell,
|
||||||
TableRow,
|
TableRow,
|
||||||
CustomMentionExtension(mentionHandler),
|
CustomMentionExtension(mentionHandler),
|
||||||
Placeholder.configure({
|
CustomPlaceholderExtension({ placeholder }),
|
||||||
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,
|
|
||||||
}),
|
|
||||||
CharacterCount,
|
CharacterCount,
|
||||||
|
CustomColorExtension,
|
||||||
CustomTextAlignExtension,
|
CustomTextAlignExtension,
|
||||||
CustomCalloutExtension,
|
CustomCalloutExtension,
|
||||||
UtilityExtension({
|
UtilityExtension({
|
||||||
|
|
@ -184,7 +103,6 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||||
fileHandler,
|
fileHandler,
|
||||||
isEditable: editable,
|
isEditable: editable,
|
||||||
}),
|
}),
|
||||||
CustomColorExtension,
|
|
||||||
...CoreEditorAdditionalExtensions({
|
...CoreEditorAdditionalExtensions({
|
||||||
disabledExtensions,
|
disabledExtensions,
|
||||||
flaggedExtensions,
|
flaggedExtensions,
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,16 @@ declare module "@tiptap/core" {
|
||||||
|
|
||||||
export const CustomHorizontalRule = Node.create<HorizontalRuleOptions>({
|
export const CustomHorizontalRule = Node.create<HorizontalRuleOptions>({
|
||||||
name: CORE_EXTENSIONS.HORIZONTAL_RULE,
|
name: CORE_EXTENSIONS.HORIZONTAL_RULE,
|
||||||
|
group: "block",
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
HTMLAttributes: {},
|
HTMLAttributes: {
|
||||||
|
class: "py-4 border-custom-border-400",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
group: "block",
|
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
43
packages/editor/src/core/extensions/placeholder.ts
Normal file
43
packages/editor/src/core/extensions/placeholder.ts
Normal file
|
|
@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -4,7 +4,6 @@ import TaskItem from "@tiptap/extension-task-item";
|
||||||
import TaskList from "@tiptap/extension-task-list";
|
import TaskList from "@tiptap/extension-task-list";
|
||||||
import TextStyle from "@tiptap/extension-text-style";
|
import TextStyle from "@tiptap/extension-text-style";
|
||||||
import TiptapUnderline from "@tiptap/extension-underline";
|
import TiptapUnderline from "@tiptap/extension-underline";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
|
||||||
import { Markdown } from "tiptap-markdown";
|
import { Markdown } from "tiptap-markdown";
|
||||||
// extensions
|
// extensions
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,8 +24,6 @@ import {
|
||||||
UtilityExtension,
|
UtilityExtension,
|
||||||
ImageExtension,
|
ImageExtension,
|
||||||
} from "@/extensions";
|
} from "@/extensions";
|
||||||
// helpers
|
|
||||||
import { isValidHttpUrl } from "@/helpers/common";
|
|
||||||
// plane editor extensions
|
// plane editor extensions
|
||||||
import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions";
|
||||||
// types
|
// types
|
||||||
|
|
@ -34,6 +31,7 @@ import type { IReadOnlyEditorProps } from "@/types";
|
||||||
// local imports
|
// local imports
|
||||||
import { CustomImageExtension } from "./custom-image/extension";
|
import { CustomImageExtension } from "./custom-image/extension";
|
||||||
import { EmojiExtension } from "./emoji/extension";
|
import { EmojiExtension } from "./emoji/extension";
|
||||||
|
import { CustomStarterKitExtension } from "./starter-kit";
|
||||||
|
|
||||||
type Props = Pick<IReadOnlyEditorProps, "disabledExtensions" | "flaggedExtensions" | "fileHandler" | "mentionHandler">;
|
type Props = Pick<IReadOnlyEditorProps, "disabledExtensions" | "flaggedExtensions" | "fileHandler" | "mentionHandler">;
|
||||||
|
|
||||||
|
|
@ -41,57 +39,13 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||||
const { disabledExtensions, fileHandler, flaggedExtensions, mentionHandler } = props;
|
const { disabledExtensions, fileHandler, flaggedExtensions, mentionHandler } = props;
|
||||||
|
|
||||||
const extensions = [
|
const extensions = [
|
||||||
StarterKit.configure({
|
CustomStarterKitExtension({
|
||||||
bulletList: {
|
enableHistory: false,
|
||||||
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,
|
|
||||||
}),
|
}),
|
||||||
EmojiExtension,
|
EmojiExtension,
|
||||||
CustomQuoteExtension,
|
CustomQuoteExtension,
|
||||||
CustomHorizontalRule.configure({
|
CustomHorizontalRule,
|
||||||
HTMLAttributes: {
|
CustomLinkExtension,
|
||||||
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",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
CustomTypographyExtension,
|
CustomTypographyExtension,
|
||||||
TiptapUnderline,
|
TiptapUnderline,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
|
|
@ -106,11 +60,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||||
},
|
},
|
||||||
nested: true,
|
nested: true,
|
||||||
}),
|
}),
|
||||||
CustomCodeBlockExtension.configure({
|
CustomCodeBlockExtension,
|
||||||
HTMLAttributes: {
|
|
||||||
class: "",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
CustomCodeInlineExtension,
|
CustomCodeInlineExtension,
|
||||||
Markdown.configure({
|
Markdown.configure({
|
||||||
html: true,
|
html: true,
|
||||||
|
|
|
||||||
46
packages/editor/src/core/extensions/starter-kit.ts
Normal file
46
packages/editor/src/core/extensions/starter-kit.ts
Normal file
|
|
@ -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 }),
|
||||||
|
});
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue