[PULSE-42] feat: text alignment for all editors (#5847)
* feat: text alignment for editors * fix: text alignment types * fix: build errors * fix: build error * fix: toolbar movement post alignment selection * fix: callout type * fix: image node types * chore: add ts error warning
This commit is contained in:
parent
bb311b750f
commit
b4de055463
27 changed files with 641 additions and 324 deletions
|
|
@ -48,6 +48,7 @@
|
|||
"@tiptap/extension-placeholder": "^2.3.0",
|
||||
"@tiptap/extension-task-item": "^2.1.13",
|
||||
"@tiptap/extension-task-list": "^2.1.13",
|
||||
"@tiptap/extension-text-align": "^2.8.0",
|
||||
"@tiptap/extension-text-style": "^2.7.1",
|
||||
"@tiptap/extension-underline": "^2.1.13",
|
||||
"@tiptap/pm": "^2.1.13",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
import { Editor } from "@tiptap/core";
|
||||
import { AlignCenter, AlignLeft, AlignRight, LucideIcon } from "lucide-react";
|
||||
// components
|
||||
import { TextAlignItem } from "@/components/menus";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// types
|
||||
import { TEditorCommands } from "@/types";
|
||||
|
||||
type Props = {
|
||||
editor: Editor;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const TextAlignmentSelector: React.FC<Props> = (props) => {
|
||||
const { editor, onClose } = props;
|
||||
|
||||
const menuItem = TextAlignItem(editor);
|
||||
|
||||
const textAlignmentOptions: {
|
||||
itemKey: TEditorCommands;
|
||||
renderKey: string;
|
||||
icon: LucideIcon;
|
||||
command: () => void;
|
||||
isActive: () => boolean;
|
||||
}[] = [
|
||||
{
|
||||
itemKey: "text-align",
|
||||
renderKey: "text-align-left",
|
||||
icon: AlignLeft,
|
||||
command: () =>
|
||||
menuItem.command({
|
||||
alignment: "left",
|
||||
}),
|
||||
isActive: () =>
|
||||
menuItem.isActive({
|
||||
alignment: "left",
|
||||
}),
|
||||
},
|
||||
{
|
||||
itemKey: "text-align",
|
||||
renderKey: "text-align-center",
|
||||
icon: AlignCenter,
|
||||
command: () =>
|
||||
menuItem.command({
|
||||
alignment: "center",
|
||||
}),
|
||||
isActive: () =>
|
||||
menuItem.isActive({
|
||||
alignment: "center",
|
||||
}),
|
||||
},
|
||||
{
|
||||
itemKey: "text-align",
|
||||
renderKey: "text-align-right",
|
||||
icon: AlignRight,
|
||||
command: () =>
|
||||
menuItem.command({
|
||||
alignment: "right",
|
||||
}),
|
||||
isActive: () =>
|
||||
menuItem.isActive({
|
||||
alignment: "right",
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex gap-0.5 px-2">
|
||||
{textAlignmentOptions.map((item) => (
|
||||
<button
|
||||
key={item.renderKey}
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
item.command();
|
||||
onClose();
|
||||
}}
|
||||
className={cn(
|
||||
"size-7 grid place-items-center rounded text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 transition-colors",
|
||||
{
|
||||
"bg-custom-background-80 text-custom-text-100": item.isActive(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon className="size-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -16,8 +16,8 @@ type Props = {
|
|||
export const BubbleMenuColorSelector: FC<Props> = (props) => {
|
||||
const { editor, isOpen, setIsOpen } = props;
|
||||
|
||||
const activeTextColor = COLORS_LIST.find((c) => TextColorItem(editor).isActive(c.key));
|
||||
const activeBackgroundColor = COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive(c.key));
|
||||
const activeTextColor = COLORS_LIST.find((c) => TextColorItem(editor).isActive({ color: c.key }));
|
||||
const activeBackgroundColor = COLORS_LIST.find((c) => BackgroundColorItem(editor).isActive({ color: c.key }));
|
||||
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
|
|
@ -64,7 +64,7 @@ export const BubbleMenuColorSelector: FC<Props> = (props) => {
|
|||
style={{
|
||||
backgroundColor: color.textColor,
|
||||
}}
|
||||
onClick={() => TextColorItem(editor).command(color.key)}
|
||||
onClick={() => TextColorItem(editor).command({ color: color.key })}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
|
|
@ -87,7 +87,7 @@ export const BubbleMenuColorSelector: FC<Props> = (props) => {
|
|||
style={{
|
||||
backgroundColor: color.backgroundColor,
|
||||
}}
|
||||
onClick={() => BackgroundColorItem(editor).command(color.key)}
|
||||
onClick={() => BackgroundColorItem(editor).command({ color: color.key })}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import {
|
|||
} from "@/components/menus";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// types
|
||||
import { TEditorCommands } from "@/types";
|
||||
|
||||
type Props = {
|
||||
editor: Editor;
|
||||
|
|
@ -29,7 +31,7 @@ type Props = {
|
|||
export const BubbleMenuNodeSelector: FC<Props> = (props) => {
|
||||
const { editor, isOpen, setIsOpen } = props;
|
||||
|
||||
const items: EditorMenuItem[] = [
|
||||
const items: EditorMenuItem<TEditorCommands>[] = [
|
||||
TextItem(editor),
|
||||
HeadingOneItem(editor),
|
||||
HeadingTwoItem(editor),
|
||||
|
|
@ -44,7 +46,7 @@ export const BubbleMenuNodeSelector: FC<Props> = (props) => {
|
|||
CodeItem(editor),
|
||||
];
|
||||
|
||||
const activeItem = items.filter((item) => item.isActive("")).pop() ?? {
|
||||
const activeItem = items.filter((item) => item.isActive()).pop() ?? {
|
||||
name: "Multiple",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { FC, useEffect, useState } from "react";
|
||||
import { BubbleMenu, BubbleMenuProps, isNodeSelection } from "@tiptap/react";
|
||||
import { BubbleMenu, BubbleMenuProps, Editor, isNodeSelection } from "@tiptap/react";
|
||||
// components
|
||||
import {
|
||||
BoldItem,
|
||||
|
|
@ -7,7 +7,6 @@ import {
|
|||
BubbleMenuLinkSelector,
|
||||
BubbleMenuNodeSelector,
|
||||
CodeItem,
|
||||
EditorMenuItem,
|
||||
ItalicItem,
|
||||
StrikeThroughItem,
|
||||
UnderLineItem,
|
||||
|
|
@ -16,6 +15,8 @@ import {
|
|||
import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common";
|
||||
// local components
|
||||
import { TextAlignmentSelector } from "./alignment-selector";
|
||||
|
||||
type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children">;
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
||||
const [isSelecting, setIsSelecting] = useState(false);
|
||||
|
||||
const items: EditorMenuItem[] = props.editor.isActive("code")
|
||||
const basicFormattingOptions = props.editor.isActive("code")
|
||||
? [CodeItem(props.editor)]
|
||||
: [BoldItem(props.editor), ItalicItem(props.editor), UnderLineItem(props.editor), StrikeThroughItem(props.editor)];
|
||||
|
||||
|
|
@ -132,7 +133,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||
)}
|
||||
</div>
|
||||
<div className="flex gap-0.5 px-2">
|
||||
{items.map((item) => (
|
||||
{basicFormattingOptions.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
|
|
@ -151,6 +152,15 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||
</button>
|
||||
))}
|
||||
</div>
|
||||
<TextAlignmentSelector
|
||||
editor={props.editor}
|
||||
onClose={() => {
|
||||
const editor = props.editor as Editor;
|
||||
if (!editor) return;
|
||||
const pos = editor.state.selection.to;
|
||||
editor.commands.setTextSelection(pos ?? 0);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BubbleMenu>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Selection } from "@tiptap/pm/state";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import {
|
||||
BoldIcon,
|
||||
|
|
@ -22,6 +21,7 @@ import {
|
|||
LucideIcon,
|
||||
MinusSquare,
|
||||
Palette,
|
||||
AlignCenter,
|
||||
} from "lucide-react";
|
||||
// helpers
|
||||
import {
|
||||
|
|
@ -29,6 +29,7 @@ import {
|
|||
insertImage,
|
||||
insertTableCommand,
|
||||
setText,
|
||||
setTextAlign,
|
||||
toggleBackgroundColor,
|
||||
toggleBlockquote,
|
||||
toggleBold,
|
||||
|
|
@ -48,24 +49,20 @@ import {
|
|||
toggleUnderline,
|
||||
} from "@/helpers/editor-commands";
|
||||
// types
|
||||
import { TColorEditorCommands, TNonColorEditorCommands } from "@/types";
|
||||
import { TCommandWithProps, TEditorCommands } from "@/types";
|
||||
|
||||
export type EditorMenuItem = {
|
||||
type isActiveFunction<T extends TEditorCommands> = (params?: TCommandWithProps<T>) => boolean;
|
||||
type commandFunction<T extends TEditorCommands> = (params?: TCommandWithProps<T>) => void;
|
||||
|
||||
export type EditorMenuItem<T extends TEditorCommands> = {
|
||||
key: T;
|
||||
name: string;
|
||||
command: (...args: any) => void;
|
||||
command: commandFunction<T>;
|
||||
icon: LucideIcon;
|
||||
} & (
|
||||
| {
|
||||
key: TNonColorEditorCommands;
|
||||
isActive: () => boolean;
|
||||
}
|
||||
| {
|
||||
key: TColorEditorCommands;
|
||||
isActive: (color: string | undefined) => boolean;
|
||||
}
|
||||
);
|
||||
isActive: isActiveFunction<T>;
|
||||
};
|
||||
|
||||
export const TextItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const TextItem = (editor: Editor): EditorMenuItem<"text"> => ({
|
||||
key: "text",
|
||||
name: "Text",
|
||||
isActive: () => editor.isActive("paragraph"),
|
||||
|
|
@ -73,7 +70,7 @@ export const TextItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: CaseSensitive,
|
||||
});
|
||||
|
||||
export const HeadingOneItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const HeadingOneItem = (editor: Editor): EditorMenuItem<"h1"> => ({
|
||||
key: "h1",
|
||||
name: "Heading 1",
|
||||
isActive: () => editor.isActive("heading", { level: 1 }),
|
||||
|
|
@ -81,7 +78,7 @@ export const HeadingOneItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: Heading1,
|
||||
});
|
||||
|
||||
export const HeadingTwoItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const HeadingTwoItem = (editor: Editor): EditorMenuItem<"h2"> => ({
|
||||
key: "h2",
|
||||
name: "Heading 2",
|
||||
isActive: () => editor.isActive("heading", { level: 2 }),
|
||||
|
|
@ -89,7 +86,7 @@ export const HeadingTwoItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: Heading2,
|
||||
});
|
||||
|
||||
export const HeadingThreeItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const HeadingThreeItem = (editor: Editor): EditorMenuItem<"h3"> => ({
|
||||
key: "h3",
|
||||
name: "Heading 3",
|
||||
isActive: () => editor.isActive("heading", { level: 3 }),
|
||||
|
|
@ -97,7 +94,7 @@ export const HeadingThreeItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: Heading3,
|
||||
});
|
||||
|
||||
export const HeadingFourItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const HeadingFourItem = (editor: Editor): EditorMenuItem<"h4"> => ({
|
||||
key: "h4",
|
||||
name: "Heading 4",
|
||||
isActive: () => editor.isActive("heading", { level: 4 }),
|
||||
|
|
@ -105,7 +102,7 @@ export const HeadingFourItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: Heading4,
|
||||
});
|
||||
|
||||
export const HeadingFiveItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const HeadingFiveItem = (editor: Editor): EditorMenuItem<"h5"> => ({
|
||||
key: "h5",
|
||||
name: "Heading 5",
|
||||
isActive: () => editor.isActive("heading", { level: 5 }),
|
||||
|
|
@ -113,7 +110,7 @@ export const HeadingFiveItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: Heading5,
|
||||
});
|
||||
|
||||
export const HeadingSixItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const HeadingSixItem = (editor: Editor): EditorMenuItem<"h6"> => ({
|
||||
key: "h6",
|
||||
name: "Heading 6",
|
||||
isActive: () => editor.isActive("heading", { level: 6 }),
|
||||
|
|
@ -121,7 +118,7 @@ export const HeadingSixItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: Heading6,
|
||||
});
|
||||
|
||||
export const BoldItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const BoldItem = (editor: Editor): EditorMenuItem<"bold"> => ({
|
||||
key: "bold",
|
||||
name: "Bold",
|
||||
isActive: () => editor?.isActive("bold"),
|
||||
|
|
@ -129,7 +126,7 @@ export const BoldItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: BoldIcon,
|
||||
});
|
||||
|
||||
export const ItalicItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const ItalicItem = (editor: Editor): EditorMenuItem<"italic"> => ({
|
||||
key: "italic",
|
||||
name: "Italic",
|
||||
isActive: () => editor?.isActive("italic"),
|
||||
|
|
@ -137,7 +134,7 @@ export const ItalicItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: ItalicIcon,
|
||||
});
|
||||
|
||||
export const UnderLineItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const UnderLineItem = (editor: Editor): EditorMenuItem<"underline"> => ({
|
||||
key: "underline",
|
||||
name: "Underline",
|
||||
isActive: () => editor?.isActive("underline"),
|
||||
|
|
@ -145,7 +142,7 @@ export const UnderLineItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: UnderlineIcon,
|
||||
});
|
||||
|
||||
export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const StrikeThroughItem = (editor: Editor): EditorMenuItem<"strikethrough"> => ({
|
||||
key: "strikethrough",
|
||||
name: "Strikethrough",
|
||||
isActive: () => editor?.isActive("strike"),
|
||||
|
|
@ -153,7 +150,7 @@ export const StrikeThroughItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: StrikethroughIcon,
|
||||
});
|
||||
|
||||
export const BulletListItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const BulletListItem = (editor: Editor): EditorMenuItem<"bulleted-list"> => ({
|
||||
key: "bulleted-list",
|
||||
name: "Bulleted list",
|
||||
isActive: () => editor?.isActive("bulletList"),
|
||||
|
|
@ -161,7 +158,7 @@ export const BulletListItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: ListIcon,
|
||||
});
|
||||
|
||||
export const NumberedListItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const NumberedListItem = (editor: Editor): EditorMenuItem<"numbered-list"> => ({
|
||||
key: "numbered-list",
|
||||
name: "Numbered list",
|
||||
isActive: () => editor?.isActive("orderedList"),
|
||||
|
|
@ -169,7 +166,7 @@ export const NumberedListItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: ListOrderedIcon,
|
||||
});
|
||||
|
||||
export const TodoListItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const TodoListItem = (editor: Editor): EditorMenuItem<"to-do-list"> => ({
|
||||
key: "to-do-list",
|
||||
name: "To-do list",
|
||||
isActive: () => editor.isActive("taskItem"),
|
||||
|
|
@ -177,7 +174,7 @@ export const TodoListItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: CheckSquare,
|
||||
});
|
||||
|
||||
export const QuoteItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const QuoteItem = (editor: Editor): EditorMenuItem<"quote"> => ({
|
||||
key: "quote",
|
||||
name: "Quote",
|
||||
isActive: () => editor?.isActive("blockquote"),
|
||||
|
|
@ -185,7 +182,7 @@ export const QuoteItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: TextQuote,
|
||||
});
|
||||
|
||||
export const CodeItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const CodeItem = (editor: Editor): EditorMenuItem<"code"> => ({
|
||||
key: "code",
|
||||
name: "Code",
|
||||
isActive: () => editor?.isActive("code") || editor?.isActive("codeBlock"),
|
||||
|
|
@ -193,7 +190,7 @@ export const CodeItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: CodeIcon,
|
||||
});
|
||||
|
||||
export const TableItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const TableItem = (editor: Editor): EditorMenuItem<"table"> => ({
|
||||
key: "table",
|
||||
name: "Table",
|
||||
isActive: () => editor?.isActive("table"),
|
||||
|
|
@ -201,14 +198,14 @@ export const TableItem = (editor: Editor): EditorMenuItem => ({
|
|||
icon: TableIcon,
|
||||
});
|
||||
|
||||
export const ImageItem = (editor: Editor) =>
|
||||
({
|
||||
key: "image",
|
||||
name: "Image",
|
||||
isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"),
|
||||
command: (savedSelection: Selection | null) => insertImage({ editor, event: "insert", pos: savedSelection?.from }),
|
||||
icon: ImageIcon,
|
||||
}) as const;
|
||||
export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({
|
||||
key: "image",
|
||||
name: "Image",
|
||||
isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"),
|
||||
command: ({ savedSelection }) =>
|
||||
insertImage({ editor, event: "insert", pos: savedSelection?.from ?? editor.state.selection.from }),
|
||||
icon: ImageIcon,
|
||||
});
|
||||
|
||||
export const HorizontalRuleItem = (editor: Editor) =>
|
||||
({
|
||||
|
|
@ -219,23 +216,31 @@ export const HorizontalRuleItem = (editor: Editor) =>
|
|||
icon: MinusSquare,
|
||||
}) as const;
|
||||
|
||||
export const TextColorItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ({
|
||||
key: "text-color",
|
||||
name: "Color",
|
||||
isActive: (color) => editor.isActive("customColor", { color }),
|
||||
command: (color: string) => toggleTextColor(color, editor),
|
||||
isActive: ({ color }) => editor.isActive("customColor", { color }),
|
||||
command: ({ color }) => toggleTextColor(color, editor),
|
||||
icon: Palette,
|
||||
});
|
||||
|
||||
export const BackgroundColorItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const BackgroundColorItem = (editor: Editor): EditorMenuItem<"background-color"> => ({
|
||||
key: "background-color",
|
||||
name: "Background color",
|
||||
isActive: (color) => editor.isActive("customColor", { backgroundColor: color }),
|
||||
command: (color: string) => toggleBackgroundColor(color, editor),
|
||||
isActive: ({ color }) => editor.isActive("customColor", { backgroundColor: color }),
|
||||
command: ({ color }) => toggleBackgroundColor(color, editor),
|
||||
icon: Palette,
|
||||
});
|
||||
|
||||
export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem[] => {
|
||||
export const TextAlignItem = (editor: Editor): EditorMenuItem<"text-align"> => ({
|
||||
key: "text-align",
|
||||
name: "Text align",
|
||||
isActive: ({ alignment }) => editor.isActive({ textAlign: alignment }),
|
||||
command: ({ alignment }) => setTextAlign(alignment, editor),
|
||||
icon: AlignCenter,
|
||||
});
|
||||
|
||||
export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem<TEditorCommands>[] => {
|
||||
if (!editor) return [];
|
||||
|
||||
return [
|
||||
|
|
@ -260,5 +265,6 @@ export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem[] => {
|
|||
HorizontalRuleItem(editor),
|
||||
TextColorItem(editor),
|
||||
BackgroundColorItem(editor),
|
||||
TextAlignItem(editor),
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { IssueWidgetWithoutProps } from "./issue-embed/issue-embed-without-props
|
|||
import { CustomMentionWithoutProps } from "./mentions/mentions-without-props";
|
||||
import { CustomQuoteExtension } from "./quote";
|
||||
import { TableHeader, TableCell, TableRow, Table } from "./table";
|
||||
import { CustomTextAlignExtension } from "./text-align";
|
||||
import { CustomCalloutExtensionConfig } from "./callout/extension-config";
|
||||
import { CustomColorExtension } from "./custom-color";
|
||||
|
||||
|
|
@ -85,6 +86,7 @@ export const CoreEditorExtensionsWithoutProps = [
|
|||
TableCell,
|
||||
TableRow,
|
||||
CustomMentionWithoutProps(),
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutExtensionConfig,
|
||||
CustomColorExtension,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
CustomLinkExtension,
|
||||
CustomMention,
|
||||
CustomQuoteExtension,
|
||||
CustomTextAlignExtension,
|
||||
CustomTypographyExtension,
|
||||
DropHandlerExtension,
|
||||
ImageExtension,
|
||||
|
|
@ -158,6 +159,7 @@ export const CoreEditorExtensions = (args: TArguments) => {
|
|||
includeChildren: true,
|
||||
}),
|
||||
CharacterCount,
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutExtension,
|
||||
CustomColorExtension,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ export * from "./custom-color";
|
|||
export * from "./drop";
|
||||
export * from "./enter-key-extension";
|
||||
export * from "./extensions";
|
||||
export * from "./headers";
|
||||
export * from "./horizontal-rule";
|
||||
export * from "./keymap";
|
||||
export * from "./quote";
|
||||
export * from "./read-only-extensions";
|
||||
export * from "./side-menu";
|
||||
export * from "./slash-commands";
|
||||
export * from "./headers";
|
||||
export * from "./text-align";
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
CustomMention,
|
||||
HeadingListExtension,
|
||||
CustomReadOnlyImageExtension,
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutReadOnlyExtension,
|
||||
CustomColorExtension,
|
||||
} from "@/extensions";
|
||||
|
|
@ -125,6 +126,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props) => {
|
|||
CharacterCount,
|
||||
CustomColorExtension,
|
||||
HeadingListExtension,
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutReadOnlyExtension,
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react
|
|||
import { TSlashCommandSection } from "./command-items-list";
|
||||
import { CommandMenuItem } from "./command-menu-item";
|
||||
|
||||
type Props = {
|
||||
export type SlashCommandsMenuProps = {
|
||||
items: TSlashCommandSection[];
|
||||
command: any;
|
||||
};
|
||||
|
||||
export const SlashCommandsMenu = (props: Props) => {
|
||||
export const SlashCommandsMenu = (props: SlashCommandsMenuProps) => {
|
||||
const { items: sections, command } = props;
|
||||
// states
|
||||
const [selectedIndex, setSelectedIndex] = useState({
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import tippy from "tippy.js";
|
|||
import { ISlashCommandItem } from "@/types";
|
||||
// components
|
||||
import { getSlashCommandFilteredSections } from "./command-items-list";
|
||||
import { SlashCommandsMenu } from "./command-menu";
|
||||
import { SlashCommandsMenu, SlashCommandsMenuProps } from "./command-menu";
|
||||
|
||||
export type SlashCommandOptions = {
|
||||
suggestion: Omit<SuggestionOptions, "editor">;
|
||||
|
|
@ -55,7 +55,7 @@ interface CommandListInstance {
|
|||
}
|
||||
|
||||
const renderItems = () => {
|
||||
let component: ReactRenderer<CommandListInstance, typeof SlashCommandsMenu> | null = null;
|
||||
let component: ReactRenderer<CommandListInstance, SlashCommandsMenuProps> | null = null;
|
||||
let popup: any | null = null;
|
||||
return {
|
||||
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
|
||||
|
|
|
|||
8
packages/editor/src/core/extensions/text-align.ts
Normal file
8
packages/editor/src/core/extensions/text-align.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import TextAlign from "@tiptap/extension-text-align";
|
||||
|
||||
export type TTextAlign = "left" | "center" | "right";
|
||||
|
||||
export const CustomTextAlignExtension = TextAlign.configure({
|
||||
alignments: ["left", "center", "right"],
|
||||
types: ["heading", "paragraph"],
|
||||
});
|
||||
|
|
@ -181,10 +181,14 @@ export const toggleBackgroundColor = (color: string | undefined, editor: Editor,
|
|||
}
|
||||
};
|
||||
|
||||
export const setTextAlign = (alignment: string, editor: Editor) => {
|
||||
editor.chain().focus().setTextAlign(alignment).run();
|
||||
};
|
||||
|
||||
export const insertHorizontalRule = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||
else editor.chain().focus().setHorizontalRule().run();
|
||||
}
|
||||
};
|
||||
export const insertCallout = (editor: Editor, range?: Range) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).insertCallout().run();
|
||||
else editor.chain().focus().insertCallout().run();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { EditorProps } from "@tiptap/pm/view";
|
|||
import { useEditor as useTiptapEditor, Editor } from "@tiptap/react";
|
||||
import * as Y from "yjs";
|
||||
// components
|
||||
import { getEditorMenuItems } from "@/components/menus";
|
||||
import { EditorMenuItem, getEditorMenuItems } from "@/components/menus";
|
||||
// extensions
|
||||
import { CoreEditorExtensions } from "@/extensions";
|
||||
// helpers
|
||||
|
|
@ -155,11 +155,11 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
const item = getEditorMenuItem(itemKey);
|
||||
if (item) {
|
||||
if (item.key === "image") {
|
||||
item.command(savedSelectionRef.current);
|
||||
} else if (itemKey === "text-color" || itemKey === "background-color") {
|
||||
item.command(props.color);
|
||||
(item as EditorMenuItem<"image">).command({
|
||||
savedSelection: savedSelectionRef.current,
|
||||
});
|
||||
} else {
|
||||
item.command();
|
||||
item.command(props);
|
||||
}
|
||||
} else {
|
||||
console.warn(`No command found for item: ${itemKey}`);
|
||||
|
|
@ -173,11 +173,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
const item = getEditorMenuItem(itemKey);
|
||||
if (!item) return false;
|
||||
|
||||
if (itemKey === "text-color" || itemKey === "background-color") {
|
||||
return item.isActive(props.color);
|
||||
} else {
|
||||
return item.isActive("");
|
||||
}
|
||||
return item.isActive(props);
|
||||
},
|
||||
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
|
||||
// Subscribe to update event emitted from headers extension
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { JSONContent } from "@tiptap/core";
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
// helpers
|
||||
import { IMarking } from "@/helpers/scroll-to-node";
|
||||
// types
|
||||
|
|
@ -6,14 +7,64 @@ import {
|
|||
IMentionHighlight,
|
||||
IMentionSuggestion,
|
||||
TAIHandler,
|
||||
TColorEditorCommands,
|
||||
TDisplayConfig,
|
||||
TEmbedConfig,
|
||||
TExtensions,
|
||||
TFileHandler,
|
||||
TNonColorEditorCommands,
|
||||
TServerHandler,
|
||||
} from "@/types";
|
||||
import { TTextAlign } from "@/extensions";
|
||||
|
||||
export type TEditorCommands =
|
||||
| "text"
|
||||
| "h1"
|
||||
| "h2"
|
||||
| "h3"
|
||||
| "h4"
|
||||
| "h5"
|
||||
| "h6"
|
||||
| "bold"
|
||||
| "italic"
|
||||
| "underline"
|
||||
| "strikethrough"
|
||||
| "bulleted-list"
|
||||
| "numbered-list"
|
||||
| "to-do-list"
|
||||
| "quote"
|
||||
| "code"
|
||||
| "table"
|
||||
| "image"
|
||||
| "divider"
|
||||
| "issue-embed"
|
||||
| "text-color"
|
||||
| "background-color"
|
||||
| "text-align"
|
||||
| "callout";
|
||||
|
||||
export type TCommandExtraProps = {
|
||||
image: {
|
||||
savedSelection: Selection | null;
|
||||
};
|
||||
"text-color": {
|
||||
color: string | undefined;
|
||||
};
|
||||
"background-color": {
|
||||
color: string | undefined;
|
||||
};
|
||||
"text-align": {
|
||||
alignment: TTextAlign;
|
||||
};
|
||||
};
|
||||
|
||||
// Create a utility type that maps a command to its extra props or an empty object if none are defined
|
||||
export type TCommandWithProps<T extends TEditorCommands> = T extends keyof TCommandExtraProps
|
||||
? TCommandExtraProps[T] // If the command has extra props, include them
|
||||
: object; // Otherwise, just return the command type with no extra props
|
||||
|
||||
type TCommandWithPropsWithItemKey<T extends TEditorCommands> = T extends keyof TCommandExtraProps
|
||||
? { itemKey: T } & TCommandExtraProps[T]
|
||||
: { itemKey: T };
|
||||
|
||||
// editor refs
|
||||
export type EditorReadOnlyRefApi = {
|
||||
getMarkDown: () => string;
|
||||
|
|
@ -39,26 +90,8 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
|
|||
scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void;
|
||||
getCurrentCursorPosition: () => number | undefined;
|
||||
setEditorValueAtCursorPosition: (content: string) => void;
|
||||
executeMenuItemCommand: (
|
||||
props:
|
||||
| {
|
||||
itemKey: TNonColorEditorCommands;
|
||||
}
|
||||
| {
|
||||
itemKey: TColorEditorCommands;
|
||||
color: string | undefined;
|
||||
}
|
||||
) => void;
|
||||
isMenuItemActive: (
|
||||
props:
|
||||
| {
|
||||
itemKey: TNonColorEditorCommands;
|
||||
}
|
||||
| {
|
||||
itemKey: TColorEditorCommands;
|
||||
color: string | undefined;
|
||||
}
|
||||
) => boolean;
|
||||
executeMenuItemCommand: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => void;
|
||||
isMenuItemActive: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => boolean;
|
||||
onStateChange: (callback: () => void) => () => void;
|
||||
setFocusAtPosition: (position: number) => void;
|
||||
isEditorReadyToDiscard: () => boolean;
|
||||
|
|
|
|||
|
|
@ -1,33 +1,7 @@
|
|||
import { CSSProperties } from "react";
|
||||
import { Editor, Range } from "@tiptap/core";
|
||||
|
||||
export type TEditorCommands =
|
||||
| "text"
|
||||
| "h1"
|
||||
| "h2"
|
||||
| "h3"
|
||||
| "h4"
|
||||
| "h5"
|
||||
| "h6"
|
||||
| "bold"
|
||||
| "italic"
|
||||
| "underline"
|
||||
| "strikethrough"
|
||||
| "bulleted-list"
|
||||
| "numbered-list"
|
||||
| "to-do-list"
|
||||
| "quote"
|
||||
| "code"
|
||||
| "table"
|
||||
| "image"
|
||||
| "divider"
|
||||
| "issue-embed"
|
||||
| "text-color"
|
||||
| "background-color"
|
||||
| "callout";
|
||||
|
||||
export type TColorEditorCommands = Extract<TEditorCommands, "text-color" | "background-color">;
|
||||
export type TNonColorEditorCommands = Exclude<TEditorCommands, "text-color" | "background-color">;
|
||||
// types
|
||||
import { TEditorCommands } from "@/types";
|
||||
|
||||
export type CommandProps = {
|
||||
editor: Editor;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue