diff --git a/apps/client/src/features/comment/components/comment-dialog.tsx b/apps/client/src/features/comment/components/comment-dialog.tsx index 5a9eab15..f73e605a 100644 --- a/apps/client/src/features/comment/components/comment-dialog.tsx +++ b/apps/client/src/features/comment/components/comment-dialog.tsx @@ -1,26 +1,26 @@ -import React, { useState } from 'react'; -import { Avatar, Dialog, Group, Stack, Text } from '@mantine/core'; -import { useClickOutside } from '@mantine/hooks'; -import { useAtom } from 'jotai'; +import React, { useState } from "react"; +import { Avatar, Dialog, Group, Stack, Text } from "@mantine/core"; +import { useClickOutside } from "@mantine/hooks"; +import { useAtom } from "jotai"; import { activeCommentIdAtom, draftCommentIdAtom, showCommentPopupAtom, -} from '@/features/comment/atoms/comment-atom'; -import { Editor } from '@tiptap/core'; -import CommentEditor from '@/features/comment/components/comment-editor'; -import CommentActions from '@/features/comment/components/comment-actions'; -import { currentUserAtom } from '@/features/user/atoms/current-user-atom'; -import { useCreateCommentMutation } from '@/features/comment/queries/comment-query'; -import { asideStateAtom } from '@/components/navbar/atoms/sidebar-atom'; +} from "@/features/comment/atoms/comment-atom"; +import CommentEditor from "@/features/comment/components/comment-editor"; +import CommentActions from "@/features/comment/components/comment-actions"; +import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; +import { useCreateCommentMutation } from "@/features/comment/queries/comment-query"; +import { asideStateAtom } from "@/components/navbar/atoms/sidebar-atom"; +import { useEditor } from "@tiptap/react"; interface CommentDialogProps { - editor: Editor, - pageId: string, + editor: ReturnType; + pageId: string; } function CommentDialog({ editor, pageId }: CommentDialogProps) { - const [comment, setComment] = useState(''); + const [comment, setComment] = useState(""); const [, setShowCommentPopup] = useAtom(showCommentPopupAtom); const [, setActiveCommentId] = useAtom(activeCommentIdAtom); const [draftCommentId, setDraftCommentId] = useAtom(draftCommentIdAtom); @@ -34,6 +34,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { const handleDialogClose = () => { setShowCommentPopup(false); + // @ts-ignore editor.chain().focus().unsetCommentDecoration().run(); }; @@ -52,11 +53,17 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { selection: selectedText, }; - const createdComment = await createCommentMutation.mutateAsync(commentData); - editor.chain().setComment(createdComment.id).unsetCommentDecoration().run(); + const createdComment = + await createCommentMutation.mutateAsync(commentData); + editor + .chain() + .setContent(createdComment.id) + // @ts-ignore + .unsetCommentDecoration() + .run(); setActiveCommentId(createdComment.id); - setAsideState({ tab: 'comments', isAsideOpen: true }); + setAsideState({ tab: "comments", isAsideOpen: true }); setTimeout(() => { const selector = `div[data-comment-id="${createdComment.id}"]`; const commentElement = document.querySelector(selector); @@ -64,7 +71,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { }); } finally { setShowCommentPopup(false); - setDraftCommentId(''); + setDraftCommentId(""); } }; @@ -73,24 +80,38 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { }; return ( - - + - {currentUser.user.name.charAt(0)} + + {currentUser.user.name.charAt(0)} +
- {currentUser.user.name} + + {currentUser.user.name} +
- - +
); diff --git a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx index 10634268..33df7612 100644 --- a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx @@ -1,14 +1,29 @@ -import { BubbleMenu, BubbleMenuProps, isNodeSelection } from '@tiptap/react'; -import { FC, useState } from 'react'; -import { IconBold, IconCode, IconItalic, IconStrikethrough, IconUnderline, IconMessage } from '@tabler/icons-react'; -import clsx from 'clsx'; -import classes from './bubble-menu.module.css'; -import { ActionIcon, rem, Tooltip } from '@mantine/core'; -import { ColorSelector } from './color-selector'; -import { NodeSelector } from './node-selector'; -import { draftCommentIdAtom, showCommentPopupAtom } from '@/features/comment/atoms/comment-atom'; -import { useAtom } from 'jotai'; -import { v4 as uuidv4 } from 'uuid'; +import { + BubbleMenu, + BubbleMenuProps, + isNodeSelection, + useEditor, +} from "@tiptap/react"; +import { FC, useState } from "react"; +import { + IconBold, + IconCode, + IconItalic, + IconStrikethrough, + IconUnderline, + IconMessage, +} from "@tabler/icons-react"; +import clsx from "clsx"; +import classes from "./bubble-menu.module.css"; +import { ActionIcon, rem, Tooltip } from "@mantine/core"; +import { ColorSelector } from "./color-selector"; +import { NodeSelector } from "./node-selector"; +import { + draftCommentIdAtom, + showCommentPopupAtom, +} from "@/features/comment/atoms/comment-atom"; +import { useAtom } from "jotai"; +import { v4 as uuidv4 } from "uuid"; export interface BubbleMenuItem { name: string; @@ -17,7 +32,9 @@ export interface BubbleMenuItem { icon: typeof IconBold; } -type EditorBubbleMenuProps = Omit; +type EditorBubbleMenuProps = Omit & { + editor: ReturnType; +}; export const EditorBubbleMenu: FC = (props) => { const [, setShowCommentPopup] = useAtom(showCommentPopupAtom); @@ -25,44 +42,44 @@ export const EditorBubbleMenu: FC = (props) => { const items: BubbleMenuItem[] = [ { - name: 'bold', - isActive: () => props.editor.isActive('bold'), + name: "bold", + isActive: () => props.editor.isActive("bold"), command: () => props.editor.chain().focus().toggleBold().run(), icon: IconBold, }, { - name: 'italic', - isActive: () => props.editor.isActive('italic'), + name: "italic", + isActive: () => props.editor.isActive("italic"), command: () => props.editor.chain().focus().toggleItalic().run(), icon: IconItalic, }, { - name: 'underline', - isActive: () => props.editor.isActive('underline'), + name: "underline", + isActive: () => props.editor.isActive("underline"), command: () => props.editor.chain().focus().toggleUnderline().run(), icon: IconUnderline, }, { - name: 'strike', - isActive: () => props.editor.isActive('strike'), + name: "strike", + isActive: () => props.editor.isActive("strike"), command: () => props.editor.chain().focus().toggleStrike().run(), icon: IconStrikethrough, }, { - name: 'code', - isActive: () => props.editor.isActive('code'), + name: "code", + isActive: () => props.editor.isActive("code"), command: () => props.editor.chain().focus().toggleCode().run(), icon: IconCode, }, ]; const commentItem: BubbleMenuItem = { - - name: 'comment', - isActive: () => props.editor.isActive('comment'), + name: "comment", + isActive: () => props.editor.isActive("comment"), command: () => { const commentId = uuidv4(); + // @ts-ignore props.editor.chain().focus().setCommentDecoration().run(); setDraftCommentId(commentId); setShowCommentPopup(true); @@ -76,13 +93,17 @@ export const EditorBubbleMenu: FC = (props) => { const { selection } = state; const { empty } = selection; - if (editor.isActive('image') || empty || isNodeSelection(selection)) { + if ( + props.editor.isActive("image") || + empty || + isNodeSelection(selection) + ) { return false; } return true; }, tippyOptions: { - moveTransition: 'transform 0.15s ease-out', + moveTransition: "transform 0.15s ease-out", onHidden: () => { setIsNodeSelectorOpen(false); setIsColorSelectorOpen(false); @@ -96,10 +117,7 @@ export const EditorBubbleMenu: FC = (props) => { const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); return ( - + = (props) => { {items.map((item, index) => ( - - + - ))} @@ -136,14 +158,17 @@ export const EditorBubbleMenu: FC = (props) => { /> - - + - ); }; diff --git a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx index bfcbfac2..a0c76148 100644 --- a/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/color-selector.tsx @@ -1,8 +1,8 @@ -import { Editor } from '@tiptap/core'; -import { Dispatch, FC, SetStateAction } from 'react'; -import { IconCheck, IconChevronDown } from '@tabler/icons-react'; -import { Button, Popover, rem, ScrollArea, Text } from '@mantine/core'; -import classes from './bubble-menu.module.css'; +import { Dispatch, FC, SetStateAction } from "react"; +import { IconCheck, IconChevronDown } from "@tabler/icons-react"; +import { Button, Popover, rem, ScrollArea, Text } from "@mantine/core"; +import classes from "./bubble-menu.module.css"; +import { useEditor } from "@tiptap/react"; export interface BubbleColorMenuItem { name: string; @@ -10,121 +10,125 @@ export interface BubbleColorMenuItem { } interface ColorSelectorProps { - editor: Editor; + editor: ReturnType; isOpen: boolean; setIsOpen: Dispatch>; } const TEXT_COLORS: BubbleColorMenuItem[] = [ { - name: 'Default', - color: '', + name: "Default", + color: "", }, { - name: 'Blue', - color: '#2563EB', + name: "Blue", + color: "#2563EB", }, { - name: 'Green', - color: '#008A00', + name: "Green", + color: "#008A00", }, { - name: 'Purple', - color: '#9333EA', + name: "Purple", + color: "#9333EA", }, { - name: 'Red', - color: '#E00000', + name: "Red", + color: "#E00000", }, { - name: 'Yellow', - color: '#EAB308', + name: "Yellow", + color: "#EAB308", }, { - name: 'Orange', - color: '#FFA500', + name: "Orange", + color: "#FFA500", }, { - name: 'Pink', - color: '#BA4081', + name: "Pink", + color: "#BA4081", }, { - name: 'Gray', - color: '#A8A29E', + name: "Gray", + color: "#A8A29E", }, ]; // TODO: handle dark mode const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [ { - name: 'Default', - color: '', + name: "Default", + color: "", }, { - name: 'Blue', - color: '#c1ecf9', + name: "Blue", + color: "#c1ecf9", }, { - name: 'Green', - color: '#acf79f', + name: "Green", + color: "#acf79f", }, { - name: 'Purple', - color: '#f6f3f8', + name: "Purple", + color: "#f6f3f8", }, { - name: 'Red', - color: '#fdebeb', + name: "Red", + color: "#fdebeb", }, { - name: 'Yellow', - color: '#fbf4a2', + name: "Yellow", + color: "#fbf4a2", }, { - name: 'Orange', - color: '#faebdd', + name: "Orange", + color: "#faebdd", }, { - name: 'Pink', - color: '#faf1f5', + name: "Pink", + color: "#faf1f5", }, { - name: 'Gray', - color: '#f1f1ef', + name: "Gray", + color: "#f1f1ef", }, ]; -export const ColorSelector: FC = - ({ editor, isOpen, setIsOpen }) => { +export const ColorSelector: FC = ({ + editor, + isOpen, + setIsOpen, +}) => { + const activeColorItem = TEXT_COLORS.find(({ color }) => + editor.isActive("textStyle", { color }), + ); - const activeColorItem = TEXT_COLORS.find(({ color }) => - editor.isActive('textStyle', { color }), - ); + const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) => + editor.isActive("highlight", { color }), + ); - const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) => - editor.isActive('highlight', { color }), - ); + return ( + + + ))} - BACKGROUND + + BACKGROUND + {HIGHLIGHT_COLORS.map(({ name, color }, index) => ( ))} - - - - - ); - }; + + + + ); +}; diff --git a/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx index 6105bc63..a1ad1c56 100644 --- a/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx @@ -1,20 +1,23 @@ -import { Editor } from '@tiptap/core'; -import React, { Dispatch, FC, SetStateAction } from 'react'; +import React, { Dispatch, FC, SetStateAction } from "react"; import { IconBlockquote, - IconCheck, IconCheckbox, IconChevronDown, IconCode, + IconCheck, + IconCheckbox, + IconChevronDown, + IconCode, IconH1, IconH2, IconH3, IconList, IconListNumbers, IconTypography, -} from '@tabler/icons-react'; -import { Popover, Button, rem, ScrollArea } from '@mantine/core'; -import classes from '@/features/editor/components/bubble-menu/bubble-menu.module.css'; +} from "@tabler/icons-react"; +import { Popover, Button, rem, ScrollArea } from "@mantine/core"; +import classes from "@/features/editor/components/bubble-menu/bubble-menu.module.css"; +import { useEditor } from "@tiptap/react"; interface NodeSelectorProps { - editor: Editor; + editor: ReturnType; isOpen: boolean; setIsOpen: Dispatch>; } @@ -26,123 +29,121 @@ export interface BubbleMenuItem { isActive: () => boolean; } -export const NodeSelector: FC = - ({ editor, isOpen, setIsOpen }) => { +export const NodeSelector: FC = ({ + editor, + isOpen, + setIsOpen, +}) => { + const items: BubbleMenuItem[] = [ + { + name: "Text", + icon: IconTypography, + command: () => + editor.chain().focus().toggleNode("paragraph", "paragraph").run(), + isActive: () => + editor.isActive("paragraph") && + !editor.isActive("bulletList") && + !editor.isActive("orderedList"), + }, + { + name: "Heading 1", + icon: IconH1, + command: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), + isActive: () => editor.isActive("heading", { level: 1 }), + }, + { + name: "Heading 2", + icon: IconH2, + command: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), + isActive: () => editor.isActive("heading", { level: 2 }), + }, + { + name: "Heading 3", + icon: IconH3, + command: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), + isActive: () => editor.isActive("heading", { level: 3 }), + }, + { + name: "To-do List", + icon: IconCheckbox, + command: () => editor.chain().focus().toggleTaskList().run(), + isActive: () => editor.isActive("taskItem"), + }, + { + name: "Bullet List", + icon: IconList, + command: () => editor.chain().focus().toggleBulletList().run(), + isActive: () => editor.isActive("bulletList"), + }, + { + name: "Numbered List", + icon: IconListNumbers, + command: () => editor.chain().focus().toggleOrderedList().run(), + isActive: () => editor.isActive("orderedList"), + }, + { + name: "Blockquote", + icon: IconBlockquote, + command: () => + editor + .chain() + .focus() + .toggleNode("paragraph", "paragraph") + .toggleBlockquote() + .run(), + isActive: () => editor.isActive("blockquote"), + }, + { + name: "Code", + icon: IconCode, + command: () => editor.chain().focus().toggleCodeBlock().run(), + isActive: () => editor.isActive("codeBlock"), + }, + ]; - const items: BubbleMenuItem[] = [ - { - name: 'Text', - icon: IconTypography, - command: () => - editor.chain().focus().toggleNode('paragraph', 'paragraph').run(), - isActive: () => - editor.isActive('paragraph') && - !editor.isActive('bulletList') && - !editor.isActive('orderedList'), - }, - { - name: 'Heading 1', - icon: IconH1, - command: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), - isActive: () => editor.isActive('heading', { level: 1 }), - }, - { - name: 'Heading 2', - icon: IconH2, - command: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), - isActive: () => editor.isActive('heading', { level: 2 }), - }, - { - name: 'Heading 3', - icon: IconH3, - command: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), - isActive: () => editor.isActive('heading', { level: 3 }), - }, - { - name: 'To-do List', - icon: IconCheckbox, - command: () => editor.chain().focus().toggleTaskList().run(), - isActive: () => editor.isActive('taskItem'), - }, - { - name: 'Bullet List', - icon: IconList, - command: () => editor.chain().focus().toggleBulletList().run(), - isActive: () => editor.isActive('bulletList'), - }, - { - name: 'Numbered List', - icon: IconListNumbers, - command: () => editor.chain().focus().toggleOrderedList().run(), - isActive: () => editor.isActive('orderedList'), - }, - { - name: 'Blockquote', - icon: IconBlockquote, - command: () => - editor - .chain() - .focus() - .toggleNode('paragraph', 'paragraph') - .toggleBlockquote() - .run(), - isActive: () => editor.isActive('blockquote'), - }, - { - name: 'Code', - icon: IconCode, - command: () => editor.chain().focus().toggleCodeBlock().run(), - isActive: () => editor.isActive('codeBlock'), - }, - ]; - - const activeItem = items.filter((item) => item.isActive()).pop() ?? { - name: 'Multiple', - }; - - return ( - - - - - - - - - - - - - {items.map((item, index) => ( - - - ))} - - - - - - - ); + const activeItem = items.filter((item) => item.isActive()).pop() ?? { + name: "Multiple", }; + + return ( + + + + + + + + + {items.map((item, index) => ( + + ))} + + + + + ); +}; diff --git a/apps/client/src/features/editor/components/slash-menu/render-items.ts b/apps/client/src/features/editor/components/slash-menu/render-items.ts index 479ed3a9..db6424e8 100644 --- a/apps/client/src/features/editor/components/slash-menu/render-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/render-items.ts @@ -1,14 +1,16 @@ -import { Editor } from '@tiptap/core'; -import { ReactRenderer } from '@tiptap/react'; -import CommandList from '@/features/editor/components/slash-menu/command-list'; -import tippy from 'tippy.js'; +import { ReactRenderer, useEditor } from "@tiptap/react"; +import CommandList from "@/features/editor/components/slash-menu/command-list"; +import tippy from "tippy.js"; const renderItems = () => { let component: ReactRenderer | null = null; let popup: any | null = null; return { - onStart: (props: { editor: Editor; clientRect: DOMRect }) => { + onStart: (props: { + editor: ReturnType; + clientRect: DOMRect; + }) => { component = new ReactRenderer(CommandList, { props, editor: props.editor, @@ -19,17 +21,20 @@ const renderItems = () => { } // @ts-ignore - popup = tippy('body', { + popup = tippy("body", { getReferenceClientRect: props.clientRect, appendTo: () => document.body, content: component.element, showOnCreate: true, interactive: true, - trigger: 'manual', - placement: 'bottom-start', + trigger: "manual", + placement: "bottom-start", }); }, - onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => { + onUpdate: (props: { + editor: ReturnType; + clientRect: DOMRect; + }) => { component?.updateProps(props); if (!props.clientRect) { @@ -42,7 +47,7 @@ const renderItems = () => { }); }, onKeyDown: (props: { event: KeyboardEvent }) => { - if (props.event.key === 'Escape') { + if (props.event.key === "Escape") { popup?.[0].hide(); return true; diff --git a/apps/client/src/features/editor/components/slash-menu/types.ts b/apps/client/src/features/editor/components/slash-menu/types.ts index 7216ede7..cf5bd3e4 100644 --- a/apps/client/src/features/editor/components/slash-menu/types.ts +++ b/apps/client/src/features/editor/components/slash-menu/types.ts @@ -1,16 +1,17 @@ -import { Editor, Range } from '@tiptap/core'; +import { Range } from "@tiptap/core"; +import { useEditor } from "@tiptap/react"; export type CommandProps = { - editor: Editor; + editor: ReturnType; range: Range; -} +}; export type CommandListProps = { items: SlashMenuGroupedItemsType; command: (item: SlashMenuItemType) => void; - editor: Editor; + editor: ReturnType; range: Range; -} +}; export type SlashMenuItemType = { title: string; @@ -19,8 +20,8 @@ export type SlashMenuItemType = { separator?: true; searchTerms: string[]; command: (props: CommandProps) => void; - disable?: (editor: Editor) => boolean; -} + disable?: (editor: ReturnType) => boolean; +}; export type SlashMenuGroupedItemsType = { [category: string]: SlashMenuItemType[]; diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index afffc006..9816f94d 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -1,35 +1,35 @@ -import { StarterKit } from '@tiptap/starter-kit'; -import { Placeholder } from '@tiptap/extension-placeholder'; -import { TextAlign } from '@tiptap/extension-text-align'; -import { TaskList } from '@tiptap/extension-task-list'; -import { TaskItem } from '@tiptap/extension-task-item'; -import { Underline } from '@tiptap/extension-underline'; -import { Link } from '@tiptap/extension-link'; -import { Superscript } from '@tiptap/extension-superscript'; -import SubScript from '@tiptap/extension-subscript'; -import { Highlight } from '@tiptap/extension-highlight'; -import { Typography } from '@tiptap/extension-typography'; -import DragAndDrop from '@/features/editor/extensions/drag-handle'; -import { TextStyle } from '@tiptap/extension-text-style'; -import { Color } from '@tiptap/extension-color'; -import SlashCommand from '@/features/editor/extensions/slash-command'; -import { Collaboration } from '@tiptap/extension-collaboration'; -import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'; -import { HocuspocusProvider } from '@hocuspocus/provider'; -import { Comment, TrailingNode } from '@docmost/editor-ext'; +import { StarterKit } from "@tiptap/starter-kit"; +import { Placeholder } from "@tiptap/extension-placeholder"; +import { TextAlign } from "@tiptap/extension-text-align"; +import { TaskList } from "@tiptap/extension-task-list"; +import { TaskItem } from "@tiptap/extension-task-item"; +import { Underline } from "@tiptap/extension-underline"; +import { Link } from "@tiptap/extension-link"; +import { Superscript } from "@tiptap/extension-superscript"; +import SubScript from "@tiptap/extension-subscript"; +import { Highlight } from "@tiptap/extension-highlight"; +import { Typography } from "@tiptap/extension-typography"; +import DragAndDrop from "@/features/editor/extensions/drag-handle"; +import { TextStyle } from "@tiptap/extension-text-style"; +import { Color } from "@tiptap/extension-color"; +import SlashCommand from "@/features/editor/extensions/slash-command"; +import { Collaboration } from "@tiptap/extension-collaboration"; +import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor"; +import { HocuspocusProvider } from "@hocuspocus/provider"; +import { Comment, TrailingNode } from "@docmost/editor-ext"; export const mainExtensions = [ StarterKit.configure({ history: false, dropcursor: { width: 3, - color: '#70CFF8', + color: "#70CFF8", }, }), Placeholder.configure({ placeholder: 'Enter "/" for commands', }), - TextAlign.configure({ types: ['heading', 'paragraph'] }), + TextAlign.configure({ types: ["heading", "paragraph"] }), TaskList, TaskItem.configure({ nested: true, @@ -49,10 +49,10 @@ export const mainExtensions = [ SlashCommand, Comment.configure({ HTMLAttributes: { - class: 'comment-mark', + class: "comment-mark", }, }), -]; +] as any; type CollabExtensions = (provider: HocuspocusProvider) => any[]; diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index bab17cc2..161eb7ab 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -1,28 +1,38 @@ -import '@/features/editor/styles/index.css'; -import React, { - useEffect, - useLayoutEffect, - useMemo, - useState, -} from 'react'; -import { IndexeddbPersistence } from 'y-indexeddb'; -import * as Y from 'yjs'; -import { HocuspocusProvider } from '@hocuspocus/provider'; -import { EditorContent, useEditor } from '@tiptap/react'; -import { collabExtensions, mainExtensions } from '@/features/editor/extensions/extensions'; -import { useAtom } from 'jotai'; -import { authTokensAtom } from '@/features/auth/atoms/auth-tokens-atom'; -import useCollaborationUrl from '@/features/editor/hooks/use-collaboration-url'; -import { currentUserAtom } from '@/features/user/atoms/current-user-atom'; -import { pageEditorAtom } from '@/features/editor/atoms/editor-atoms'; -import { asideStateAtom } from '@/components/navbar/atoms/sidebar-atom'; -import { activeCommentIdAtom, showCommentPopupAtom } from '@/features/comment/atoms/comment-atom'; -import CommentDialog from '@/features/comment/components/comment-dialog'; -import EditorSkeleton from '@/features/editor/components/editor-skeleton'; -import { EditorBubbleMenu } from '@/features/editor/components/bubble-menu/bubble-menu'; +import "@/features/editor/styles/index.css"; +import React, { useEffect, useLayoutEffect, useMemo, useState } from "react"; +import { IndexeddbPersistence } from "y-indexeddb"; +import * as Y from "yjs"; +import { HocuspocusProvider } from "@hocuspocus/provider"; +import { EditorContent, useEditor } from "@tiptap/react"; +import { + collabExtensions, + mainExtensions, +} from "@/features/editor/extensions/extensions"; +import { useAtom } from "jotai"; +import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom"; +import useCollaborationUrl from "@/features/editor/hooks/use-collaboration-url"; +import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; +import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms"; +import { asideStateAtom } from "@/components/navbar/atoms/sidebar-atom"; +import { + activeCommentIdAtom, + showCommentPopupAtom, +} from "@/features/comment/atoms/comment-atom"; +import CommentDialog from "@/features/comment/components/comment-dialog"; +import EditorSkeleton from "@/features/editor/components/editor-skeleton"; +import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu"; -const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D']; -const getRandomElement = list => list[Math.floor(Math.random() * list.length)]; +const colors = [ + "#958DF1", + "#F98181", + "#FBBC88", + "#FAF594", + "#70CFF8", + "#94FADB", + "#B9F18D", +]; +const getRandomElement = (list) => + list[Math.floor(Math.random() * list.length)]; const getRandomColor = () => getRandomElement(colors); interface PageEditorProps { @@ -30,7 +40,10 @@ interface PageEditorProps { editable?: boolean; } -export default function PageEditor({ pageId, editable = true }: PageEditorProps) { +export default function PageEditor({ + pageId, + editable = true, +}: PageEditorProps) { const [token] = useAtom(authTokensAtom); const collaborationURL = useCollaborationUrl(); const [currentUser] = useAtom(currentUserAtom); @@ -46,12 +59,9 @@ export default function PageEditor({ pageId, editable = true }: PageEditorProps) const [isRemoteSynced, setRemoteSynced] = useState(false); const localProvider = useMemo(() => { - const provider = new IndexeddbPersistence( - pageId, - ydoc, - ); + const provider = new IndexeddbPersistence(pageId, ydoc); - provider.on('synced', () => { + provider.on("synced", () => { setLocalSynced(true); }); @@ -67,7 +77,7 @@ export default function PageEditor({ pageId, editable = true }: PageEditorProps) connect: false, }); - provider.on('synced', () => { + provider.on("synced", () => { setRemoteSynced(true); }); @@ -85,10 +95,7 @@ export default function PageEditor({ pageId, editable = true }: PageEditorProps) }; }, [remoteProvider, localProvider]); - const extensions = [ - ...mainExtensions, - ...collabExtensions(remoteProvider), - ]; + const extensions = [...mainExtensions, ...collabExtensions(remoteProvider)]; const editor = useEditor( { @@ -97,8 +104,8 @@ export default function PageEditor({ pageId, editable = true }: PageEditorProps) editorProps: { handleDOMEvents: { keydown: (_view, event) => { - if (['ArrowUp', 'ArrowDown', 'Enter'].includes(event.key)) { - const slashCommand = document.querySelector('#slash-command'); + if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { + const slashCommand = document.querySelector("#slash-command"); if (slashCommand) { return true; } @@ -118,14 +125,18 @@ export default function PageEditor({ pageId, editable = true }: PageEditorProps) useEffect(() => { if (editor && currentUser.user) { - editor.chain().focus().updateUser({ ...currentUser.user, color: getRandomColor() }).run(); + editor + .chain() + .focus() + .updateUser({ ...currentUser.user, color: getRandomColor() }) + .run(); } }, [editor, currentUser.user]); const handleActiveCommentEvent = (event) => { const { commentId } = event.detail; setActiveCommentId(commentId); - setAsideState({ tab: 'comments', isAsideOpen: true }); + setAsideState({ tab: "comments", isAsideOpen: true }); const selector = `div[data-comment-id="${commentId}"]`; const commentElement = document.querySelector(selector); @@ -133,16 +144,19 @@ export default function PageEditor({ pageId, editable = true }: PageEditorProps) }; useEffect(() => { - document.addEventListener('ACTIVE_COMMENT_EVENT', handleActiveCommentEvent); + document.addEventListener("ACTIVE_COMMENT_EVENT", handleActiveCommentEvent); return () => { - document.removeEventListener('ACTIVE_COMMENT_EVENT', handleActiveCommentEvent); + document.removeEventListener( + "ACTIVE_COMMENT_EVENT", + handleActiveCommentEvent, + ); }; }, []); useEffect(() => { setActiveCommentId(null); setShowCommentPopup(false); - setAsideState({ tab: '', isAsideOpen: false }); + setAsideState({ tab: "", isAsideOpen: false }); }, [pageId]); const isSynced = isLocalSynced || isRemoteSynced; @@ -165,6 +179,7 @@ export default function PageEditor({ pageId, editable = true }: PageEditorProps) )} - ) : ; - + ) : ( + + ); }