mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
Compare commits
1 Commits
refactor/i
...
feat/toggl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5fa46929b |
@@ -18,6 +18,7 @@ import {
|
|||||||
CustomLinkExtension,
|
CustomLinkExtension,
|
||||||
CustomMention,
|
CustomMention,
|
||||||
CustomQuoteExtension,
|
CustomQuoteExtension,
|
||||||
|
CustomToggleHeadingExtension,
|
||||||
CustomTypographyExtension,
|
CustomTypographyExtension,
|
||||||
DropHandlerExtension,
|
DropHandlerExtension,
|
||||||
ImageExtension,
|
ImageExtension,
|
||||||
@@ -155,5 +156,6 @@ export const CoreEditorExtensions = (args: TArguments) => {
|
|||||||
}),
|
}),
|
||||||
CharacterCount,
|
CharacterCount,
|
||||||
CustomColorExtension,
|
CustomColorExtension,
|
||||||
|
CustomToggleHeadingExtension,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export * from "./issue-embed";
|
|||||||
export * from "./mentions";
|
export * from "./mentions";
|
||||||
export * from "./slash-commands";
|
export * from "./slash-commands";
|
||||||
export * from "./table";
|
export * from "./table";
|
||||||
|
export * from "./toggle-heading";
|
||||||
export * from "./typography";
|
export * from "./typography";
|
||||||
export * from "./core-without-props";
|
export * from "./core-without-props";
|
||||||
export * from "./custom-code-inline";
|
export * from "./custom-code-inline";
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
HeadingListExtension,
|
HeadingListExtension,
|
||||||
CustomReadOnlyImageExtension,
|
CustomReadOnlyImageExtension,
|
||||||
CustomColorExtension,
|
CustomColorExtension,
|
||||||
|
CustomToggleHeadingReadOnlyExtension,
|
||||||
} from "@/extensions";
|
} from "@/extensions";
|
||||||
// helpers
|
// helpers
|
||||||
import { isValidHttpUrl } from "@/helpers/common";
|
import { isValidHttpUrl } from "@/helpers/common";
|
||||||
@@ -124,5 +125,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
|
|||||||
CharacterCount,
|
CharacterCount,
|
||||||
CustomColorExtension,
|
CustomColorExtension,
|
||||||
HeadingListExtension,
|
HeadingListExtension,
|
||||||
|
CustomToggleHeadingReadOnlyExtension,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Heading6,
|
Heading6,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
List,
|
List,
|
||||||
|
ListCollapse,
|
||||||
ListOrdered,
|
ListOrdered,
|
||||||
ListTodo,
|
ListTodo,
|
||||||
MinusSquare,
|
MinusSquare,
|
||||||
@@ -34,6 +35,7 @@ import {
|
|||||||
toggleTextColor,
|
toggleTextColor,
|
||||||
toggleBackgroundColor,
|
toggleBackgroundColor,
|
||||||
insertImage,
|
insertImage,
|
||||||
|
insertToggleHeading,
|
||||||
} from "@/helpers/editor-commands";
|
} from "@/helpers/editor-commands";
|
||||||
// types
|
// types
|
||||||
import { CommandProps, ISlashCommandItem } from "@/types";
|
import { CommandProps, ISlashCommandItem } from "@/types";
|
||||||
@@ -49,7 +51,8 @@ export const getSlashCommandFilteredSections =
|
|||||||
({ query }: { query: string }): TSlashCommandSection[] => {
|
({ query }: { query: string }): TSlashCommandSection[] => {
|
||||||
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
|
const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [
|
||||||
{
|
{
|
||||||
key: "general",
|
key: "basic",
|
||||||
|
title: "Basic blocks",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
commandKey: "text",
|
commandKey: "text",
|
||||||
@@ -193,6 +196,39 @@ export const getSlashCommandFilteredSections =
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "advanced",
|
||||||
|
title: "Advanced blocks",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
commandKey: "toggle-heading",
|
||||||
|
key: "toggle-heading-1",
|
||||||
|
title: "Toggle heading 1",
|
||||||
|
icon: <ListCollapse className="size-3.5" />,
|
||||||
|
description: "Insert toggle heading 1",
|
||||||
|
searchTerms: ["toggle", "heading", "collapse", "disclosure", "accordion"],
|
||||||
|
command: ({ editor, range }) => insertToggleHeading(1, editor, range),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "toggle-heading",
|
||||||
|
key: "toggle-heading-2",
|
||||||
|
title: "Toggle heading 2",
|
||||||
|
icon: <ListCollapse className="size-3.5" />,
|
||||||
|
description: "Insert toggle heading 2",
|
||||||
|
searchTerms: ["toggle", "heading", "collapse", "disclosure", "accordion"],
|
||||||
|
command: ({ editor, range }) => insertToggleHeading(2, editor, range),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "toggle-heading",
|
||||||
|
key: "toggle-heading-3",
|
||||||
|
title: "Toggle heading 3",
|
||||||
|
icon: <ListCollapse className="size-3.5" />,
|
||||||
|
description: "Insert toggle heading 3",
|
||||||
|
searchTerms: ["toggle", "heading", "collapse", "disclosure", "accordion"],
|
||||||
|
command: ({ editor, range }) => insertToggleHeading(3, editor, range),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "text-color",
|
key: "text-color",
|
||||||
title: "Colors",
|
title: "Colors",
|
||||||
|
|||||||
41
packages/editor/src/core/extensions/toggle-heading/block.tsx
Normal file
41
packages/editor/src/core/extensions/toggle-heading/block.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||||
|
import { ChevronRight } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { TToggleHeadingBlockAttributes } from "./types";
|
||||||
|
import { cn } from "@/helpers/common";
|
||||||
|
|
||||||
|
type Props = NodeViewProps & {
|
||||||
|
node: NodeViewProps["node"] & {
|
||||||
|
attrs: TToggleHeadingBlockAttributes;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomToggleHeadingBlock: React.FC<Props> = (props) => {
|
||||||
|
const { node, updateAttributes } = props;
|
||||||
|
// derived values
|
||||||
|
const headingLevel = Number(node.attrs["data-heading-level"] ?? 1);
|
||||||
|
const isToggleOpen = node.attrs["data-toggle-status"] === "open";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeViewWrapper className="editor-toggle-heading-component flex items-start gap-1 my-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
updateAttributes({
|
||||||
|
"data-toggle-status": isToggleOpen ? "close" : "open",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="flex-shrink-0 size-5 grid place-items-center rounded hover:bg-custom-background-80 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight
|
||||||
|
className={cn("size-3.5 transition-all", {
|
||||||
|
"rotate-90": isToggleOpen,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<NodeViewContent as="div" className="w-full break-words" />
|
||||||
|
</NodeViewWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Node, mergeAttributes } from "@tiptap/core";
|
||||||
|
import { Node as NodeType } from "@tiptap/pm/model";
|
||||||
|
import { MarkdownSerializerState } from "@tiptap/pm/markdown";
|
||||||
|
|
||||||
|
// Extend Tiptap's Commands interface
|
||||||
|
declare module "@tiptap/core" {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
toggleHeadingComponent: {
|
||||||
|
insertToggleHeading: ({ headingLevel }: { headingLevel: number }) => ReturnType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomToggleHeadingExtensionConfig = Node.create({
|
||||||
|
name: "toggleHeadingComponent",
|
||||||
|
group: "block",
|
||||||
|
content: "block+",
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
"data-heading-level": {
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
"data-background-color": {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
"data-toggle-status": {
|
||||||
|
default: "close",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addStorage() {
|
||||||
|
return {
|
||||||
|
markdown: {
|
||||||
|
serialize(state: MarkdownSerializerState, node: NodeType) {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: "toggle-heading-component" }];
|
||||||
|
},
|
||||||
|
|
||||||
|
// Render HTML for the callout node
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ["toggle-heading-component", mergeAttributes(HTMLAttributes)];
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||||
|
// components
|
||||||
|
import { CustomToggleHeadingBlock } from "./block";
|
||||||
|
// config
|
||||||
|
import { CustomToggleHeadingExtensionConfig } from "./extension-config";
|
||||||
|
// utils
|
||||||
|
import { DEFAULT_TOGGLE_HEADING_BLOCK_ATTRIBUTES } from "./utils";
|
||||||
|
|
||||||
|
export const CustomToggleHeadingExtension = CustomToggleHeadingExtensionConfig.extend({
|
||||||
|
selectable: true,
|
||||||
|
draggable: true,
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
insertToggleHeading:
|
||||||
|
({ headingLevel }) =>
|
||||||
|
({ commands }) =>
|
||||||
|
commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
attrs: {
|
||||||
|
level: headingLevel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attrs: {
|
||||||
|
"data-heading-level": headingLevel ?? 1,
|
||||||
|
"data-toggle-status": DEFAULT_TOGGLE_HEADING_BLOCK_ATTRIBUTES["data-toggle-status"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return ReactNodeViewRenderer(CustomToggleHeadingBlock);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./extension";
|
||||||
|
export * from "./read-only-extension";
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { CustomToggleHeadingExtensionConfig } from "./extension-config";
|
||||||
|
|
||||||
|
export const CustomToggleHeadingReadOnlyExtension = CustomToggleHeadingExtensionConfig;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type TToggleHeadingBlockAttributes = {
|
||||||
|
"data-heading-level": number | undefined;
|
||||||
|
"data-background-color": string | undefined;
|
||||||
|
"data-toggle-status": "open" | "close" | undefined;
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// types
|
||||||
|
import { TToggleHeadingBlockAttributes } from "./types";
|
||||||
|
|
||||||
|
export const DEFAULT_TOGGLE_HEADING_BLOCK_ATTRIBUTES: TToggleHeadingBlockAttributes = {
|
||||||
|
"data-heading-level": undefined,
|
||||||
|
"data-background-color": undefined,
|
||||||
|
"data-toggle-status": "close",
|
||||||
|
};
|
||||||
@@ -180,3 +180,8 @@ export const toggleBackgroundColor = (color: string | undefined, editor: Editor,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const insertToggleHeading = (headingLevel: number, editor: Editor, range?: Range) => {
|
||||||
|
if (range) editor.chain().focus().deleteRange(range).insertToggleHeading({ headingLevel }).run();
|
||||||
|
else editor.chain().focus().insertToggleHeading({ headingLevel }).run();
|
||||||
|
};
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
|
|||||||
".issue-embed",
|
".issue-embed",
|
||||||
".image-component",
|
".image-component",
|
||||||
".image-upload-component",
|
".image-upload-component",
|
||||||
|
".editor-toggle-heading-component",
|
||||||
].join(", ");
|
].join(", ");
|
||||||
|
|
||||||
for (const elem of elements) {
|
for (const elem of elements) {
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export type TEditorCommands =
|
|||||||
| "divider"
|
| "divider"
|
||||||
| "issue-embed"
|
| "issue-embed"
|
||||||
| "text-color"
|
| "text-color"
|
||||||
| "background-color";
|
| "background-color"
|
||||||
|
| "toggle-heading";
|
||||||
|
|
||||||
export type TColorEditorCommands = Extract<TEditorCommands, "text-color" | "background-color">;
|
export type TColorEditorCommands = Extract<TEditorCommands, "text-color" | "background-color">;
|
||||||
export type TNonColorEditorCommands = Exclude<TEditorCommands, "text-color" | "background-color">;
|
export type TNonColorEditorCommands = Exclude<TEditorCommands, "text-color" | "background-color">;
|
||||||
|
|||||||
@@ -316,7 +316,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
|||||||
|
|
||||||
/* tailwind typography */
|
/* tailwind typography */
|
||||||
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h1):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 2rem;
|
&:not(:first-child) {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
font-size: var(--font-size-h1);
|
font-size: var(--font-size-h1);
|
||||||
line-height: var(--line-height-h1);
|
line-height: var(--line-height-h1);
|
||||||
@@ -324,7 +327,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h2):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 1.4rem;
|
&:not(:first-child) {
|
||||||
|
margin-top: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
font-size: var(--font-size-h2);
|
font-size: var(--font-size-h2);
|
||||||
line-height: var(--line-height-h2);
|
line-height: var(--line-height-h2);
|
||||||
@@ -332,7 +338,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h3):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 1rem;
|
&:not(:first-child) {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
font-size: var(--font-size-h3);
|
font-size: var(--font-size-h3);
|
||||||
line-height: var(--line-height-h3);
|
line-height: var(--line-height-h3);
|
||||||
@@ -340,7 +349,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h4):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 1rem;
|
&:not(:first-child) {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
font-size: var(--font-size-h4);
|
font-size: var(--font-size-h4);
|
||||||
line-height: var(--line-height-h4);
|
line-height: var(--line-height-h4);
|
||||||
@@ -348,7 +360,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h5):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 1rem;
|
&:not(:first-child) {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
font-size: var(--font-size-h5);
|
font-size: var(--font-size-h5);
|
||||||
line-height: var(--line-height-h5);
|
line-height: var(--line-height-h5);
|
||||||
@@ -356,7 +371,10 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 1rem;
|
&:not(:first-child) {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
font-size: var(--font-size-h6);
|
font-size: var(--font-size-h6);
|
||||||
line-height: var(--line-height-h6);
|
line-height: var(--line-height-h6);
|
||||||
@@ -364,7 +382,14 @@ ul[data-type="taskList"] ul[data-type="taskList"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
.prose :where(p):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
||||||
margin-top: 0.25rem;
|
&:not(:first-child) {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
padding: 3px 0;
|
padding: 3px 0;
|
||||||
font-size: var(--font-size-regular);
|
font-size: var(--font-size-regular);
|
||||||
|
|||||||
Reference in New Issue
Block a user