diff --git a/packages/editor/src/ce/extensions/core/extensions.ts b/packages/editor/src/ce/extensions/core/extensions.ts deleted file mode 100644 index 8f4fd4185..000000000 --- a/packages/editor/src/ce/extensions/core/extensions.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Extensions } from "@tiptap/core"; - -export const CoreEditorAdditionalExtensions = (): Extensions => []; diff --git a/packages/editor/src/ce/extensions/core/index.ts b/packages/editor/src/ce/extensions/core/index.ts deleted file mode 100644 index 9ffc978c3..000000000 --- a/packages/editor/src/ce/extensions/core/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./extensions"; -export * from "./read-only-extensions"; diff --git a/packages/editor/src/ce/extensions/core/read-only-extensions.ts b/packages/editor/src/ce/extensions/core/read-only-extensions.ts deleted file mode 100644 index b7789af73..000000000 --- a/packages/editor/src/ce/extensions/core/read-only-extensions.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Extensions } from "@tiptap/core"; - -export const CoreReadOnlyEditorAdditionalExtensions = (): Extensions => []; diff --git a/packages/editor/src/ce/extensions/core/without-props.ts b/packages/editor/src/ce/extensions/core/without-props.ts deleted file mode 100644 index 0debff0ea..000000000 --- a/packages/editor/src/ce/extensions/core/without-props.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Extensions } from "@tiptap/core"; - -export const CoreEditorAdditionalExtensionsWithoutProps: Extensions = []; diff --git a/packages/editor/src/ce/extensions/index.ts b/packages/editor/src/ce/extensions/index.ts index b25cbcfc7..4a975b8c5 100644 --- a/packages/editor/src/ce/extensions/index.ts +++ b/packages/editor/src/ce/extensions/index.ts @@ -1,2 +1 @@ -export * from "./core"; export * from "./document-extensions"; diff --git a/packages/editor/src/ce/types/editor.ts b/packages/editor/src/ce/types/editor.ts deleted file mode 100644 index 52ef2f743..000000000 --- a/packages/editor/src/ce/types/editor.ts +++ /dev/null @@ -1 +0,0 @@ -export type TEditorAdditionalCommands = never; diff --git a/packages/editor/src/ce/types/index.ts b/packages/editor/src/ce/types/index.ts index 8b6f01847..f30596cb0 100644 --- a/packages/editor/src/ce/types/index.ts +++ b/packages/editor/src/ce/types/index.ts @@ -1,2 +1 @@ -export * from "./editor"; export * from "./issue-embed"; diff --git a/packages/editor/src/core/extensions/callout/block.tsx b/packages/editor/src/core/extensions/callout/block.tsx new file mode 100644 index 000000000..b6c6d7991 --- /dev/null +++ b/packages/editor/src/core/extensions/callout/block.tsx @@ -0,0 +1,56 @@ +import React, { useState } from "react"; +import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react"; +// constants +import { COLORS_LIST } from "@/constants/common"; +// local components +import { CalloutBlockColorSelector } from "./color-selector"; +import { CalloutBlockLogoSelector } from "./logo-selector"; +// types +import { EAttributeNames, TCalloutBlockAttributes } from "./types"; +// utils +import { updateStoredBackgroundColor } from "./utils"; + +type Props = NodeViewProps & { + node: NodeViewProps["node"] & { + attrs: TCalloutBlockAttributes; + }; + updateAttributes: (attrs: Partial) => void; +}; + +export const CustomCalloutBlock: React.FC = (props) => { + const { editor, node, updateAttributes } = props; + // states + const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); + const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); + // derived values + const activeBackgroundColor = COLORS_LIST.find((c) => node.attrs["data-background"] === c.key)?.backgroundColor; + + return ( + + setIsEmojiPickerOpen(val)} + updateAttributes={updateAttributes} + /> + setIsColorPickerOpen((prev) => !prev)} + onSelect={(val) => { + updateAttributes({ + [EAttributeNames.BACKGROUND]: val, + }); + updateStoredBackgroundColor(val); + }} + /> + + + ); +}; diff --git a/packages/editor/src/core/extensions/callout/color-selector.tsx b/packages/editor/src/core/extensions/callout/color-selector.tsx new file mode 100644 index 000000000..489b05166 --- /dev/null +++ b/packages/editor/src/core/extensions/callout/color-selector.tsx @@ -0,0 +1,75 @@ +import { Ban, ChevronDown } from "lucide-react"; +// constants +import { COLORS_LIST } from "@/constants/common"; +// helpers +import { cn } from "@/helpers/common"; + +type Props = { + disabled: boolean; + isOpen: boolean; + onSelect: (color: string | null) => void; + toggleDropdown: () => void; +}; + +export const CalloutBlockColorSelector: React.FC = (props) => { + const { disabled, isOpen, onSelect, toggleDropdown } = props; + + const handleColorSelect = (val: string | null) => { + onSelect(val); + toggleDropdown(); + }; + + return ( +
+
+ + {isOpen && ( +
+
+ {COLORS_LIST.map((color) => ( + +
+
+ )} +
+
+ ); +}; diff --git a/packages/editor/src/core/extensions/callout/extension-config.ts b/packages/editor/src/core/extensions/callout/extension-config.ts new file mode 100644 index 000000000..546311509 --- /dev/null +++ b/packages/editor/src/core/extensions/callout/extension-config.ts @@ -0,0 +1,72 @@ +import { Node, mergeAttributes } from "@tiptap/core"; +import { Node as NodeType } from "@tiptap/pm/model"; +import { MarkdownSerializerState } from "@tiptap/pm/markdown"; +// types +import { EAttributeNames, TCalloutBlockAttributes } from "./types"; +// utils +import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES } from "./utils"; + +// Extend Tiptap's Commands interface +declare module "@tiptap/core" { + interface Commands { + calloutComponent: { + insertCallout: () => ReturnType; + }; + } +} + +export const CustomCalloutExtensionConfig = Node.create({ + name: "calloutComponent", + group: "block", + content: "block+", + + addAttributes() { + const attributes = { + // Reduce instead of map to accumulate the attributes directly into an object + ...Object.values(EAttributeNames).reduce((acc, value) => { + acc[value] = { + default: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[value], + }; + return acc; + }, {}), + }; + return attributes; + }, + + addStorage() { + return { + markdown: { + serialize(state: MarkdownSerializerState, node: NodeType) { + const attrs = node.attrs as TCalloutBlockAttributes; + const logoInUse = attrs["data-logo-in-use"]; + // add callout logo + if (logoInUse === "emoji") { + state.write( + `> ${attrs[\n` + ); + } else { + state.write(`> ${attrs["data-icon-name"]} icon\n`); + } + // add an empty line after the logo + state.write("> \n"); + // add '> ' before each line of the callout content + state.wrapBlock("> ", null, node, () => state.renderContent(node)); + state.closeBlock(node); + }, + }, + }; + }, + + parseHTML() { + return [ + { + tag: `div[${EAttributeNames.BLOCK_TYPE}="${DEFAULT_CALLOUT_BLOCK_ATTRIBUTES[EAttributeNames.BLOCK_TYPE]}"]`, + }, + ]; + }, + + // Render HTML for the callout node + renderHTML({ HTMLAttributes }) { + return ["div", mergeAttributes(HTMLAttributes), 0]; + }, +}); diff --git a/packages/editor/src/core/extensions/callout/extension.tsx b/packages/editor/src/core/extensions/callout/extension.tsx new file mode 100644 index 000000000..a83964b37 --- /dev/null +++ b/packages/editor/src/core/extensions/callout/extension.tsx @@ -0,0 +1,68 @@ +import { findParentNodeClosestToPos, Predicate, ReactNodeViewRenderer } from "@tiptap/react"; +// extensions +import { CustomCalloutBlock } from "@/extensions"; +// helpers +import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-paragraph-at-node-boundary"; +// config +import { CustomCalloutExtensionConfig } from "./extension-config"; +// utils +import { getStoredBackgroundColor, getStoredLogo } from "./utils"; + +export const CustomCalloutExtension = CustomCalloutExtensionConfig.extend({ + selectable: true, + draggable: true, + + addCommands() { + return { + insertCallout: + () => + ({ commands }) => { + // get stored logo values and background color from the local storage + const storedLogoValues = getStoredLogo(); + const storedBackgroundValue = getStoredBackgroundColor(); + + return commands.insertContent({ + type: this.name, + content: [ + { + type: "paragraph", + }, + ], + attrs: { + ...storedLogoValues, + "data-background": storedBackgroundValue, + }, + }); + }, + }; + }, + + addKeyboardShortcuts() { + return { + Backspace: ({ editor }) => { + const { $from, empty } = editor.state.selection; + try { + const isParentNodeCallout: Predicate = (node) => node.type === this.type; + const parentNodeDetails = findParentNodeClosestToPos($from, isParentNodeCallout); + // Check if selection is empty and at the beginning of the callout + if (empty && parentNodeDetails) { + const isCursorAtCalloutBeginning = $from.pos === parentNodeDetails.start + 1; + if (parentNodeDetails.node.content.size > 2 && isCursorAtCalloutBeginning) { + editor.commands.setTextSelection(parentNodeDetails.pos - 1); + return true; + } + } + } catch (error) { + console.error("Error in performing backspace action on callout", error); + } + return false; // Allow the default behavior if conditions are not met + }, + ArrowDown: insertEmptyParagraphAtNodeBoundaries("down", this.name), + ArrowUp: insertEmptyParagraphAtNodeBoundaries("up", this.name), + }; + }, + + addNodeView() { + return ReactNodeViewRenderer(CustomCalloutBlock); + }, +}); diff --git a/packages/editor/src/core/extensions/callout/index.ts b/packages/editor/src/core/extensions/callout/index.ts new file mode 100644 index 000000000..57915d313 --- /dev/null +++ b/packages/editor/src/core/extensions/callout/index.ts @@ -0,0 +1,3 @@ +export * from "./block"; +export * from "./extension"; +export * from "./read-only-extension"; diff --git a/packages/editor/src/core/extensions/callout/logo-selector.tsx b/packages/editor/src/core/extensions/callout/logo-selector.tsx new file mode 100644 index 000000000..4e9f966af --- /dev/null +++ b/packages/editor/src/core/extensions/callout/logo-selector.tsx @@ -0,0 +1,97 @@ +// plane helpers +import { convertHexEmojiToDecimal } from "@plane/helpers"; +// plane ui +import { EmojiIconPicker, EmojiIconPickerTypes, Logo, TEmojiLogoProps } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common"; +// types +import { TCalloutBlockAttributes } from "./types"; +// utils +import { DEFAULT_CALLOUT_BLOCK_ATTRIBUTES, updateStoredLogo } from "./utils"; + +type Props = { + blockAttributes: TCalloutBlockAttributes; + disabled: boolean; + handleOpen: (val: boolean) => void; + isOpen: boolean; + updateAttributes: (attrs: Partial) => void; +}; + +export const CalloutBlockLogoSelector: React.FC = (props) => { + const { blockAttributes, disabled, handleOpen, isOpen, updateAttributes } = props; + + const logoValue: TEmojiLogoProps = { + in_use: blockAttributes["data-logo-in-use"], + icon: { + color: blockAttributes["data-icon-color"], + name: blockAttributes["data-icon-name"], + }, + emoji: { + value: blockAttributes["data-emoji-unicode"]?.toString(), + url: blockAttributes["data-emoji-url"], + }, + }; + + return ( +
+ } + onChange={(val) => { + // construct the new logo value based on the type of value + let newLogoValue: Partial = {}; + let newLogoValueToStoreInLocalStorage: TEmojiLogoProps = { + in_use: "emoji", + emoji: { + value: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"], + url: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-url"], + }, + }; + if (val.type === "emoji") { + newLogoValue = { + "data-emoji-unicode": convertHexEmojiToDecimal(val.value.unified), + "data-emoji-url": val.value.imageUrl, + }; + newLogoValueToStoreInLocalStorage = { + in_use: "emoji", + emoji: { + value: convertHexEmojiToDecimal(val.value.unified), + url: val.value.imageUrl, + }, + }; + } else if (val.type === "icon") { + newLogoValue = { + "data-icon-name": val.value.name, + "data-icon-color": val.value.color, + }; + newLogoValueToStoreInLocalStorage = { + in_use: "icon", + icon: { + name: val.value.name, + color: val.value.color, + }, + }; + } + // update node attributes + updateAttributes({ + "data-logo-in-use": val.type, + ...newLogoValue, + }); + // update stored logo in local storage + updateStoredLogo(newLogoValueToStoreInLocalStorage); + handleOpen(false); + }} + defaultIconColor={logoValue?.in_use && logoValue.in_use === "icon" ? logoValue?.icon?.color : undefined} + defaultOpen={logoValue.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON} + disabled={disabled} + searchDisabled + /> +
+ ); +}; diff --git a/packages/editor/src/core/extensions/callout/read-only-extension.tsx b/packages/editor/src/core/extensions/callout/read-only-extension.tsx new file mode 100644 index 000000000..ad7dbd50d --- /dev/null +++ b/packages/editor/src/core/extensions/callout/read-only-extension.tsx @@ -0,0 +1,14 @@ +import { ReactNodeViewRenderer } from "@tiptap/react"; +// extensions +import { CustomCalloutBlock } from "@/extensions"; +// config +import { CustomCalloutExtensionConfig } from "./extension-config"; + +export const CustomCalloutReadOnlyExtension = CustomCalloutExtensionConfig.extend({ + selectable: false, + draggable: false, + + addNodeView() { + return ReactNodeViewRenderer(CustomCalloutBlock); + }, +}); diff --git a/packages/editor/src/core/extensions/callout/types.ts b/packages/editor/src/core/extensions/callout/types.ts new file mode 100644 index 000000000..17c55d9e5 --- /dev/null +++ b/packages/editor/src/core/extensions/callout/types.ts @@ -0,0 +1,26 @@ +export enum EAttributeNames { + ICON_COLOR = "data-icon-color", + ICON_NAME = "data-icon-name", + EMOJI_UNICODE = "data-emoji-unicode", + EMOJI_URL = "data-emoji-url", + LOGO_IN_USE = "data-logo-in-use", + BACKGROUND = "data-background", + BLOCK_TYPE = "data-block-type", +} + +export type TCalloutBlockIconAttributes = { + [EAttributeNames.ICON_COLOR]: string | undefined; + [EAttributeNames.ICON_NAME]: string | undefined; +}; + +export type TCalloutBlockEmojiAttributes = { + [EAttributeNames.EMOJI_UNICODE]: string | undefined; + [EAttributeNames.EMOJI_URL]: string | undefined; +}; + +export type TCalloutBlockAttributes = { + [EAttributeNames.LOGO_IN_USE]: "emoji" | "icon"; + [EAttributeNames.BACKGROUND]: string; + [EAttributeNames.BLOCK_TYPE]: "callout-component"; +} & TCalloutBlockIconAttributes & + TCalloutBlockEmojiAttributes; diff --git a/packages/editor/src/core/extensions/callout/utils.ts b/packages/editor/src/core/extensions/callout/utils.ts new file mode 100644 index 000000000..c450cbdd2 --- /dev/null +++ b/packages/editor/src/core/extensions/callout/utils.ts @@ -0,0 +1,85 @@ +// plane helpers +import { sanitizeHTML } from "@plane/helpers"; +// plane ui +import { TEmojiLogoProps } from "@plane/ui"; +// types +import { + EAttributeNames, + TCalloutBlockAttributes, + TCalloutBlockEmojiAttributes, + TCalloutBlockIconAttributes, +} from "./types"; + +export const DEFAULT_CALLOUT_BLOCK_ATTRIBUTES: TCalloutBlockAttributes = { + "data-logo-in-use": "emoji", + "data-icon-color": null, + "data-icon-name": null, + "data-emoji-unicode": "128161", + "data-emoji-url": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png", + "data-background": null, + "data-block-type": "callout-component", +}; + +type TStoredLogoValue = Pick & + (TCalloutBlockEmojiAttributes | TCalloutBlockIconAttributes); + +// function to get the stored logo from local storage +export const getStoredLogo = (): TStoredLogoValue => { + const fallBackValues: TStoredLogoValue = { + "data-logo-in-use": "emoji", + "data-emoji-unicode": DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"], + "data-emoji-url": DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-url"], + }; + + if (typeof window !== "undefined") { + const storedData = sanitizeHTML(localStorage.getItem("editor-calloutComponent-logo")); + if (storedData) { + let parsedData: TEmojiLogoProps; + try { + parsedData = JSON.parse(storedData); + } catch (error) { + console.error(`Error parsing stored callout logo, stored value- ${storedData}`, error); + localStorage.removeItem("editor-calloutComponent-logo"); + return fallBackValues; + } + if (parsedData.in_use === "emoji" && parsedData.emoji?.value) { + return { + "data-logo-in-use": "emoji", + "data-emoji-unicode": parsedData.emoji.value || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"], + "data-emoji-url": parsedData.emoji.url || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-url"], + }; + } + if (parsedData.in_use === "icon" && parsedData.icon?.name) { + return { + "data-logo-in-use": "icon", + "data-icon-name": parsedData.icon.name || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-icon-name"], + "data-icon-color": parsedData.icon.color || DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-icon-color"], + }; + } + } + } + // fallback values + return fallBackValues; +}; +// function to update the stored logo on local storage +export const updateStoredLogo = (value: TEmojiLogoProps): void => { + if (typeof window === "undefined") return; + localStorage.setItem("editor-calloutComponent-logo", JSON.stringify(value)); +}; +// function to get the stored background color from local storage +export const getStoredBackgroundColor = (): string | null => { + if (typeof window !== "undefined") { + return sanitizeHTML(localStorage.getItem("editor-calloutComponent-background")); + } + return null; +}; +// function to update the stored background color on local storage +export const updateStoredBackgroundColor = (value: string | null): void => { + if (typeof window === "undefined") return; + if (value === null) { + localStorage.removeItem("editor-calloutComponent-background"); + return; + } else { + localStorage.setItem("editor-calloutComponent-background", value); + } +}; diff --git a/packages/editor/src/core/extensions/core-without-props.ts b/packages/editor/src/core/extensions/core-without-props.ts index b1d53aa34..075d90f2d 100644 --- a/packages/editor/src/core/extensions/core-without-props.ts +++ b/packages/editor/src/core/extensions/core-without-props.ts @@ -1,4 +1,3 @@ -import { Extensions } from "@tiptap/core"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import TextStyle from "@tiptap/extension-text-style"; @@ -18,10 +17,10 @@ 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"; -import { CoreEditorAdditionalExtensionsWithoutProps } from "@/plane-editor/extensions/core/without-props"; -export const CoreEditorExtensionsWithoutProps: Extensions = [ +export const CoreEditorExtensionsWithoutProps = [ StarterKit.configure({ bulletList: { HTMLAttributes: { @@ -88,8 +87,8 @@ export const CoreEditorExtensionsWithoutProps: Extensions = [ TableRow, CustomMentionWithoutProps(), CustomTextAlignExtension, + CustomCalloutExtensionConfig, CustomColorExtension, - ...CoreEditorAdditionalExtensionsWithoutProps, ]; export const DocumentEditorExtensionsWithoutProps = [IssueWidgetWithoutProps()]; diff --git a/packages/editor/src/core/extensions/extensions.tsx b/packages/editor/src/core/extensions/extensions.tsx index 96e90a525..959d20e2b 100644 --- a/packages/editor/src/core/extensions/extensions.tsx +++ b/packages/editor/src/core/extensions/extensions.tsx @@ -1,4 +1,3 @@ -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"; @@ -9,6 +8,7 @@ import StarterKit from "@tiptap/starter-kit"; import { Markdown } from "tiptap-markdown"; // extensions import { + CustomCalloutExtension, CustomCodeBlockExtension, CustomCodeInlineExtension, CustomCodeMarkPlugin, @@ -33,8 +33,6 @@ import { import { isValidHttpUrl } from "@/helpers/common"; // types import { IMentionHighlight, IMentionSuggestion, TFileHandler } from "@/types"; -// plane editor extensions -import { CoreEditorAdditionalExtensions } from "@/plane-editor/extensions"; type TArguments = { enableHistory: boolean; @@ -47,7 +45,7 @@ type TArguments = { tabIndex?: number; }; -export const CoreEditorExtensions = (args: TArguments): Extensions => { +export const CoreEditorExtensions = (args: TArguments) => { const { enableHistory, fileHandler, mentionConfig, placeholder, tabIndex } = args; return [ @@ -162,7 +160,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => { }), CharacterCount, CustomTextAlignExtension, + CustomCalloutExtension, CustomColorExtension, - ...CoreEditorAdditionalExtensions(), ]; }; diff --git a/packages/editor/src/core/extensions/index.ts b/packages/editor/src/core/extensions/index.ts index 5821b5ff5..d1fa0ce6d 100644 --- a/packages/editor/src/core/extensions/index.ts +++ b/packages/editor/src/core/extensions/index.ts @@ -1,3 +1,4 @@ +export * from "./callout"; export * from "./code"; export * from "./code-inline"; export * from "./custom-image"; diff --git a/packages/editor/src/core/extensions/read-only-extensions.tsx b/packages/editor/src/core/extensions/read-only-extensions.tsx index 1eb0e8522..2d90592d6 100644 --- a/packages/editor/src/core/extensions/read-only-extensions.tsx +++ b/packages/editor/src/core/extensions/read-only-extensions.tsx @@ -1,4 +1,3 @@ -import { Extensions } from "@tiptap/core"; import CharacterCount from "@tiptap/extension-character-count"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; @@ -23,14 +22,13 @@ import { HeadingListExtension, CustomReadOnlyImageExtension, CustomTextAlignExtension, + CustomCalloutReadOnlyExtension, CustomColorExtension, } from "@/extensions"; // helpers import { isValidHttpUrl } from "@/helpers/common"; // types import { IMentionHighlight, TFileHandler } from "@/types"; -// plane editor extensions -import { CoreReadOnlyEditorAdditionalExtensions } from "@/plane-editor/extensions"; type Props = { fileHandler: Pick; @@ -39,7 +37,7 @@ type Props = { }; }; -export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { +export const CoreReadOnlyEditorExtensions = (props: Props) => { const { fileHandler, mentionConfig } = props; return [ @@ -129,6 +127,6 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => { CustomColorExtension, HeadingListExtension, CustomTextAlignExtension, - ...CoreReadOnlyEditorAdditionalExtensions(), + CustomCalloutReadOnlyExtension, ]; }; diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx index d3fc807d0..c19bda306 100644 --- a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx +++ b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx @@ -12,6 +12,7 @@ import { List, ListOrdered, ListTodo, + MessageSquareText, MinusSquare, Table, TextQuote, @@ -34,20 +35,20 @@ import { toggleTextColor, toggleBackgroundColor, insertImage, + insertCallout, setText, } from "@/helpers/editor-commands"; // types -import { CommandProps, ISlashCommandItem, TSlashCommandSectionKeys } from "@/types"; -import { TSlashCommandAdditionalOption } from "./root"; +import { CommandProps, ISlashCommandItem } from "@/types"; export type TSlashCommandSection = { - key: TSlashCommandSectionKeys; + key: string; title?: string; items: ISlashCommandItem[]; }; export const getSlashCommandFilteredSections = - (additionalOptions?: TSlashCommandAdditionalOption[]) => + (additionalOptions?: ISlashCommandItem[]) => ({ query }: { query: string }): TSlashCommandSection[] => { const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [ { @@ -179,6 +180,15 @@ export const getSlashCommandFilteredSections = searchTerms: ["img", "photo", "picture", "media", "upload"], command: ({ editor, range }: CommandProps) => insertImage({ editor, event: "insert", range }), }, + { + commandKey: "callout", + key: "callout", + title: "Callout", + icon: , + description: "Insert callout", + searchTerms: ["callout", "comment", "message", "info", "alert"], + command: ({ editor, range }: CommandProps) => insertCallout(editor, range), + }, { commandKey: "divider", key: "divider", @@ -191,7 +201,7 @@ export const getSlashCommandFilteredSections = ], }, { - key: "text-colors", + key: "text-color", title: "Colors", items: [ { @@ -232,7 +242,7 @@ export const getSlashCommandFilteredSections = ], }, { - key: "background-colors", + key: "background-color", title: "Background colors", items: [ { @@ -269,18 +279,8 @@ export const getSlashCommandFilteredSections = }, ]; - additionalOptions?.forEach((item) => { - const sectionToPushTo = SLASH_COMMAND_SECTIONS.find((s) => s.key === item.section) ?? SLASH_COMMAND_SECTIONS[0]; - const itemIndexToPushAfter = sectionToPushTo.items.findIndex((i) => i.commandKey === item.pushAfter); - if (itemIndexToPushAfter !== undefined) { - const resolvedIndex = - itemIndexToPushAfter + 1 < sectionToPushTo.items.length - ? itemIndexToPushAfter + 1 - : sectionToPushTo.items.length - 1; - sectionToPushTo.items.splice(resolvedIndex, 0, item); - } else { - sectionToPushTo.items.push(item); - } + additionalOptions?.map((item) => { + SLASH_COMMAND_SECTIONS?.[0]?.items.push(item); }); const filteredSlashSections = SLASH_COMMAND_SECTIONS.map((section) => ({ diff --git a/packages/editor/src/core/extensions/slash-commands/root.tsx b/packages/editor/src/core/extensions/slash-commands/root.tsx index afe670d8f..a99cbc5f9 100644 --- a/packages/editor/src/core/extensions/slash-commands/root.tsx +++ b/packages/editor/src/core/extensions/slash-commands/root.tsx @@ -3,7 +3,7 @@ import { ReactRenderer } from "@tiptap/react"; import Suggestion, { SuggestionOptions } from "@tiptap/suggestion"; import tippy from "tippy.js"; // types -import { ISlashCommandItem, TEditorCommands, TSlashCommandSectionKeys } from "@/types"; +import { ISlashCommandItem } from "@/types"; // components import { getSlashCommandFilteredSections } from "./command-items-list"; import { SlashCommandsMenu, SlashCommandsMenuProps } from "./command-menu"; @@ -12,11 +12,6 @@ export type SlashCommandOptions = { suggestion: Omit; }; -export type TSlashCommandAdditionalOption = ISlashCommandItem & { - section: TSlashCommandSectionKeys; - pushAfter: TEditorCommands; -}; - const Command = Extension.create({ name: "slash-command", addOptions() { @@ -107,7 +102,7 @@ const renderItems = () => { }; }; -export const SlashCommands = (additionalOptions?: TSlashCommandAdditionalOption[]) => +export const SlashCommands = (additionalOptions?: ISlashCommandItem[]) => Command.configure({ suggestion: { items: getSlashCommandFilteredSections(additionalOptions), diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index 8b59360a9..ec593d536 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -189,3 +189,7 @@ 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(); +}; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index b3f9a5c6e..53aae1f26 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -14,8 +14,6 @@ import { TServerHandler, } from "@/types"; import { TTextAlign } from "@/extensions"; -// plane editor types -import { TEditorAdditionalCommands } from "@/plane-editor/types"; export type TEditorCommands = | "text" @@ -41,7 +39,7 @@ export type TEditorCommands = | "text-color" | "background-color" | "text-align" - | TEditorAdditionalCommands; + | "callout"; export type TCommandExtraProps = { image: { @@ -123,7 +121,7 @@ export interface IEditorProps { onEnterKeyPress?: (e?: any) => void; placeholder?: string | ((isFocused: boolean, value: string) => string); tabIndex?: number; - value?: string | null; + value?: string | null; } export interface ILiteTextEditor extends IEditorProps { extensions?: any[]; diff --git a/packages/editor/src/core/types/slash-commands-suggestion.ts b/packages/editor/src/core/types/slash-commands-suggestion.ts index d6dfae076..91c93203a 100644 --- a/packages/editor/src/core/types/slash-commands-suggestion.ts +++ b/packages/editor/src/core/types/slash-commands-suggestion.ts @@ -8,8 +8,6 @@ export type CommandProps = { range: Range; }; -export type TSlashCommandSectionKeys = "general" | "text-colors" | "background-colors"; - export type ISlashCommandItem = { commandKey: TEditorCommands; key: string;