From 2a3db4e7cbd60845ee508136a04ef6c826f8c2cb Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:03:53 +0000 Subject: [PATCH] refactor page editor --- .../src/components/layouts/shell.module.css | 12 +- .../comment/components/comment-list-item.tsx | 4 +- .../atoms/{editorAtom.ts => editor-atoms.ts} | 2 +- .../editor/components/editor-skeleton.tsx | 39 ++++ .../editor/{ => extensions}/extensions.ts | 8 +- client/src/features/editor/full-editor.tsx | 4 +- .../editor/{editor.tsx => page-editor.tsx} | 220 +++++++++--------- client/src/features/editor/title-editor.tsx | 17 +- .../components/history-editor.tsx | 2 +- .../page-history/components/history-list.tsx | 4 +- client/src/pages/page/page.tsx | 2 +- 11 files changed, 173 insertions(+), 141 deletions(-) rename client/src/features/editor/atoms/{editorAtom.ts => editor-atoms.ts} (70%) create mode 100644 client/src/features/editor/components/editor-skeleton.tsx rename client/src/features/editor/{ => extensions}/extensions.ts (89%) rename client/src/features/editor/{editor.tsx => page-editor.tsx} (50%) diff --git a/client/src/components/layouts/shell.module.css b/client/src/components/layouts/shell.module.css index f48f7422..3c113f44 100644 --- a/client/src/components/layouts/shell.module.css +++ b/client/src/components/layouts/shell.module.css @@ -1,15 +1,15 @@ -@media (max-width: 992px) { - .header, - .footer { +.header, +.footer { + @media (max-width: 992px) { [data-layout='alt'] & { --_section-right: var(--app-shell-aside-offset, 0px); } - } + } -@media (min-width: 993px) { - .aside { +.aside { + @media (min-width: 993px) { background: var(--mantine-color-gray-light); [data-layout='alt'] & { diff --git a/client/src/features/comment/components/comment-list-item.tsx b/client/src/features/comment/components/comment-list-item.tsx index ae63d623..2e2501b2 100644 --- a/client/src/features/comment/components/comment-list-item.tsx +++ b/client/src/features/comment/components/comment-list-item.tsx @@ -4,7 +4,7 @@ import classes from './comment.module.css'; import { useAtomValue } from 'jotai'; import { timeAgo } from '@/lib/time'; import CommentEditor from '@/features/comment/components/comment-editor'; -import { editorAtom } from '@/features/editor/atoms/editorAtom'; +import { editorAtoms } from '@/features/editor/atoms/editor-atoms'; import CommentActions from '@/features/comment/components/comment-actions'; import CommentMenu from '@/features/comment/components/comment-menu'; import { useHover } from '@mantine/hooks'; @@ -22,7 +22,7 @@ function CommentListItem({ comment }: CommentListItemProps) { const [isEditing, setIsEditing] = useState(false); const [isLoading, setIsLoading] = useState(false); - const editor = useAtomValue(editorAtom); + const editor = useAtomValue(editorAtoms); const [content, setContent] = useState(comment.content); const updateCommentMutation = useUpdateCommentMutation(); const deleteCommentMutation = useDeleteCommentMutation(comment.pageId); diff --git a/client/src/features/editor/atoms/editorAtom.ts b/client/src/features/editor/atoms/editor-atoms.ts similarity index 70% rename from client/src/features/editor/atoms/editorAtom.ts rename to client/src/features/editor/atoms/editor-atoms.ts index 1d168110..fa4fcb1d 100644 --- a/client/src/features/editor/atoms/editorAtom.ts +++ b/client/src/features/editor/atoms/editor-atoms.ts @@ -1,6 +1,6 @@ import { atom } from 'jotai'; import { Editor } from '@tiptap/core'; -export const editorAtom = atom(null); +export const editorAtoms = atom(null); export const titleEditorAtom = atom(null); diff --git a/client/src/features/editor/components/editor-skeleton.tsx b/client/src/features/editor/components/editor-skeleton.tsx new file mode 100644 index 00000000..fd8d1323 --- /dev/null +++ b/client/src/features/editor/components/editor-skeleton.tsx @@ -0,0 +1,39 @@ +import React, { useState, useEffect } from 'react'; +import { Skeleton } from '@mantine/core'; + +function EditorSkeleton() { + const [showSkeleton, setShowSkeleton] = useState(false); + + useEffect(() => { + const timer = setTimeout(() => setShowSkeleton(true), 100); + return () => clearTimeout(timer); + }, []); + + if (!showSkeleton) { + return null; + } + + return ( + <> + + + + + + + + + + + + + + + + + + + ); +} + +export default EditorSkeleton; diff --git a/client/src/features/editor/extensions.ts b/client/src/features/editor/extensions/extensions.ts similarity index 89% rename from client/src/features/editor/extensions.ts rename to client/src/features/editor/extensions/extensions.ts index 624f5d88..c1c51145 100644 --- a/client/src/features/editor/extensions.ts +++ b/client/src/features/editor/extensions/extensions.ts @@ -17,7 +17,7 @@ import SlashCommand from '@/features/editor/extensions/slash-command'; import { Collaboration } from '@tiptap/extension-collaboration'; import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'; import { Comment } from '@/features/editor/extensions/comment/comment'; -import * as Y from 'yjs'; +import { HocuspocusProvider } from '@hocuspocus/provider'; export const mainExtensions = [ StarterKit.configure({ @@ -55,11 +55,11 @@ export const mainExtensions = [ }), ]; -type CollabExtensions = (ydoc: Y.Doc, provider: any) => any[]; +type CollabExtensions = (provider: HocuspocusProvider) => any[]; -export const collabExtensions: CollabExtensions = (ydoc, provider) => [ +export const collabExtensions: CollabExtensions = (provider) => [ Collaboration.configure({ - document: ydoc, + document: provider.document, }), CollaborationCursor.configure({ provider, diff --git a/client/src/features/editor/full-editor.tsx b/client/src/features/editor/full-editor.tsx index 027a1de6..7f1f4c91 100644 --- a/client/src/features/editor/full-editor.tsx +++ b/client/src/features/editor/full-editor.tsx @@ -1,7 +1,7 @@ import classes from '@/features/editor/styles/editor.module.css'; -import Editor from '@/features/editor/editor'; import React from 'react'; import { TitleEditor } from '@/features/editor/title-editor'; +import PageEditor from '@/features/editor/page-editor'; export interface FullEditorProps { pageId: string; @@ -13,7 +13,7 @@ export function FullEditor({ pageId, title }: FullEditorProps) { return (
- +
); diff --git a/client/src/features/editor/editor.tsx b/client/src/features/editor/page-editor.tsx similarity index 50% rename from client/src/features/editor/editor.tsx rename to client/src/features/editor/page-editor.tsx index 126b7e6c..b780e511 100644 --- a/client/src/features/editor/editor.tsx +++ b/client/src/features/editor/page-editor.tsx @@ -1,140 +1,120 @@ import '@/features/editor/styles/index.css'; - -import { HocuspocusProvider } from '@hocuspocus/provider'; +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 React, { useEffect, useState } from 'react'; +import { collabExtensions, mainExtensions } from '@/features/editor/extensions/extensions'; import { useAtom } from 'jotai'; -import { currentUserAtom } from '@/features/user/atoms/current-user-atom'; import { authTokensAtom } from '@/features/auth/atoms/auth-tokens-atom'; import useCollaborationUrl from '@/features/editor/hooks/use-collaboration-url'; -import { IndexeddbPersistence } from 'y-indexeddb'; -import { EditorBubbleMenu } from '@/features/editor/components/bubble-menu/bubble-menu'; +import { currentUserAtom } from '@/features/user/atoms/current-user-atom'; +import { editorAtoms } 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 { editorAtom, titleEditorAtom } from '@/features/editor/atoms/editorAtom'; -import { collabExtensions, mainExtensions } from '@/features/editor/extensions'; - -interface EditorProps { - pageId: string, -} +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 getRandomColor = () => getRandomElement(colors); -export default function Editor({ pageId }: EditorProps) { +interface PageEditorProps { + pageId: string; + editable?: boolean; +} + +export default function PageEditor({ pageId, editable = true }: PageEditorProps) { const [token] = useAtom(authTokensAtom); const collaborationURL = useCollaborationUrl(); - const [provider, setProvider] = useState(); - const [yDoc] = useState(() => new Y.Doc()); - const [isLocalSynced, setLocalSynced] = useState(false); - const [isRemoteSynced, setRemoteSynced] = useState(false); - - useEffect(() => { - if (token) { - const indexeddbProvider = new IndexeddbPersistence(pageId, yDoc); - - const provider = new HocuspocusProvider({ - url: collaborationURL, - name: pageId, - document: yDoc, - token: token.accessToken, - }); - - indexeddbProvider.on('synced', () => { - console.log('index synced'); - setLocalSynced(true); - }); - - provider.on('synced', () => { - console.log('remote synced'); - setRemoteSynced(true); - }); - - setProvider(provider); - return () => { - setProvider(null); - provider.destroy(); - indexeddbProvider.destroy(); - setRemoteSynced(false); - setLocalSynced(false); - }; - } - }, [pageId, token]); - - if (!provider) { - return null; - } - - const isSynced = isLocalSynced || isRemoteSynced; - if (isSynced){ - window.scrollTo(0, 0); - } - return (isSynced && ); -} - -interface TiptapEditorProps { - ydoc: Y.Doc, - provider: HocuspocusProvider, - pageId: string, -} - -function TiptapEditor({ ydoc, provider, pageId }: TiptapEditorProps) { const [currentUser] = useAtom(currentUserAtom); - const [, setEditor] = useAtom(editorAtom); - const [titleEditor] = useAtom(titleEditorAtom); - const [asideState, setAsideState] = useAtom(asideStateAtom); + const [, setEditor] = useAtom(editorAtoms); + const [, setAsideState] = useAtom(asideStateAtom); + const [, setActiveCommentId] = useAtom(activeCommentIdAtom); const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom); + const ydoc = useMemo(() => new Y.Doc(), [pageId]); + + const [isLocalSynced, setLocalSynced] = useState(false); + const [isRemoteSynced, setRemoteSynced] = useState(false); + + const localProvider = useMemo(() => { + const provider = new IndexeddbPersistence( + pageId, + ydoc, + ); + + provider.on('synced', () => { + setLocalSynced(true); + }); + + return provider; + }, [pageId, ydoc]); + + const remoteProvider = useMemo(() => { + const provider = new HocuspocusProvider({ + name: pageId, + url: collaborationURL, + document: ydoc, + token: token?.accessToken, + connect: false, + }); + + provider.on('synced', () => { + setRemoteSynced(true); + }); + + return provider; + }, [ydoc, pageId, token?.accessToken]); + + useLayoutEffect(() => { + remoteProvider.connect(); + + return () => { + setRemoteSynced(false); + setLocalSynced(false); + remoteProvider.destroy(); + localProvider.destroy(); + }; + }, [remoteProvider, localProvider]); + const extensions = [ ...mainExtensions, - ...collabExtensions(ydoc, provider), + ...collabExtensions(remoteProvider), ]; - const editor = useEditor({ - extensions: extensions, - autofocus: 0, - editorProps: { - handleDOMEvents: { - keydown: (_view, event) => { - if (['ArrowUp', 'ArrowDown', 'Enter'].includes(event.key)) { - const slashCommand = document.querySelector('#slash-command'); - if (slashCommand) { - return true; + const editor = useEditor( + { + extensions, + editable, + editorProps: { + handleDOMEvents: { + keydown: (_view, event) => { + if (['ArrowUp', 'ArrowDown', 'Enter'].includes(event.key)) { + const slashCommand = document.querySelector('#slash-command'); + if (slashCommand) { + return true; + } } - } + }, }, }, + onCreate({ editor }) { + if (editor) { + // @ts-ignore + setEditor(editor); + } + }, }, - onCreate({ editor }) { - if (editor) { - // @ts-ignore - setEditor(editor); - } - }, - onUpdate({ editor }) { - const { selection } = editor.state; - if (!selection.empty) { - return; - } - - const viewportCoords = editor.view.coordsAtPos(selection.from); - const absoluteOffset = window.scrollY + viewportCoords.top; - window.scrollTo( - window.scrollX, - absoluteOffset - (window.innerHeight / 2), - ); - }, - }); - - useEffect(() => { - setTimeout(() => { - titleEditor?.commands.focus('end'); - }, 200); - }, [editor]); + [pageId, editable, remoteProvider], + ); useEffect(() => { if (editor && currentUser.user) { @@ -165,18 +145,26 @@ function TiptapEditor({ ydoc, provider, pageId }: TiptapEditorProps) { setAsideState({ tab: '', isAsideOpen: false }); }, [pageId]); - return ( + const isSynced = isLocalSynced || isRemoteSynced; + + return isSynced ? (
- {editor && - (
- + {isSynced && ( +
+ {editor && editor.isEditable && ( +
+ +
+ )} + {showCommentPopup && ( )} -
)} +
+ )}
- ); -} + ) : ; +} diff --git a/client/src/features/editor/title-editor.tsx b/client/src/features/editor/title-editor.tsx index 3e9ec1e6..bb6af116 100644 --- a/client/src/features/editor/title-editor.tsx +++ b/client/src/features/editor/title-editor.tsx @@ -6,7 +6,7 @@ import { Heading } from '@tiptap/extension-heading'; import { Text } from '@tiptap/extension-text'; import { Placeholder } from '@tiptap/extension-placeholder'; import { useAtomValue } from 'jotai'; -import { editorAtom, titleEditorAtom } from '@/features/editor/atoms/editorAtom'; +import { editorAtoms, titleEditorAtom } from '@/features/editor/atoms/editor-atoms'; import { useUpdatePageMutation } from '@/features/page/queries/page-query'; import { useDebouncedValue } from '@mantine/hooks'; import { useAtom } from 'jotai'; @@ -22,7 +22,7 @@ export function TitleEditor({ pageId, title }: TitleEditorProps) { const [debouncedTitleState, setDebouncedTitleState] = useState(''); const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 1000); const updatePageMutation = useUpdatePageMutation(); - const contentEditor = useAtomValue(editorAtom); + const pageEditor = useAtomValue(editorAtoms); const [, setTitleEditor] = useAtom(titleEditorAtom); const [treeData, setTreeData] = useAtom(treeDataAtom); @@ -58,7 +58,6 @@ export function TitleEditor({ pageId, title }: TitleEditorProps) { const newTreeData = updateTreeNodeName(treeData, pageId, debouncedTitle); setTreeData(newTreeData); - } }, [debouncedTitle]); @@ -66,10 +65,16 @@ export function TitleEditor({ pageId, title }: TitleEditorProps) { if (titleEditor && title !== titleEditor.getText()) { titleEditor.commands.setContent(title); } - }, [title, titleEditor]); + }, [pageId, title, titleEditor]); + + useEffect(() => { + setTimeout(() => { + titleEditor?.commands.focus('end'); + }, 500); + }, [titleEditor]); function handleTitleKeyDown(event) { - if (!titleEditor || !contentEditor || event.shiftKey) return; + if (!titleEditor || !pageEditor || event.shiftKey) return; const { key } = event; const { $head } = titleEditor.state.selection; @@ -78,7 +83,7 @@ export function TitleEditor({ pageId, title }: TitleEditorProps) { (key === 'ArrowRight' && !$head.nodeAfter); if (shouldFocusEditor) { - contentEditor.commands.focus('start'); + pageEditor.commands.focus('start'); } } diff --git a/client/src/features/page-history/components/history-editor.tsx b/client/src/features/page-history/components/history-editor.tsx index a456f4ee..4b5839af 100644 --- a/client/src/features/page-history/components/history-editor.tsx +++ b/client/src/features/page-history/components/history-editor.tsx @@ -1,7 +1,7 @@ import '@/features/editor/styles/index.css'; import React, { useEffect } from 'react'; import { EditorContent, useEditor } from '@tiptap/react'; -import { mainExtensions } from '@/features/editor/extensions'; +import { mainExtensions } from '@/features/editor/extensions/extensions'; import { Title } from '@mantine/core'; export interface HistoryEditorProps { diff --git a/client/src/features/page-history/components/history-list.tsx b/client/src/features/page-history/components/history-list.tsx index 1d068d6b..63d9ca2a 100644 --- a/client/src/features/page-history/components/history-list.tsx +++ b/client/src/features/page-history/components/history-list.tsx @@ -5,7 +5,7 @@ import { activeHistoryIdAtom, historyAtoms } from '@/features/page-history/atoms import { useAtom } from 'jotai'; import { useCallback, useEffect } from 'react'; import { Button, ScrollArea, Group, Divider, Text } from '@mantine/core'; -import { editorAtom, titleEditorAtom } from '@/features/editor/atoms/editorAtom'; +import { editorAtoms, titleEditorAtom } from '@/features/editor/atoms/editor-atoms'; import { modals } from '@mantine/modals'; import { notifications } from '@mantine/notifications'; @@ -15,7 +15,7 @@ function HistoryList() { const { data, isLoading, isError } = usePageHistoryListQuery(pageId); const { data: activeHistoryData } = usePageHistoryQuery(activeHistoryId); - const [mainEditor] = useAtom(editorAtom); + const [mainEditor] = useAtom(editorAtoms); const [mainEditorTitle] = useAtom(titleEditorAtom); const [, setHistoryModalOpen] = useAtom(historyAtoms); diff --git a/client/src/pages/page/page.tsx b/client/src/pages/page/page.tsx index 871bf66f..02c50b4b 100644 --- a/client/src/pages/page/page.tsx +++ b/client/src/pages/page/page.tsx @@ -18,7 +18,7 @@ export default function Page() { return ( data && (
- +
)