mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
editor fixes for space (#2119)
This commit is contained in:
parent
8ba482bc9c
commit
6d52707ff5
@ -77,14 +77,16 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||||||
{...bubbleMenuProps}
|
{...bubbleMenuProps}
|
||||||
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
||||||
>
|
>
|
||||||
<NodeSelector
|
{!props.editor.isActive("table") && (
|
||||||
editor={props.editor!}
|
<NodeSelector
|
||||||
isOpen={isNodeSelectorOpen}
|
editor={props.editor!}
|
||||||
setIsOpen={() => {
|
isOpen={isNodeSelectorOpen}
|
||||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
setIsOpen={() => {
|
||||||
setIsLinkSelectorOpen(false);
|
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||||
}}
|
setIsLinkSelectorOpen(false);
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<LinkSelector
|
<LinkSelector
|
||||||
editor={props.editor!!}
|
editor={props.editor!!}
|
||||||
isOpen={isLinkSelectorOpen}
|
isOpen={isLinkSelectorOpen}
|
||||||
|
@ -28,7 +28,10 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
|
|||||||
name: "Text",
|
name: "Text",
|
||||||
icon: TextIcon,
|
icon: TextIcon,
|
||||||
command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
|
command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
|
||||||
isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"),
|
isActive: () =>
|
||||||
|
editor.isActive("paragraph") &&
|
||||||
|
!editor.isActive("bulletList") &&
|
||||||
|
!editor.isActive("orderedList"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "H1",
|
name: "H1",
|
||||||
@ -69,7 +72,8 @@ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen
|
|||||||
{
|
{
|
||||||
name: "Quote",
|
name: "Quote",
|
||||||
icon: TextQuote,
|
icon: TextQuote,
|
||||||
command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
|
command: () =>
|
||||||
|
editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
|
||||||
isActive: () => editor.isActive("blockquote"),
|
isActive: () => editor.isActive("blockquote"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,7 @@ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
|||||||
import { lowlight } from "lowlight/lib/core";
|
import { lowlight } from "lowlight/lib/core";
|
||||||
import SlashCommand from "../slash-command";
|
import SlashCommand from "../slash-command";
|
||||||
import { InputRule } from "@tiptap/core";
|
import { InputRule } from "@tiptap/core";
|
||||||
|
import Gapcursor from "@tiptap/extension-gapcursor";
|
||||||
|
|
||||||
import ts from "highlight.js/lib/languages/typescript";
|
import ts from "highlight.js/lib/languages/typescript";
|
||||||
|
|
||||||
@ -20,6 +21,10 @@ import "highlight.js/styles/github-dark.css";
|
|||||||
import UniqueID from "@tiptap-pro/extension-unique-id";
|
import UniqueID from "@tiptap-pro/extension-unique-id";
|
||||||
import UpdatedImage from "./updated-image";
|
import UpdatedImage from "./updated-image";
|
||||||
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
|
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
|
||||||
|
import { CustomTableCell } from "./table/table-cell";
|
||||||
|
import { Table } from "./table/table";
|
||||||
|
import { TableHeader } from "./table/table-header";
|
||||||
|
import { TableRow } from "@tiptap/extension-table-row";
|
||||||
|
|
||||||
lowlight.registerLanguage("ts", ts);
|
lowlight.registerLanguage("ts", ts);
|
||||||
|
|
||||||
@ -27,113 +32,122 @@ export const TiptapExtensions = (
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||||
) => [
|
) => [
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
bulletList: {
|
bulletList: {
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "list-disc list-outside leading-3 -mt-2",
|
class: "list-disc list-outside leading-3 -mt-2",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
orderedList: {
|
||||||
orderedList: {
|
HTMLAttributes: {
|
||||||
HTMLAttributes: {
|
class: "list-decimal list-outside leading-3 -mt-2",
|
||||||
class: "list-decimal list-outside leading-3 -mt-2",
|
},
|
||||||
},
|
},
|
||||||
},
|
listItem: {
|
||||||
listItem: {
|
HTMLAttributes: {
|
||||||
HTMLAttributes: {
|
class: "leading-normal -mb-2",
|
||||||
class: "leading-normal -mb-2",
|
},
|
||||||
},
|
},
|
||||||
},
|
blockquote: {
|
||||||
blockquote: {
|
HTMLAttributes: {
|
||||||
HTMLAttributes: {
|
class: "border-l-4 border-custom-border-300",
|
||||||
class: "border-l-4 border-custom-border-300",
|
},
|
||||||
},
|
},
|
||||||
},
|
code: {
|
||||||
code: {
|
HTMLAttributes: {
|
||||||
HTMLAttributes: {
|
class:
|
||||||
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
||||||
spellcheck: "false",
|
spellcheck: "false",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
codeBlock: false,
|
||||||
codeBlock: false,
|
horizontalRule: false,
|
||||||
horizontalRule: false,
|
dropcursor: {
|
||||||
dropcursor: {
|
color: "rgba(var(--color-text-100))",
|
||||||
color: "#DBEAFE",
|
width: 2,
|
||||||
width: 2,
|
},
|
||||||
},
|
gapcursor: false,
|
||||||
gapcursor: false,
|
}),
|
||||||
}),
|
CodeBlockLowlight.configure({
|
||||||
CodeBlockLowlight.configure({
|
lowlight,
|
||||||
lowlight,
|
}),
|
||||||
}),
|
HorizontalRule.extend({
|
||||||
HorizontalRule.extend({
|
addInputRules() {
|
||||||
addInputRules() {
|
return [
|
||||||
return [
|
new InputRule({
|
||||||
new InputRule({
|
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
||||||
find: /^(?:---|—-|___\s|\*\*\*\s)$/,
|
handler: ({ state, range, commands }) => {
|
||||||
handler: ({ state, range, commands }) => {
|
commands.splitBlock();
|
||||||
commands.splitBlock();
|
|
||||||
|
|
||||||
const attributes = {};
|
const attributes = {};
|
||||||
const { tr } = state;
|
const { tr } = state;
|
||||||
const start = range.from;
|
const start = range.from;
|
||||||
const end = range.to;
|
const end = range.to;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
tr.replaceWith(start - 1, end, this.type.create(attributes));
|
tr.replaceWith(start - 1, end, this.type.create(attributes));
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
}).configure({
|
}).configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "mb-6 border-t border-custom-border-300",
|
class: "mb-6 border-t border-custom-border-300",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
TiptapLink.configure({
|
Gapcursor,
|
||||||
protocols: ["http", "https"],
|
TiptapLink.configure({
|
||||||
validate: (url) => isValidHttpUrl(url),
|
protocols: ["http", "https"],
|
||||||
HTMLAttributes: {
|
validate: (url) => isValidHttpUrl(url),
|
||||||
class:
|
HTMLAttributes: {
|
||||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
class:
|
||||||
},
|
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||||
}),
|
},
|
||||||
UpdatedImage.configure({
|
}),
|
||||||
HTMLAttributes: {
|
UpdatedImage.configure({
|
||||||
class: "rounded-lg border border-custom-border-300",
|
HTMLAttributes: {
|
||||||
},
|
class: "rounded-lg border border-custom-border-300",
|
||||||
}),
|
},
|
||||||
Placeholder.configure({
|
}),
|
||||||
placeholder: ({ node }) => {
|
Placeholder.configure({
|
||||||
if (node.type.name === "heading") {
|
placeholder: ({ node }) => {
|
||||||
return `Heading ${node.attrs.level}`;
|
if (node.type.name === "heading") {
|
||||||
}
|
return `Heading ${node.attrs.level}`;
|
||||||
|
}
|
||||||
|
if (node.type.name === "image" || node.type.name === "table") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
return "Press '/' for commands...";
|
return "Press '/' for commands...";
|
||||||
},
|
},
|
||||||
includeChildren: true,
|
includeChildren: true,
|
||||||
}),
|
}),
|
||||||
UniqueID.configure({
|
UniqueID.configure({
|
||||||
types: ["image"],
|
types: ["image"],
|
||||||
}),
|
}),
|
||||||
SlashCommand(workspaceSlug, setIsSubmitting),
|
SlashCommand(workspaceSlug, setIsSubmitting),
|
||||||
TiptapUnderline,
|
TiptapUnderline,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
Color,
|
Color,
|
||||||
Highlight.configure({
|
Highlight.configure({
|
||||||
multicolor: true,
|
multicolor: true,
|
||||||
}),
|
}),
|
||||||
TaskList.configure({
|
TaskList.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "not-prose pl-2",
|
class: "not-prose pl-2",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
TaskItem.configure({
|
TaskItem.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "flex items-start my-4",
|
class: "flex items-start my-4",
|
||||||
},
|
},
|
||||||
nested: true,
|
nested: true,
|
||||||
}),
|
}),
|
||||||
Markdown.configure({
|
Markdown.configure({
|
||||||
html: true,
|
html: true,
|
||||||
transformCopiedText: true,
|
transformCopiedText: true,
|
||||||
}),
|
}),
|
||||||
];
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
CustomTableCell,
|
||||||
|
TableRow,
|
||||||
|
];
|
||||||
|
32
space/components/tiptap/extensions/table/table-cell.ts
Normal file
32
space/components/tiptap/extensions/table/table-cell.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { TableCell } from "@tiptap/extension-table-cell";
|
||||||
|
|
||||||
|
export const CustomTableCell = TableCell.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
isHeader: {
|
||||||
|
default: false,
|
||||||
|
parseHTML: (element) => {
|
||||||
|
isHeader: element.tagName === "TD";
|
||||||
|
},
|
||||||
|
renderHTML: (attributes) => {
|
||||||
|
tag: attributes.isHeader ? "th" : "td";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
if (HTMLAttributes.isHeader) {
|
||||||
|
return [
|
||||||
|
"th",
|
||||||
|
{
|
||||||
|
...HTMLAttributes,
|
||||||
|
class: `relative ${HTMLAttributes.class}`,
|
||||||
|
},
|
||||||
|
["span", { class: "absolute top-0 right-0" }],
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return ["td", HTMLAttributes, 0];
|
||||||
|
},
|
||||||
|
});
|
7
space/components/tiptap/extensions/table/table-header.ts
Normal file
7
space/components/tiptap/extensions/table/table-header.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
|
||||||
|
|
||||||
|
const TableHeader = BaseTableHeader.extend({
|
||||||
|
content: "paragraph",
|
||||||
|
});
|
||||||
|
|
||||||
|
export { TableHeader };
|
9
space/components/tiptap/extensions/table/table.ts
Normal file
9
space/components/tiptap/extensions/table/table.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Table as BaseTable } from "@tiptap/extension-table";
|
||||||
|
|
||||||
|
const Table = BaseTable.configure({
|
||||||
|
resizable: true,
|
||||||
|
cellMinWidth: 100,
|
||||||
|
allowTableNodeSelection: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Table };
|
@ -6,6 +6,7 @@ import { EditorBubbleMenu } from "./bubble-menu";
|
|||||||
import { TiptapExtensions } from "./extensions";
|
import { TiptapExtensions } from "./extensions";
|
||||||
import { TiptapEditorProps } from "./props";
|
import { TiptapEditorProps } from "./props";
|
||||||
import { ImageResizer } from "./extensions/image-resize";
|
import { ImageResizer } from "./extensions/image-resize";
|
||||||
|
import { TableMenu } from "./table-menu";
|
||||||
|
|
||||||
export interface ITipTapRichTextEditor {
|
export interface ITipTapRichTextEditor {
|
||||||
value: string;
|
value: string;
|
||||||
@ -37,6 +38,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
|
|||||||
borderOnFocus,
|
borderOnFocus,
|
||||||
customClassName,
|
customClassName,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
editable: editable ?? true,
|
editable: editable ?? true,
|
||||||
editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting),
|
editorProps: TiptapEditorProps(workspaceSlug, setIsSubmitting),
|
||||||
@ -81,8 +83,8 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
|
|||||||
|
|
||||||
const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
|
const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
|
||||||
${noBorder ? "" : "border border-custom-border-200"} ${
|
${noBorder ? "" : "border border-custom-border-200"} ${
|
||||||
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
||||||
} ${customClassName}`;
|
} ${customClassName}`;
|
||||||
|
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
@ -98,6 +100,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
|
|||||||
{editor && <EditorBubbleMenu editor={editor} />}
|
{editor && <EditorBubbleMenu editor={editor} />}
|
||||||
<div className={`${editorContentCustomClassNames}`}>
|
<div className={`${editorContentCustomClassNames}`}>
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
|
<TableMenu editor={editor} />
|
||||||
{editor?.isActive("image") && <ImageResizer editor={editor} />}
|
{editor?.isActive("image") && <ImageResizer editor={editor} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,43 +1,51 @@
|
|||||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state";
|
||||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||||
import fileService from "services/file.service";
|
import fileService from "services/file.service";
|
||||||
|
|
||||||
const deleteKey = new PluginKey("delete-image");
|
const deleteKey = new PluginKey("delete-image");
|
||||||
|
const IMAGE_NODE_TYPE = "image";
|
||||||
|
|
||||||
const TrackImageDeletionPlugin = () =>
|
interface ImageNode extends ProseMirrorNode {
|
||||||
|
attrs: {
|
||||||
|
src: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const TrackImageDeletionPlugin = (): Plugin =>
|
||||||
new Plugin({
|
new Plugin({
|
||||||
key: deleteKey,
|
key: deleteKey,
|
||||||
appendTransaction: (transactions, oldState, newState) => {
|
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
||||||
|
const newImageSources = new Set();
|
||||||
|
newState.doc.descendants((node) => {
|
||||||
|
if (node.type.name === IMAGE_NODE_TYPE) {
|
||||||
|
newImageSources.add(node.attrs.src);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
transactions.forEach((transaction) => {
|
transactions.forEach((transaction) => {
|
||||||
if (!transaction.docChanged) return;
|
if (!transaction.docChanged) return;
|
||||||
|
|
||||||
const removedImages: ProseMirrorNode[] = [];
|
const removedImages: ImageNode[] = [];
|
||||||
|
|
||||||
oldState.doc.descendants((oldNode, oldPos) => {
|
oldState.doc.descendants((oldNode, oldPos) => {
|
||||||
if (oldNode.type.name !== "image") return;
|
if (oldNode.type.name !== IMAGE_NODE_TYPE) return;
|
||||||
|
if (oldPos < 0 || oldPos > newState.doc.content.size) return;
|
||||||
if (!newState.doc.resolve(oldPos).parent) return;
|
if (!newState.doc.resolve(oldPos).parent) return;
|
||||||
|
|
||||||
const newNode = newState.doc.nodeAt(oldPos);
|
const newNode = newState.doc.nodeAt(oldPos);
|
||||||
|
|
||||||
// Check if the node has been deleted or replaced
|
// Check if the node has been deleted or replaced
|
||||||
if (!newNode || newNode.type.name !== "image") {
|
if (!newNode || newNode.type.name !== IMAGE_NODE_TYPE) {
|
||||||
// Check if the node still exists elsewhere in the document
|
if (!newImageSources.has(oldNode.attrs.src)) {
|
||||||
let nodeExists = false;
|
removedImages.push(oldNode as ImageNode);
|
||||||
newState.doc.descendants((node) => {
|
|
||||||
if (node.attrs.id === oldNode.attrs.id) {
|
|
||||||
nodeExists = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!nodeExists) {
|
|
||||||
removedImages.push(oldNode as ProseMirrorNode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
removedImages.forEach((node) => {
|
removedImages.forEach(async (node) => {
|
||||||
const src = node.attrs.src;
|
const src = node.attrs.src;
|
||||||
onNodeDeleted(src);
|
await onNodeDeleted(src);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -47,10 +55,14 @@ const TrackImageDeletionPlugin = () =>
|
|||||||
|
|
||||||
export default TrackImageDeletionPlugin;
|
export default TrackImageDeletionPlugin;
|
||||||
|
|
||||||
async function onNodeDeleted(src: string) {
|
async function onNodeDeleted(src: string): Promise<void> {
|
||||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
try {
|
||||||
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
|
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||||
if (resStatus === 204) {
|
const resStatus = await fileService.deleteImage(assetUrlWithWorkspaceId);
|
||||||
console.log("Image deleted successfully");
|
if (resStatus === 204) {
|
||||||
|
console.log("Image deleted successfully");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting image: ", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
import { EditorState, Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
||||||
import fileService from "services/file.service";
|
import fileService from "services/file.service";
|
||||||
@ -46,7 +45,11 @@ export default UploadImagesPlugin;
|
|||||||
|
|
||||||
function findPlaceholder(state: EditorState, id: {}) {
|
function findPlaceholder(state: EditorState, id: {}) {
|
||||||
const decos = uploadKey.getState(state);
|
const decos = uploadKey.getState(state);
|
||||||
const found = decos.find(undefined, undefined, (spec: { id: number | undefined }) => spec.id == id);
|
const found = decos.find(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
(spec: { id: number | undefined }) => spec.id == id
|
||||||
|
);
|
||||||
return found.length ? found[0].from : null;
|
return found.length ? found[0].from : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,8 +62,6 @@ export async function startImageUpload(
|
|||||||
) {
|
) {
|
||||||
if (!file.type.includes("image/")) {
|
if (!file.type.includes("image/")) {
|
||||||
return;
|
return;
|
||||||
} else if (file.size / 1024 / 1024 > 20) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = {};
|
const id = {};
|
||||||
@ -93,7 +94,9 @@ export async function startImageUpload(
|
|||||||
const imageSrc = typeof src === "object" ? reader.result : src;
|
const imageSrc = typeof src === "object" ? reader.result : src;
|
||||||
|
|
||||||
const node = schema.nodes.image.create({ src: imageSrc });
|
const node = schema.nodes.image.create({ src: imageSrc });
|
||||||
const transaction = view.state.tr.replaceWith(pos, pos, node).setMeta(uploadKey, { remove: { id } });
|
const transaction = view.state.tr
|
||||||
|
.replaceWith(pos, pos, node)
|
||||||
|
.setMeta(uploadKey, { remove: { id } });
|
||||||
view.dispatch(transaction);
|
view.dispatch(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +110,9 @@ const UploadImageHandler = (file: File, workspaceSlug: string): Promise<string>
|
|||||||
formData.append("attributes", JSON.stringify({}));
|
formData.append("attributes", JSON.stringify({}));
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
const imageUrl = await fileService.uploadFile(workspaceSlug, formData).then((response) => response.asset);
|
const imageUrl = await fileService
|
||||||
|
.uploadFile(workspaceSlug, formData)
|
||||||
|
.then((response) => response.asset);
|
||||||
|
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.src = imageUrl;
|
image.src = imageUrl;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
import { startImageUpload } from "./plugins/upload-image";
|
import { startImageUpload } from "./plugins/upload-image";
|
||||||
|
import { findTableAncestor } from "./table-menu";
|
||||||
|
|
||||||
export function TiptapEditorProps(
|
export function TiptapEditorProps(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
@ -21,6 +22,15 @@ export function TiptapEditorProps(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handlePaste: (view, event) => {
|
handlePaste: (view, event) => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const selection: any = window?.getSelection();
|
||||||
|
if (selection.rangeCount !== 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
if (findTableAncestor(range.startContainer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
|
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const file = event.clipboardData.files[0];
|
const file = event.clipboardData.files[0];
|
||||||
@ -31,6 +41,15 @@ export function TiptapEditorProps(
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
handleDrop: (view, event, _slice, moved) => {
|
handleDrop: (view, event, _slice, moved) => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const selection: any = window?.getSelection();
|
||||||
|
if (selection.rangeCount !== 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
if (findTableAncestor(range.startContainer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
|
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const file = event.dataTransfer.files[0];
|
const file = event.dataTransfer.files[0];
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
MinusSquare,
|
MinusSquare,
|
||||||
CheckSquare,
|
CheckSquare,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
|
Table,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { startImageUpload } from "../plugins/upload-image";
|
import { startImageUpload } from "../plugins/upload-image";
|
||||||
import { cn } from "../utils";
|
import { cn } from "../utils";
|
||||||
@ -46,6 +47,9 @@ const Command = Extension.create({
|
|||||||
return [
|
return [
|
||||||
Suggestion({
|
Suggestion({
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
|
allow({ editor }) {
|
||||||
|
return !editor.isActive("table");
|
||||||
|
},
|
||||||
...this.options.suggestion,
|
...this.options.suggestion,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@ -53,7 +57,10 @@ const Command = Extension.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getSuggestionItems =
|
const getSuggestionItems =
|
||||||
(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) =>
|
(
|
||||||
|
workspaceSlug: string,
|
||||||
|
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||||
|
) =>
|
||||||
({ query }: { query: string }) =>
|
({ query }: { query: string }) =>
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@ -119,6 +126,20 @@ const getSuggestionItems =
|
|||||||
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Table",
|
||||||
|
description: "Create a Table",
|
||||||
|
searchTerms: ["table", "cell", "db", "data", "tabular"],
|
||||||
|
icon: <Table size={18} />,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.deleteRange(range)
|
||||||
|
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
||||||
|
.run();
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Numbered List",
|
title: "Numbered List",
|
||||||
description: "Create a list with numbering.",
|
description: "Create a list with numbering.",
|
||||||
@ -134,14 +155,21 @@ const getSuggestionItems =
|
|||||||
searchTerms: ["blockquote"],
|
searchTerms: ["blockquote"],
|
||||||
icon: <TextQuote size={18} />,
|
icon: <TextQuote size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.deleteRange(range)
|
||||||
|
.toggleNode("paragraph", "paragraph")
|
||||||
|
.toggleBlockquote()
|
||||||
|
.run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Code",
|
title: "Code",
|
||||||
description: "Capture a code snippet.",
|
description: "Capture a code snippet.",
|
||||||
searchTerms: ["codeblock"],
|
searchTerms: ["codeblock"],
|
||||||
icon: <Code size={18} />,
|
icon: <Code size={18} />,
|
||||||
command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
command: ({ editor, range }: CommandProps) =>
|
||||||
|
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Image",
|
title: "Image",
|
||||||
@ -190,7 +218,15 @@ export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => {
|
const CommandList = ({
|
||||||
|
items,
|
||||||
|
command,
|
||||||
|
}: {
|
||||||
|
items: CommandItemProps[];
|
||||||
|
command: any;
|
||||||
|
editor: any;
|
||||||
|
range: any;
|
||||||
|
}) => {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
const selectItem = useCallback(
|
const selectItem = useCallback(
|
||||||
|
16
space/components/tiptap/table-menu/InsertBottomTableIcon.tsx
Normal file
16
space/components/tiptap/table-menu/InsertBottomTableIcon.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const InsertBottomTableIcon = (props: any) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M212.309-152.31q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-375.383q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm535.382-219.998H212.309q-4.616 0-8.463 3.846-3.846 3.846-3.846 8.462V-600q0 4.616 3.846 8.462 3.847 3.847 8.463 3.847h535.382q4.616 0 8.463-3.847Q760-595.384 760-600v-135.383q0-4.616-3.846-8.462-3.847-3.846-8.463-3.846ZM200-587.691v-160 160Z"
|
||||||
|
fill="rgb(var(--color-text-300))"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default InsertBottomTableIcon;
|
15
space/components/tiptap/table-menu/InsertLeftTableIcon.tsx
Normal file
15
space/components/tiptap/table-menu/InsertLeftTableIcon.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const InsertLeftTableIcon = (props: any) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M224.617-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm147.691-607.69q0-4.616-3.846-8.463-3.846-3.846-8.462-3.846H600q-4.616 0-8.462 3.846-3.847 3.847-3.847 8.463v535.382q0 4.616 3.847 8.463Q595.384-200 600-200h135.383q4.616 0 8.462-3.846 3.846-3.847 3.846-8.463v-535.382ZM587.691-200h160-160Z"
|
||||||
|
fill="rgb(var(--color-text-300))"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
export default InsertLeftTableIcon;
|
16
space/components/tiptap/table-menu/InsertRightTableIcon.tsx
Normal file
16
space/components/tiptap/table-menu/InsertRightTableIcon.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const InsertRightTableIcon = (props: any) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M600-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm-375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm-12.308-607.69v535.382q0 4.616 3.846 8.463 3.846 3.846 8.462 3.846H360q4.616 0 8.462-3.846 3.847-3.847 3.847-8.463v-535.382q0-4.616-3.847-8.463Q364.616-760 360-760H224.617q-4.616 0-8.462 3.846-3.846 3.847-3.846 8.463Zm160 547.691h-160 160Z"
|
||||||
|
fill="rgb(var(--color-text-300))"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default InsertRightTableIcon;
|
15
space/components/tiptap/table-menu/InsertTopTableIcon.tsx
Normal file
15
space/components/tiptap/table-menu/InsertTopTableIcon.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const InsertTopTableIcon = (props: any) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M212.309-527.693q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0 375.383q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-59.999h535.382q4.616 0 8.463-3.846 3.846-3.846 3.846-8.462V-360q0-4.616-3.846-8.462-3.847-3.847-8.463-3.847H212.309q-4.616 0-8.463 3.847Q200-364.616 200-360v135.383q0 4.616 3.846 8.462 3.847 3.846 8.463 3.846Zm-12.309-160v160-160Z"
|
||||||
|
fill="rgb(var(--color-text-300))"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
export default InsertTopTableIcon;
|
143
space/components/tiptap/table-menu/index.tsx
Normal file
143
space/components/tiptap/table-menu/index.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Rows, Columns, ToggleRight } from "lucide-react";
|
||||||
|
import { cn } from "../utils";
|
||||||
|
import { Tooltip } from "components/ui";
|
||||||
|
import InsertLeftTableIcon from "./InsertLeftTableIcon";
|
||||||
|
import InsertRightTableIcon from "./InsertRightTableIcon";
|
||||||
|
import InsertTopTableIcon from "./InsertTopTableIcon";
|
||||||
|
import InsertBottomTableIcon from "./InsertBottomTableIcon";
|
||||||
|
|
||||||
|
interface TableMenuItem {
|
||||||
|
command: () => void;
|
||||||
|
icon: any;
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
|
||||||
|
while (node !== null && node.nodeName !== "TABLE") {
|
||||||
|
node = node.parentNode;
|
||||||
|
}
|
||||||
|
return node as HTMLTableElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TableMenu = ({ editor }: { editor: any }) => {
|
||||||
|
const [tableLocation, setTableLocation] = useState({ bottom: 0, left: 0 });
|
||||||
|
const isOpen = editor?.isActive("table");
|
||||||
|
|
||||||
|
const items: TableMenuItem[] = [
|
||||||
|
{
|
||||||
|
command: () => editor.chain().focus().addColumnBefore().run(),
|
||||||
|
icon: InsertLeftTableIcon,
|
||||||
|
key: "insert-column-left",
|
||||||
|
name: "Insert 1 column left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: () => editor.chain().focus().addColumnAfter().run(),
|
||||||
|
icon: InsertRightTableIcon,
|
||||||
|
key: "insert-column-right",
|
||||||
|
name: "Insert 1 column right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: () => editor.chain().focus().addRowBefore().run(),
|
||||||
|
icon: InsertTopTableIcon,
|
||||||
|
key: "insert-row-above",
|
||||||
|
name: "Insert 1 row above",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: () => editor.chain().focus().addRowAfter().run(),
|
||||||
|
icon: InsertBottomTableIcon,
|
||||||
|
key: "insert-row-below",
|
||||||
|
name: "Insert 1 row below",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: () => editor.chain().focus().deleteColumn().run(),
|
||||||
|
icon: Columns,
|
||||||
|
key: "delete-column",
|
||||||
|
name: "Delete column",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: () => editor.chain().focus().deleteRow().run(),
|
||||||
|
icon: Rows,
|
||||||
|
key: "delete-row",
|
||||||
|
name: "Delete row",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: () => editor.chain().focus().toggleHeaderRow().run(),
|
||||||
|
icon: ToggleRight,
|
||||||
|
key: "toggle-header-row",
|
||||||
|
name: "Toggle header row",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!window) return;
|
||||||
|
|
||||||
|
const handleWindowClick = () => {
|
||||||
|
const selection: any = window?.getSelection();
|
||||||
|
|
||||||
|
if (selection.rangeCount !== 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const tableNode = findTableAncestor(range.startContainer);
|
||||||
|
|
||||||
|
let parent = tableNode?.parentElement;
|
||||||
|
|
||||||
|
if (tableNode) {
|
||||||
|
const tableRect = tableNode.getBoundingClientRect();
|
||||||
|
const tableCenter = tableRect.left + tableRect.width / 2;
|
||||||
|
const menuWidth = 45;
|
||||||
|
const menuLeft = tableCenter - menuWidth / 2;
|
||||||
|
const tableBottom = tableRect.bottom;
|
||||||
|
|
||||||
|
setTableLocation({ bottom: tableBottom, left: menuLeft });
|
||||||
|
|
||||||
|
while (parent) {
|
||||||
|
if (!parent.classList.contains("disable-scroll"))
|
||||||
|
parent.classList.add("disable-scroll");
|
||||||
|
parent = parent.parentElement;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const scrollDisabledContainers = document.querySelectorAll(".disable-scroll");
|
||||||
|
|
||||||
|
scrollDisabledContainers.forEach((container) => {
|
||||||
|
container.classList.remove("disable-scroll");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("click", handleWindowClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("click", handleWindowClick);
|
||||||
|
};
|
||||||
|
}, [tableLocation, editor]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={`fixed left-1/2 transform -translate-x-1/2 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 shadow-custom-shadow-sm p-1 ${
|
||||||
|
isOpen ? "block" : "hidden"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
bottom: `calc(100vh - ${tableLocation.bottom + 45}px)`,
|
||||||
|
left: `${tableLocation.left}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Tooltip key={index} tooltipContent={item.name}>
|
||||||
|
<button
|
||||||
|
onClick={item.command}
|
||||||
|
className="p-1.5 text-custom-text-200 hover:bg-text-custom-text-100 hover:bg-custom-background-80 active:bg-custom-background-80 rounded"
|
||||||
|
title={item.name}
|
||||||
|
>
|
||||||
|
<item.icon
|
||||||
|
className={cn("h-4 w-4 text-lg", {
|
||||||
|
"text-red-600": item.key.includes("delete"),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
@ -1,9 +1,10 @@
|
|||||||
|
import { useImperativeHandle, useRef, forwardRef, useEffect } from "react";
|
||||||
import { useEditor, EditorContent, Editor } from "@tiptap/react";
|
import { useEditor, EditorContent, Editor } from "@tiptap/react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
// components
|
||||||
import { EditorBubbleMenu } from "./bubble-menu";
|
import { EditorBubbleMenu } from "./bubble-menu";
|
||||||
import { TiptapExtensions } from "./extensions";
|
import { TiptapExtensions } from "./extensions";
|
||||||
import { TiptapEditorProps } from "./props";
|
import { TiptapEditorProps } from "./props";
|
||||||
import { useImperativeHandle, useRef, forwardRef } from "react";
|
|
||||||
import { ImageResizer } from "./extensions/image-resize";
|
import { ImageResizer } from "./extensions/image-resize";
|
||||||
import { TableMenu } from "./table-menu";
|
import { TableMenu } from "./table-menu";
|
||||||
|
|
||||||
@ -55,6 +56,12 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editor) {
|
||||||
|
editor.commands.setContent(value);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
|
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
|
||||||
|
|
||||||
useImperativeHandle(forwardedRef, () => ({
|
useImperativeHandle(forwardedRef, () => ({
|
||||||
@ -76,8 +83,8 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
|
|||||||
|
|
||||||
const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
|
const editorClassNames = `relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md
|
||||||
${noBorder ? "" : "border border-custom-border-200"} ${
|
${noBorder ? "" : "border border-custom-border-200"} ${
|
||||||
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0"
|
||||||
} ${customClassName}`;
|
} ${customClassName}`;
|
||||||
|
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
|
Loading…
Reference in New Issue
Block a user