From efcc7f20ccf63ff16ab9b744af4e3e87d47d42fd Mon Sep 17 00:00:00 2001
From: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com>
Date: Thu, 28 Sep 2023 20:25:31 +0530
Subject: [PATCH] added seperation of extensions and props
---
packages/editor/core/src/hooks/useEditor.tsx | 34 ++
packages/editor/core/src/index.ts | 7 +
packages/editor/core/src/interfaces/index.ts | 4 +
.../core/src/ui/extensions/index-new.tsx | 88 +++++
packages/editor/core/src/ui/index-new.tsx | 150 ++++++++
packages/editor/core/src/ui/index.tsx | 1 -
packages/editor/core/src/ui/props.tsx | 1 -
.../rich-text-editor/src/extensions.tsx | 66 ++++
.../editor/rich-text-editor/src/index.tsx | 0
.../rich-text-editor/src/slash-command.tsx | 363 ++++++++++++++++++
.../rich-text-editor/src/table/table-cell.ts | 32 ++
.../src/table/table-header.ts | 7 +
.../rich-text-editor/src/table/table.ts | 9 +
.../src/types/upload-image.ts | 1 +
packages/tailwind-config-custom/package.json | 9 +-
web/postcss.config.js | 9 +-
yarn.lock | 34 +-
17 files changed, 804 insertions(+), 11 deletions(-)
create mode 100644 packages/editor/core/src/hooks/useEditor.tsx
create mode 100644 packages/editor/core/src/interfaces/index.ts
create mode 100644 packages/editor/core/src/ui/extensions/index-new.tsx
create mode 100644 packages/editor/core/src/ui/index-new.tsx
create mode 100644 packages/editor/rich-text-editor/src/extensions.tsx
create mode 100644 packages/editor/rich-text-editor/src/index.tsx
create mode 100644 packages/editor/rich-text-editor/src/slash-command.tsx
create mode 100644 packages/editor/rich-text-editor/src/table/table-cell.ts
create mode 100644 packages/editor/rich-text-editor/src/table/table-header.ts
create mode 100644 packages/editor/rich-text-editor/src/table/table.ts
create mode 100644 packages/editor/rich-text-editor/src/types/upload-image.ts
diff --git a/packages/editor/core/src/hooks/useEditor.tsx b/packages/editor/core/src/hooks/useEditor.tsx
new file mode 100644
index 000000000..9d121fcff
--- /dev/null
+++ b/packages/editor/core/src/hooks/useEditor.tsx
@@ -0,0 +1,34 @@
+import { DeleteImage } from "@/types/delete-image";
+import { UploadImage } from "@/types/upload-image";
+import { TiptapExtensions } from "@/ui/extensions";
+import { TiptapEditorProps } from "@/ui/props";
+import { useEditor as useTiptapEditor } from "@tiptap/react";
+
+interface ITiptapEditor {
+ value: string;
+ uploadFile: UploadImage;
+ deleteFile: DeleteImage;
+ onChange?: (json: any, html: string) => void;
+ setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
+ setShouldShowAlert?: (showAlert: boolean) => void;
+ editable?: boolean;
+ debouncedUpdatesEnabled?: boolean;
+ debouncedUpdates: ({ onChange, editor }: { onChange?: (json: any, html: string) => void; editor: any }) => void;
+}
+
+export const useEditor = ({ uploadFile, debouncedUpdates, setShouldShowAlert, deleteFile, setIsSubmitting, value, onChange, debouncedUpdatesEnabled, editable }: ITiptapEditor) => useTiptapEditor({
+ editable: editable ?? true,
+ editorProps: TiptapEditorProps(uploadFile, setIsSubmitting),
+ extensions: TiptapExtensions(uploadFile, deleteFile, setIsSubmitting),
+ content: (typeof value === "string" && value.trim() !== "") ? value : "
",
+ onUpdate: async ({ editor }) => {
+ // for instant feedback loop
+ setIsSubmitting?.("submitting");
+ setShouldShowAlert?.(true);
+ if (debouncedUpdatesEnabled) {
+ debouncedUpdates({ onChange, editor });
+ } else {
+ onChange?.(editor.getJSON(), editor.getHTML());
+ }
+ },
+});
diff --git a/packages/editor/core/src/index.ts b/packages/editor/core/src/index.ts
index 7e137d3c6..890841cb9 100644
--- a/packages/editor/core/src/index.ts
+++ b/packages/editor/core/src/index.ts
@@ -1,5 +1,12 @@
import "@/styles/tailwind.css";
import "@/styles/editor.css";
+// export { ImageResizer } from "./ui/extensions/image/image-resize";
+// export { TiptapEditorProps } from "./ui/props";
+// export { TableMenu } from "./ui/menus/table-menu";
+// export { TiptapExtensions } from "./ui/extensions";
+// export { cn } from "./lib/utils";
+// export { FixedMenu } from "./ui/menus/fixed-menu";
+// export { EditorBubbleMenu } from "./ui/menus/bubble-menu";
export { TiptapEditor, TiptapEditorWithRef } from "@/ui";
diff --git a/packages/editor/core/src/interfaces/index.ts b/packages/editor/core/src/interfaces/index.ts
new file mode 100644
index 000000000..7f5492df7
--- /dev/null
+++ b/packages/editor/core/src/interfaces/index.ts
@@ -0,0 +1,4 @@
+export interface EditorHandle {
+ clearEditor: () => void;
+ setEditorValue: (content: string) => void;
+}
diff --git a/packages/editor/core/src/ui/extensions/index-new.tsx b/packages/editor/core/src/ui/extensions/index-new.tsx
new file mode 100644
index 000000000..27639fe06
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/index-new.tsx
@@ -0,0 +1,88 @@
+import StarterKit from "@tiptap/starter-kit";
+import TiptapLink from "@tiptap/extension-link";
+import TiptapUnderline from "@tiptap/extension-underline";
+import TextStyle from "@tiptap/extension-text-style";
+import { Color } from "@tiptap/extension-color";
+import TaskItem from "@tiptap/extension-task-item";
+import TaskList from "@tiptap/extension-task-list";
+import { Markdown } from "tiptap-markdown";
+import Gapcursor from "@tiptap/extension-gapcursor";
+
+import UpdatedImage from "@/ui/extensions/image/updated-image";
+
+import { DeleteImage } from "@/types/delete-image";
+
+import isValidHttpUrl from "@/ui/menus/bubble-menu/utils"
+
+export const TiptapExtensions = (
+ deleteFile: DeleteImage,
+) => [
+ StarterKit.configure({
+ bulletList: {
+ HTMLAttributes: {
+ class: "list-disc list-outside leading-3 -mt-2",
+ },
+ },
+ orderedList: {
+ HTMLAttributes: {
+ class: "list-decimal list-outside leading-3 -mt-2",
+ },
+ },
+ listItem: {
+ HTMLAttributes: {
+ class: "leading-normal -mb-2",
+ },
+ },
+ blockquote: {
+ HTMLAttributes: {
+ class: "border-l-4 border-custom-border-300",
+ },
+ },
+ code: {
+ HTMLAttributes: {
+ class:
+ "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
+ spellcheck: "false",
+ },
+ },
+ codeBlock: false,
+ horizontalRule: false,
+ dropcursor: {
+ color: "rgba(var(--color-text-100))",
+ width: 2,
+ },
+ gapcursor: false,
+ }),
+ Gapcursor,
+ TiptapLink.configure({
+ protocols: ["http", "https"],
+ validate: (url) => isValidHttpUrl(url),
+ HTMLAttributes: {
+ class:
+ "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
+ },
+ }),
+ UpdatedImage(deleteFile).configure({
+ HTMLAttributes: {
+ class: "rounded-lg border border-custom-border-300",
+ },
+ }),
+ TiptapUnderline,
+ TextStyle,
+ Color,
+ TaskList.configure({
+ HTMLAttributes: {
+ class: "not-prose pl-2",
+ },
+ }),
+ TaskItem.configure({
+ HTMLAttributes: {
+ class: "flex items-start my-4",
+ },
+ nested: true,
+ }),
+ Markdown.configure({
+ html: true,
+ transformCopiedText: true,
+ }),
+ ];
diff --git a/packages/editor/core/src/ui/index-new.tsx b/packages/editor/core/src/ui/index-new.tsx
new file mode 100644
index 000000000..b6e42348b
--- /dev/null
+++ b/packages/editor/core/src/ui/index-new.tsx
@@ -0,0 +1,150 @@
+"use client"
+import * as React from 'react';
+import { useImperativeHandle, useRef, forwardRef } from "react";
+import { useEditor, EditorContent, Editor, Extension } from "@tiptap/react";
+import { useDebouncedCallback } from "use-debounce";
+import { TableMenu } from '@/ui/menus/table-menu';
+import { TiptapExtensions } from '@/ui/extensions';
+import { EditorBubbleMenu } from '@/ui/menus/bubble-menu';
+import { ImageResizer } from '@/ui/extensions/image/image-resize';
+import { TiptapEditorProps } from '@/ui/props';
+import { UploadImage } from '@/types/upload-image';
+import { DeleteImage } from '@/types/delete-image';
+import { cn } from '@/lib/utils';
+import { FixedMenu } from './menus/fixed-menu';
+import { EditorProps } from '@tiptap/pm/view';
+
+interface ITiptapEditor {
+ value: string;
+ uploadFile: UploadImage;
+ deleteFile: DeleteImage;
+ noBorder?: boolean;
+ borderOnFocus?: boolean;
+ customClassName?: string;
+ editorContentCustomClassNames?: string;
+ onChange?: (json: any, html: string) => void;
+ setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
+ setShouldShowAlert?: (showAlert: boolean) => void;
+ editable?: boolean;
+ forwardedRef?: any;
+ debouncedUpdatesEnabled?: boolean;
+ accessValue: string;
+ onAccessChange: (accessKey: string) => void;
+ commentAccess: {
+ icon: string;
+ key: string;
+ label: "Private" | "Public";
+ }[];
+ extensions?: Extension[];
+ editorProps?: EditorProps;
+}
+
+interface TiptapProps extends ITiptapEditor {
+ forwardedRef?: React.Ref;
+}
+
+interface EditorHandle {
+ clearEditor: () => void;
+ setEditorValue: (content: string) => void;
+}
+
+const DEBOUNCE_DELAY = 1500;
+
+const TiptapEditor = ({
+ onChange,
+ debouncedUpdatesEnabled,
+ editable,
+ setIsSubmitting,
+ setShouldShowAlert,
+ editorContentCustomClassNames,
+ value,
+ uploadFile,
+ extensions = [],
+ editorProps = {},
+ deleteFile,
+ noBorder,
+ borderOnFocus,
+ customClassName,
+ forwardedRef,
+ accessValue,
+ onAccessChange,
+ commentAccess,
+}: TiptapProps) => {
+ const editor = useEditor({
+ editable: editable ?? true,
+ editorProps: {
+ ...TiptapEditorProps(uploadFile, setIsSubmitting),
+ ...editorProps,
+ },
+ extensions: [...TiptapExtensions(uploadFile, deleteFile, setIsSubmitting), ...extensions],
+ content: (typeof value === "string" && value.trim() !== "") ? value : "",
+ onUpdate: async ({ editor }) => {
+ // for instant feedback loop
+ setIsSubmitting?.("submitting");
+ setShouldShowAlert?.(true);
+ if (debouncedUpdatesEnabled) {
+ debouncedUpdates({ onChange, editor });
+ } else {
+ onChange?.(editor.getJSON(), editor.getHTML());
+ }
+ },
+ });
+
+ const editorRef: React.MutableRefObject = useRef(null);
+ editorRef.current = editor;
+
+ useImperativeHandle(forwardedRef, () => ({
+ clearEditor: () => {
+ editorRef.current?.commands.clearContent();
+ },
+ setEditorValue: (content: string) => {
+ editorRef.current?.commands.setContent(content);
+ },
+ }));
+
+ const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
+ if (onChange) {
+ onChange(editor.getJSON(), editor.getHTML());
+ }
+ }, DEBOUNCE_DELAY);
+
+ const editorClassNames = cn(
+ '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',
+ borderOnFocus ? 'focus:border border-custom-border-300' : 'focus:border-0',
+ customClassName
+ );
+
+ if (!editor) return null;
+
+ return (
+ {
+ editor?.chain().focus().run();
+ }}
+ className={`tiptap-editor-container cursor-text ${editorClassNames}`}
+ >
+
+
+
+
+ {editor?.isActive("image") &&
}
+
+ {editor && editable !== false &&
+ (
+
+
)
+ }
+
+
+ );
+};
+
+const TiptapEditorWithRef = forwardRef((props, ref) => (
+
+));
+
+TiptapEditorWithRef.displayName = "TiptapEditorWithRef";
+
+export { TiptapEditor, TiptapEditorWithRef };
diff --git a/packages/editor/core/src/ui/index.tsx b/packages/editor/core/src/ui/index.tsx
index fc870f309..28808f649 100644
--- a/packages/editor/core/src/ui/index.tsx
+++ b/packages/editor/core/src/ui/index.tsx
@@ -68,7 +68,6 @@ const TiptapEditor = ({
const editor = useEditor({
editable: editable ?? true,
editorProps: TiptapEditorProps(uploadFile, setIsSubmitting),
- // @ts-expect-err
extensions: TiptapExtensions(uploadFile, deleteFile, setIsSubmitting),
content: (typeof value === "string" && value.trim() !== "") ? value : "",
onUpdate: async ({ editor }) => {
diff --git a/packages/editor/core/src/ui/props.tsx b/packages/editor/core/src/ui/props.tsx
index 528f664ea..d2a0d8063 100644
--- a/packages/editor/core/src/ui/props.tsx
+++ b/packages/editor/core/src/ui/props.tsx
@@ -58,7 +58,6 @@ export function TiptapEditorProps(
left: event.clientX,
top: event.clientY,
});
- // here we deduct 1 from the pos or else the image will create an extra node
if (coordinates) {
startImageUpload(file, view, coordinates.pos - 1, uploadFile, setIsSubmitting);
}
diff --git a/packages/editor/rich-text-editor/src/extensions.tsx b/packages/editor/rich-text-editor/src/extensions.tsx
new file mode 100644
index 000000000..85087e5e1
--- /dev/null
+++ b/packages/editor/rich-text-editor/src/extensions.tsx
@@ -0,0 +1,66 @@
+import HorizontalRule from "@tiptap/extension-horizontal-rule";
+import { TableRow } from "@tiptap/extension-table-row";
+import Placeholder from "@tiptap/extension-placeholder";
+import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
+import { InputRule } from "@tiptap/core";
+
+import ts from "highlight.js/lib/languages/typescript";
+import { lowlight } from "lowlight/lib/core";
+import "highlight.js/styles/github-dark.css";
+import { Table } from "./table/table";
+import { TableHeader } from "./table/table-header";
+import { CustomTableCell } from "./table/table-cell";
+import SlashCommand from "./slash-command";
+import { UploadImage } from "./types/upload-image";
+
+lowlight.registerLanguage("ts", ts);
+
+export const TiptapExtensions = (
+ uploadFile: UploadImage,
+ setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
+) => [
+ CodeBlockLowlight.configure({
+ lowlight,
+ }),
+ HorizontalRule.extend({
+ addInputRules() {
+ return [
+ new InputRule({
+ find: /^(?:---|—-|___\s|\*\*\*\s)$/,
+ handler: ({ state, range, commands }) => {
+ commands.splitBlock();
+
+ const attributes = {};
+ const { tr } = state;
+ const start = range.from;
+ const end = range.to;
+ // @ts-ignore
+ tr.replaceWith(start - 1, end, this.type.create(attributes));
+ },
+ }),
+ ];
+ },
+ }).configure({
+ HTMLAttributes: {
+ class: "mb-6 border-t border-custom-border-300",
+ },
+ }),
+ Placeholder.configure({
+ placeholder: ({ node }) => {
+ if (node.type.name === "heading") {
+ return `Heading ${node.attrs.level}`;
+ }
+ if (node.type.name === "image" || node.type.name === "table") {
+ return "";
+ }
+
+ return "Press '/' for commands...";
+ },
+ includeChildren: true,
+ }),
+ SlashCommand(uploadFile, setIsSubmitting),
+ Table,
+ TableHeader,
+ CustomTableCell,
+ TableRow,
+ ];
diff --git a/packages/editor/rich-text-editor/src/index.tsx b/packages/editor/rich-text-editor/src/index.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/editor/rich-text-editor/src/slash-command.tsx b/packages/editor/rich-text-editor/src/slash-command.tsx
new file mode 100644
index 000000000..844d4c55a
--- /dev/null
+++ b/packages/editor/rich-text-editor/src/slash-command.tsx
@@ -0,0 +1,363 @@
+import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react";
+import { Editor, Range, Extension } from "@tiptap/core";
+import Suggestion from "@tiptap/suggestion";
+import { ReactRenderer } from "@tiptap/react";
+import tippy from "tippy.js";
+import {
+ Heading1,
+ Heading2,
+ Heading3,
+ List,
+ ListOrdered,
+ Text,
+ TextQuote,
+ Code,
+ MinusSquare,
+ CheckSquare,
+ ImageIcon,
+ Table,
+} from "lucide-react";
+import { startImageUpload } from "@/ui/plugins/upload-image";
+import { cn } from "@/lib/utils";
+import { UploadImage } from "@/types/upload-image";
+
+interface CommandItemProps {
+ title: string;
+ description: string;
+ icon: ReactNode;
+}
+
+interface CommandProps {
+ editor: Editor;
+ range: Range;
+}
+
+const Command = Extension.create({
+ name: "slash-command",
+ addOptions() {
+ return {
+ suggestion: {
+ char: "/",
+ command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
+ props.command({ editor, range });
+ },
+ },
+ };
+ },
+ addProseMirrorPlugins() {
+ return [
+ Suggestion({
+ editor: this.editor,
+ allow({ editor }) {
+ return !editor.isActive("table");
+ },
+ ...this.options.suggestion,
+ }),
+ ];
+ },
+});
+
+const getSuggestionItems =
+ (
+ uploadFile: UploadImage,
+ setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
+ ) =>
+ ({ query }: { query: string }) =>
+ [
+ {
+ title: "Text",
+ description: "Just start typing with plain text.",
+ searchTerms: ["p", "paragraph"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
+ },
+ },
+ {
+ title: "Heading 1",
+ description: "Big section heading.",
+ searchTerms: ["title", "big", "large"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
+ },
+ },
+ {
+ title: "Heading 2",
+ description: "Medium section heading.",
+ searchTerms: ["subtitle", "medium"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
+ },
+ },
+ {
+ title: "Heading 3",
+ description: "Small section heading.",
+ searchTerms: ["subtitle", "small"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
+ },
+ },
+ {
+ title: "To-do List",
+ description: "Track tasks with a to-do list.",
+ searchTerms: ["todo", "task", "list", "check", "checkbox"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).toggleTaskList().run();
+ },
+ },
+ {
+ title: "Bullet List",
+ description: "Create a simple bullet list.",
+ searchTerms: ["unordered", "point"],
+ icon:
,
+ command: ({ editor, range }: CommandProps) => {
+ // @ts-ignore
+ editor.chain().focus().deleteRange(range).toggleBulletList().run();
+ },
+ },
+ {
+ title: "Divider",
+ description: "Visually divide blocks",
+ searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).setHorizontalRule().run();
+ },
+ },
+ {
+ title: "Table",
+ description: "Create a Table",
+ searchTerms: ["table", "cell", "db", "data", "tabular"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor
+ .chain()
+ .focus()
+ .deleteRange(range)
+ .insertTable({ rows: 3, cols: 3, withHeaderRow: true })
+ .run();
+ },
+ },
+ {
+ title: "Numbered List",
+ description: "Create a list with numbering.",
+ searchTerms: ["ordered"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ // @ts-ignore
+ editor.chain().focus().deleteRange(range).toggleOrderedList().run();
+ },
+ },
+ {
+ title: "Quote",
+ description: "Capture a quote.",
+ searchTerms: ["blockquote"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) =>
+ // @ts-ignore
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
+ },
+ {
+ title: "Code",
+ description: "Capture a code snippet.",
+ searchTerms: ["codeblock"],
+ icon:
,
+ command: ({ editor, range }: CommandProps) =>
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
+ },
+ {
+ title: "Image",
+ description: "Upload an image from your computer.",
+ searchTerms: ["photo", "picture", "media"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).run();
+ // upload image
+ const input = document.createElement("input");
+ input.type = "file";
+ input.accept = "image/*";
+ input.onchange = async () => {
+ if (input.files?.length) {
+ const file = input.files[0];
+ const pos = editor.view.state.selection.from;
+ startImageUpload(file, editor.view, pos, uploadFile, setIsSubmitting);
+ }
+ };
+ input.click();
+ },
+ },
+ ].filter((item) => {
+ if (typeof query === "string" && query.length > 0) {
+ const search = query.toLowerCase();
+ return (
+ item.title.toLowerCase().includes(search) ||
+ item.description.toLowerCase().includes(search) ||
+ (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
+ );
+ }
+ return true;
+ });
+
+export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
+ const containerHeight = container.offsetHeight;
+ const itemHeight = item ? item.offsetHeight : 0;
+
+ const top = item.offsetTop;
+ const bottom = top + itemHeight;
+
+ if (top < container.scrollTop) {
+ container.scrollTop -= container.scrollTop - top + 5;
+ } else if (bottom > containerHeight + container.scrollTop) {
+ container.scrollTop += bottom - containerHeight - container.scrollTop + 5;
+ }
+};
+
+const CommandList = ({
+ items,
+ command,
+}: {
+ items: CommandItemProps[];
+ command: any;
+ editor: any;
+ range: any;
+}) => {
+ const [selectedIndex, setSelectedIndex] = useState(0);
+
+ const selectItem = useCallback(
+ (index: number) => {
+ const item = items[index];
+ if (item) {
+ command(item);
+ }
+ },
+ [command, items]
+ );
+
+ useEffect(() => {
+ const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (navigationKeys.includes(e.key)) {
+ e.preventDefault();
+ if (e.key === "ArrowUp") {
+ setSelectedIndex((selectedIndex + items.length - 1) % items.length);
+ return true;
+ }
+ if (e.key === "ArrowDown") {
+ setSelectedIndex((selectedIndex + 1) % items.length);
+ return true;
+ }
+ if (e.key === "Enter") {
+ selectItem(selectedIndex);
+ return true;
+ }
+ return false;
+ }
+ };
+ document.addEventListener("keydown", onKeyDown);
+ return () => {
+ document.removeEventListener("keydown", onKeyDown);
+ };
+ }, [items, selectedIndex, setSelectedIndex, selectItem]);
+
+ useEffect(() => {
+ setSelectedIndex(0);
+ }, [items]);
+
+ const commandListContainer = useRef(null);
+
+ useLayoutEffect(() => {
+ const container = commandListContainer?.current;
+
+ const item = container?.children[selectedIndex] as HTMLElement;
+
+ if (item && container) updateScrollView(container, item);
+ }, [selectedIndex]);
+
+ return items.length > 0 ? (
+
+ {items.map((item: CommandItemProps, index: number) => (
+
+ ))}
+
+ ) : null;
+};
+
+const renderItems = () => {
+ let component: ReactRenderer | null = null;
+ let popup: any | null = null;
+
+ return {
+ onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
+ component = new ReactRenderer(CommandList, {
+ props,
+ editor: props.editor,
+ });
+
+ // @ts-ignore
+ popup = tippy("body", {
+ getReferenceClientRect: props.clientRect,
+ appendTo: () => document.querySelector("#tiptap-container"),
+ content: component.element,
+ showOnCreate: true,
+ interactive: true,
+ trigger: "manual",
+ placement: "bottom-start",
+ });
+ },
+ onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
+ component?.updateProps(props);
+
+ popup &&
+ popup[0].setProps({
+ getReferenceClientRect: props.clientRect,
+ });
+ },
+ onKeyDown: (props: { event: KeyboardEvent }) => {
+ if (props.event.key === "Escape") {
+ popup?.[0].hide();
+
+ return true;
+ }
+
+ // @ts-ignore
+ return component?.ref?.onKeyDown(props);
+ },
+ onExit: () => {
+ popup?.[0].destroy();
+ component?.destroy();
+ },
+ };
+};
+
+export const SlashCommand = (
+ uploadFile: UploadImage,
+ setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
+) =>
+ Command.configure({
+ suggestion: {
+ items: getSuggestionItems(uploadFile, setIsSubmitting),
+ render: renderItems,
+ },
+ });
+
+export default SlashCommand;
diff --git a/packages/editor/rich-text-editor/src/table/table-cell.ts b/packages/editor/rich-text-editor/src/table/table-cell.ts
new file mode 100644
index 000000000..643cb8c64
--- /dev/null
+++ b/packages/editor/rich-text-editor/src/table/table-cell.ts
@@ -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];
+ },
+});
diff --git a/packages/editor/rich-text-editor/src/table/table-header.ts b/packages/editor/rich-text-editor/src/table/table-header.ts
new file mode 100644
index 000000000..f23aa93ef
--- /dev/null
+++ b/packages/editor/rich-text-editor/src/table/table-header.ts
@@ -0,0 +1,7 @@
+import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
+
+const TableHeader = BaseTableHeader.extend({
+ content: "paragraph",
+});
+
+export { TableHeader };
diff --git a/packages/editor/rich-text-editor/src/table/table.ts b/packages/editor/rich-text-editor/src/table/table.ts
new file mode 100644
index 000000000..9b727bb51
--- /dev/null
+++ b/packages/editor/rich-text-editor/src/table/table.ts
@@ -0,0 +1,9 @@
+import { Table as BaseTable } from "@tiptap/extension-table";
+
+const Table = BaseTable.configure({
+ resizable: true,
+ cellMinWidth: 100,
+ allowTableNodeSelection: true,
+});
+
+export { Table };
diff --git a/packages/editor/rich-text-editor/src/types/upload-image.ts b/packages/editor/rich-text-editor/src/types/upload-image.ts
new file mode 100644
index 000000000..3cf1408d2
--- /dev/null
+++ b/packages/editor/rich-text-editor/src/types/upload-image.ts
@@ -0,0 +1 @@
+export type UploadImage = (file: File) => Promise;
diff --git a/packages/tailwind-config-custom/package.json b/packages/tailwind-config-custom/package.json
index 1bd5a0e1c..b6c3ec614 100644
--- a/packages/tailwind-config-custom/package.json
+++ b/packages/tailwind-config-custom/package.json
@@ -4,7 +4,12 @@
"description": "common tailwind configuration across monorepo",
"main": "index.js",
"devDependencies": {
- "@tailwindcss/typography": "^0.5.10",
- "tailwindcss-animate": "^1.0.7"
+ "@tailwindcss/typography": "^0.5.9",
+ "autoprefixer": "^10.4.14",
+ "postcss": "^8.4.21",
+ "prettier": "^2.8.8",
+ "prettier-plugin-tailwindcss": "^0.3.0",
+ "tailwindcss": "^3.2.7",
+ "tailwindcss-animate": "^1.0.6"
}
}
diff --git a/web/postcss.config.js b/web/postcss.config.js
index 129aa7f59..6887c8262 100644
--- a/web/postcss.config.js
+++ b/web/postcss.config.js
@@ -1 +1,8 @@
-module.exports = require("tailwind-config-custom/postcss.config");
+module.exports = {
+ plugins: {
+ "postcss-import": {},
+ "tailwindcss/nesting": {},
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/yarn.lock b/yarn.lock
index d92d076bf..b2d3e099b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2340,7 +2340,7 @@
dependencies:
tslib "^2.4.0"
-"@tailwindcss/typography@^0.5.10":
+"@tailwindcss/typography@^0.5.9":
version "0.5.10"
resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.10.tgz#2abde4c6d5c797ab49cf47610830a301de4c1e0a"
integrity sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==
@@ -3193,6 +3193,18 @@ attr-accept@^2.2.2:
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
+autoprefixer@^10.4.14:
+ version "10.4.16"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8"
+ integrity sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==
+ dependencies:
+ browserslist "^4.21.10"
+ caniuse-lite "^1.0.30001538"
+ fraction.js "^4.3.6"
+ normalize-range "^0.1.2"
+ picocolors "^1.0.0"
+ postcss-value-parser "^4.2.0"
+
autoprefixer@^10.4.15:
version "10.4.15"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.15.tgz#a1230f4aeb3636b89120b34a1f513e2f6834d530"
@@ -3406,6 +3418,11 @@ caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.300015
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001538.tgz#9dbc6b9af1ff06b5eb12350c2012b3af56744f3f"
integrity sha512-HWJnhnID+0YMtGlzcp3T9drmBJUVDchPJ08tpUGFLs9CYlwWPH2uLgpHn8fND5pCgXVtnGS3H4QR9XLMHVNkHw==
+caniuse-lite@^1.0.30001538:
+ version "1.0.30001541"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001541.tgz#b1aef0fadd87fb72db4dcb55d220eae17b81cdb1"
+ integrity sha512-bLOsqxDgTqUBkzxbNlSBt8annkDpQB9NdzdTbO2ooJ+eC/IQcvDspDc058g84ejCelF7vHUx57KIOjEecOHXaw==
+
capital-case@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669"
@@ -4860,7 +4877,7 @@ format@^0.2.0:
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==
-fraction.js@^4.2.0:
+fraction.js@^4.2.0, fraction.js@^4.3.6:
version "4.3.6"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.6.tgz#e9e3acec6c9a28cf7bc36cbe35eea4ceb2c5c92d"
integrity sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==
@@ -6811,7 +6828,7 @@ postcss@8.4.14:
picocolors "^1.0.0"
source-map-js "^1.0.2"
-postcss@^8.4.23, postcss@^8.4.29:
+postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.29:
version "8.4.30"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.30.tgz#0e0648d551a606ef2192a26da4cabafcc09c1aa7"
integrity sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==
@@ -6843,12 +6860,17 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+prettier-plugin-tailwindcss@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.3.0.tgz#8299b307c7f6467f52732265579ed9375be6c818"
+ integrity sha512-009/Xqdy7UmkcTBpwlq7jsViDqXAYSOMLDrHAdTMlVZOrKfM2o9Ci7EMWTMZ7SkKBFTG04UM9F9iM2+4i6boDA==
+
prettier-plugin-tailwindcss@^0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.4.tgz#ebfacbcb90e2ca1df671ffe4083e99f81d72040d"
integrity sha512-QZzzB1bID6qPsKHTeA9qPo1APmmxfFrA5DD3LQ+vbTmAnY40eJI7t9Q1ocqel2EKMWNPLJqdTDWZj1hKYgqSgg==
-prettier@^2.8.7:
+prettier@^2.8.7, prettier@^2.8.8:
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
@@ -7977,12 +7999,12 @@ tailwind-merge@^1.14.0:
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b"
integrity sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==
-tailwindcss-animate@^1.0.7:
+tailwindcss-animate@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"
integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==
-tailwindcss@^3.3.3:
+tailwindcss@^3.2.7, tailwindcss@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==