mirror of
https://github.com/docmost/docmost.git
synced 2026-08-19 18:51:36 +10:00
Merge branch 'main' into confluence-importer
This commit is contained in:
@@ -36,7 +36,7 @@
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.14.3",
|
||||
"alfaaz": "1.1.0",
|
||||
"axios": "1.16.0",
|
||||
"axios": "1.18.1",
|
||||
"blueimp-load-image": "5.16.0",
|
||||
"clsx": "2.1.1",
|
||||
"file-saver": "2.0.5",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
HocuspocusProviderWebsocket,
|
||||
WebSocketStatus,
|
||||
} from "@hocuspocus/provider";
|
||||
import { getCollaborationUrl } from "@/lib/config.ts";
|
||||
|
||||
const RELEASE_GRACE_MS = 5000;
|
||||
|
||||
let socket: HocuspocusProviderWebsocket | null = null;
|
||||
let editorCount = 0;
|
||||
let releaseTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
export function getCollabSocket(): HocuspocusProviderWebsocket {
|
||||
if (!socket) {
|
||||
socket = new HocuspocusProviderWebsocket({
|
||||
url: getCollaborationUrl(),
|
||||
autoConnect: false,
|
||||
});
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
export function acquireCollabSocket(): void {
|
||||
editorCount++;
|
||||
if (releaseTimer) {
|
||||
clearTimeout(releaseTimer);
|
||||
releaseTimer = null;
|
||||
}
|
||||
const collabSocket = getCollabSocket();
|
||||
collabSocket.shouldConnect = true;
|
||||
if (collabSocket.status === WebSocketStatus.Disconnected) {
|
||||
collabSocket.connect();
|
||||
}
|
||||
}
|
||||
|
||||
export function releaseCollabSocket(): void {
|
||||
editorCount--;
|
||||
if (editorCount > 0) return;
|
||||
if (releaseTimer) clearTimeout(releaseTimer);
|
||||
releaseTimer = setTimeout(() => {
|
||||
releaseTimer = null;
|
||||
if (editorCount === 0) {
|
||||
socket?.disconnect();
|
||||
}
|
||||
}, RELEASE_GRACE_MS);
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 45px;
|
||||
background: var(--mantine-color-body);
|
||||
border-bottom: 1px solid
|
||||
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
|
||||
|
||||
@@ -31,8 +31,6 @@ export const FixedToolbar: FC<FixedToolbarProps> = ({
|
||||
const workspace = useAtomValue(workspaceAtom);
|
||||
const isGenerativeAiEnabled = workspace?.settings?.ai?.generative === true;
|
||||
|
||||
if (!editor || !state) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -49,22 +47,26 @@ export const FixedToolbar: FC<FixedToolbarProps> = ({
|
||||
<div className={classes.divider} />
|
||||
</>
|
||||
)} */}
|
||||
<BlockTypeGroup editor={editor} />
|
||||
<div className={classes.divider} />
|
||||
<InlineMarksGroup editor={editor} state={state} />
|
||||
<div className={classes.divider} />
|
||||
<ColorGroup editor={editor} />
|
||||
<div className={classes.divider} />
|
||||
<ListsGroup editor={editor} state={state} />
|
||||
<div className={classes.divider} />
|
||||
<AlignmentGroup editor={editor} />
|
||||
<div className={classes.divider} />
|
||||
<MediaGroup editor={editor} templateMode={templateMode} />
|
||||
<div className={classes.divider} />
|
||||
<QuickInsertsGroup editor={editor} />
|
||||
<MoreInsertsGroup editor={editor} templateMode={templateMode} />
|
||||
<div className={classes.divider} />
|
||||
<HistoryGroup editor={editor} state={state} />
|
||||
{editor && state && (
|
||||
<>
|
||||
<BlockTypeGroup editor={editor} />
|
||||
<div className={classes.divider} />
|
||||
<InlineMarksGroup editor={editor} state={state} />
|
||||
<div className={classes.divider} />
|
||||
<ColorGroup editor={editor} />
|
||||
<div className={classes.divider} />
|
||||
<ListsGroup editor={editor} state={state} />
|
||||
<div className={classes.divider} />
|
||||
<AlignmentGroup editor={editor} />
|
||||
<div className={classes.divider} />
|
||||
<MediaGroup editor={editor} templateMode={templateMode} />
|
||||
<div className={classes.divider} />
|
||||
<QuickInsertsGroup editor={editor} />
|
||||
<MoreInsertsGroup editor={editor} templateMode={templateMode} />
|
||||
<div className={classes.divider} />
|
||||
<HistoryGroup editor={editor} state={state} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.spacer} aria-hidden />
|
||||
|
||||
@@ -3,7 +3,8 @@ import { StarterKit } from "@tiptap/starter-kit";
|
||||
import { Code } from "@tiptap/extension-code";
|
||||
import { TextAlign } from "@tiptap/extension-text-align";
|
||||
import { TaskList, TaskItem } from "@tiptap/extension-list";
|
||||
import { Placeholder, CharacterCount, UndoRedo } from "@tiptap/extensions";
|
||||
import { CharacterCount, UndoRedo } from "@tiptap/extensions";
|
||||
import { Placeholder } from "@/features/editor/extensions/placeholder";
|
||||
import { Superscript } from "@tiptap/extension-superscript";
|
||||
import SubScript from "@tiptap/extension-subscript";
|
||||
import { Typography } from "@tiptap/extension-typography";
|
||||
@@ -194,16 +195,18 @@ export const mainExtensions = [
|
||||
return i18n.t("Toggle title");
|
||||
}
|
||||
if (node.type.name === "paragraph") {
|
||||
const $pos = editor.state.doc.resolve(pos);
|
||||
const parentName = $pos.parent.type.name;
|
||||
if (
|
||||
parentName === "column" ||
|
||||
parentName === "tableCell" ||
|
||||
parentName === "tableHeader" ||
|
||||
parentName === "callout" ||
|
||||
parentName === "blockquote"
|
||||
) {
|
||||
return i18n.t("Write...");
|
||||
const doc = editor.state.doc;
|
||||
if (pos >= 0 && pos <= doc.content.size) {
|
||||
const parentName = doc.resolve(pos).parent.type.name;
|
||||
if (
|
||||
parentName === "column" ||
|
||||
parentName === "tableCell" ||
|
||||
parentName === "tableHeader" ||
|
||||
parentName === "callout" ||
|
||||
parentName === "blockquote"
|
||||
) {
|
||||
return i18n.t("Write...");
|
||||
}
|
||||
}
|
||||
return i18n.t('Write anything. Enter "/" for commands');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { isNodeEmpty } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
||||
import { Placeholder as TiptapPlaceholder } from "@tiptap/extensions";
|
||||
|
||||
export const Placeholder = TiptapPlaceholder.extend({
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
const options = this.options;
|
||||
const dataAttribute = `data-${options.dataAttribute || "placeholder"}`;
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("docmostPlaceholder"),
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
if (options.showOnlyWhenEditable && !editor.isEditable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { doc, selection } = state;
|
||||
const { anchor } = selection;
|
||||
const decorations: Decoration[] = [];
|
||||
const isEmptyDoc = editor.isEmpty;
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
if (!node.type.isTextblock) {
|
||||
return options.includeChildren;
|
||||
}
|
||||
|
||||
const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
|
||||
const isEmpty = !node.isLeaf && isNodeEmpty(node);
|
||||
|
||||
if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {
|
||||
const emptyNodeClass =
|
||||
typeof options.emptyNodeClass === "function"
|
||||
? options.emptyNodeClass({ editor, node, pos, hasAnchor })
|
||||
: options.emptyNodeClass;
|
||||
const classes = [emptyNodeClass];
|
||||
if (isEmptyDoc) {
|
||||
classes.push(options.emptyEditorClass);
|
||||
}
|
||||
|
||||
decorations.push(
|
||||
Decoration.node(pos, pos + node.nodeSize, {
|
||||
class: classes.join(" "),
|
||||
[dataAttribute]:
|
||||
typeof options.placeholder === "function"
|
||||
? options.placeholder({ editor, node, pos, hasAnchor })
|
||||
: options.placeholder,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return options.includeChildren;
|
||||
});
|
||||
|
||||
return DecorationSet.create(doc, decorations);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { getCollaborationUrl } from "@/lib/config.ts";
|
||||
|
||||
const useCollaborationURL = (): string => {
|
||||
return getCollaborationUrl();
|
||||
};
|
||||
|
||||
export default useCollaborationURL;
|
||||
@@ -2,20 +2,22 @@ import "@/features/editor/styles/index.css";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { IndexeddbPersistence } from "y-indexeddb";
|
||||
import * as Y from "yjs";
|
||||
import {
|
||||
HocuspocusProvider,
|
||||
onStatusParameters,
|
||||
WebSocketStatus,
|
||||
HocuspocusProviderWebsocket,
|
||||
onSyncedParameters,
|
||||
onStatelessParameters,
|
||||
} from "@hocuspocus/provider";
|
||||
import {
|
||||
HocuspocusProviderWebsocketComponent,
|
||||
HocuspocusRoom,
|
||||
useHocuspocusEvent,
|
||||
useHocuspocusProvider,
|
||||
} from "@hocuspocus/provider-react";
|
||||
import {
|
||||
Editor,
|
||||
EditorContent,
|
||||
@@ -28,7 +30,6 @@ import {
|
||||
mainExtensions,
|
||||
} from "@/features/editor/extensions/extensions";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import useCollaborationUrl from "@/features/editor/hooks/use-collaboration-url";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
||||
import {
|
||||
currentPageEditModeAtom,
|
||||
@@ -76,6 +77,11 @@ import { EditorLinkMenu } from "@/features/editor/components/link/link-menu";
|
||||
import ColumnsMenu from "@/features/editor/components/columns/columns-menu.tsx";
|
||||
import { TransclusionLookupProvider } from "@/features/editor/components/transclusion/transclusion-lookup-context";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
acquireCollabSocket,
|
||||
getCollabSocket,
|
||||
releaseCollabSocket,
|
||||
} from "@/features/editor/collab-socket";
|
||||
|
||||
interface PageEditorProps {
|
||||
pageId: string;
|
||||
@@ -91,7 +97,80 @@ export default function PageEditor({
|
||||
canComment,
|
||||
}: PageEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const collaborationURL = useCollaborationUrl();
|
||||
const { data: collabQuery, refetch: refetchCollabToken } = useCollabToken();
|
||||
const { pageSlug } = useParams();
|
||||
const slugId = extractPageSlugId(pageSlug);
|
||||
const [socket] = useState(getCollabSocket);
|
||||
const hasCollabToken = !!collabQuery?.token;
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasCollabToken) return;
|
||||
acquireCollabSocket();
|
||||
return () => releaseCollabSocket();
|
||||
}, [hasCollabToken]);
|
||||
|
||||
const handleStateless = ({ payload }: onStatelessParameters) => {
|
||||
try {
|
||||
const message = JSON.parse(payload);
|
||||
if (message?.type !== "page.updated" || !message.updatedAt) return;
|
||||
const pageData = queryClient.getQueryData<IPage>(["pages", slugId]);
|
||||
if (pageData) {
|
||||
queryClient.setQueryData(["pages", slugId], {
|
||||
...pageData,
|
||||
updatedAt: message.updatedAt,
|
||||
...(message.lastUpdatedBy && {
|
||||
lastUpdatedBy: message.lastUpdatedBy,
|
||||
}),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore unrelated stateless messages
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthenticationFailed = () => {
|
||||
const payload = jwtDecode(collabQuery?.token);
|
||||
const now = Date.now().valueOf() / 1000;
|
||||
const isTokenExpired = now >= payload.exp;
|
||||
if (isTokenExpired) {
|
||||
refetchCollabToken();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TransclusionLookupProvider>
|
||||
{collabQuery?.token ? (
|
||||
<HocuspocusProviderWebsocketComponent websocketProvider={socket}>
|
||||
<HocuspocusRoom
|
||||
name={`page.${pageId}`}
|
||||
token={collabQuery.token}
|
||||
flushDelay={500}
|
||||
onStateless={handleStateless}
|
||||
onAuthenticationFailed={handleAuthenticationFailed}
|
||||
>
|
||||
<CollabPageEditor
|
||||
pageId={pageId}
|
||||
editable={editable}
|
||||
content={content}
|
||||
canComment={canComment}
|
||||
/>
|
||||
</HocuspocusRoom>
|
||||
</HocuspocusProviderWebsocketComponent>
|
||||
) : (
|
||||
<StaticPageEditor content={content} ariaLabel={t("Page content")} />
|
||||
)}
|
||||
</TransclusionLookupProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function CollabPageEditor({
|
||||
pageId,
|
||||
editable,
|
||||
content,
|
||||
canComment,
|
||||
}: PageEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const provider = useHocuspocusProvider();
|
||||
const isComponentMounted = useRef(false);
|
||||
const editorRef = useRef<Editor | null>(null);
|
||||
|
||||
@@ -112,7 +191,6 @@ export default function PageEditor({
|
||||
);
|
||||
const [, setYjsSynced] = useAtom(yjsSyncedAtom);
|
||||
const menuContainerRef = useRef(null);
|
||||
const { data: collabQuery, refetch: refetchCollabToken } = useCollabToken();
|
||||
const { isIdle, resetIdle } = useIdle(FIVE_MINUTES, { initialState: false });
|
||||
const documentState = useDocumentVisibility();
|
||||
const { pageSlug } = useParams();
|
||||
@@ -123,95 +201,24 @@ export default function PageEditor({
|
||||
[isComponentMounted],
|
||||
);
|
||||
const { handleScrollTo } = useEditorScroll({ canScroll });
|
||||
// Providers only created once per pageId
|
||||
const providersRef = useRef<{
|
||||
local: IndexeddbPersistence;
|
||||
remote: HocuspocusProvider;
|
||||
socket: HocuspocusProviderWebsocket;
|
||||
} | null>(null);
|
||||
const [providersReady, setProvidersReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!providersRef.current) {
|
||||
const documentName = `page.${pageId}`;
|
||||
const ydoc = new Y.Doc();
|
||||
const local = new IndexeddbPersistence(documentName, ydoc);
|
||||
const socket = new HocuspocusProviderWebsocket({
|
||||
url: collaborationURL,
|
||||
});
|
||||
const onLocalSyncedHandler = () => {
|
||||
setIsLocalSynced(true);
|
||||
};
|
||||
const onStatusHandler = (event: onStatusParameters) => {
|
||||
setYjsConnectionStatus(event.status);
|
||||
};
|
||||
const onSyncedHandler = (event: onSyncedParameters) => {
|
||||
setIsRemoteSynced(event.state);
|
||||
};
|
||||
const onStatelessHandler = ({ payload }: onStatelessParameters) => {
|
||||
try {
|
||||
const message = JSON.parse(payload);
|
||||
if (message?.type !== "page.updated" || !message.updatedAt) return;
|
||||
const pageData = queryClient.getQueryData<IPage>(["pages", slugId]);
|
||||
if (pageData) {
|
||||
queryClient.setQueryData(["pages", slugId], {
|
||||
...pageData,
|
||||
updatedAt: message.updatedAt,
|
||||
...(message.lastUpdatedBy && {
|
||||
lastUpdatedBy: message.lastUpdatedBy,
|
||||
}),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore unrelated stateless messages
|
||||
}
|
||||
};
|
||||
const onAuthenticationFailedHandler = () => {
|
||||
const payload = jwtDecode(collabQuery?.token);
|
||||
const now = Date.now().valueOf() / 1000;
|
||||
const isTokenExpired = now >= payload.exp;
|
||||
if (isTokenExpired) {
|
||||
refetchCollabToken().then((result) => {
|
||||
if (result.data?.token) {
|
||||
socket.disconnect();
|
||||
setTimeout(() => {
|
||||
remote.configuration.token = result.data.token;
|
||||
socket.connect();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
const remote = new HocuspocusProvider({
|
||||
websocketProvider: socket,
|
||||
name: documentName,
|
||||
document: ydoc,
|
||||
token: collabQuery?.token,
|
||||
onAuthenticationFailed: onAuthenticationFailedHandler,
|
||||
onStatus: onStatusHandler,
|
||||
onSynced: onSyncedHandler,
|
||||
onStateless: onStatelessHandler,
|
||||
});
|
||||
|
||||
local.on("synced", onLocalSyncedHandler);
|
||||
providersRef.current = { socket, local, remote };
|
||||
setProvidersReady(true);
|
||||
} else {
|
||||
setProvidersReady(true);
|
||||
}
|
||||
// Only destroy on final unmount
|
||||
const local = new IndexeddbPersistence(
|
||||
provider.configuration.name,
|
||||
provider.document,
|
||||
);
|
||||
local.on("synced", () => setIsLocalSynced(true));
|
||||
return () => {
|
||||
providersRef.current?.socket.destroy();
|
||||
providersRef.current?.remote.destroy();
|
||||
providersRef.current?.local.destroy();
|
||||
providersRef.current = null;
|
||||
local.destroy();
|
||||
};
|
||||
}, [pageId]);
|
||||
}, [provider]);
|
||||
|
||||
useHocuspocusEvent("synced", ({ state }) => setIsRemoteSynced(state));
|
||||
useHocuspocusEvent("status", ({ status }) => setYjsConnectionStatus(status));
|
||||
|
||||
// Only connect/disconnect on tab/idle, not destroy
|
||||
useEffect(() => {
|
||||
if (!providersReady || !providersRef.current) return;
|
||||
const socket = providersRef.current.socket;
|
||||
const socket = provider.configuration.websocketProvider;
|
||||
|
||||
if (
|
||||
isIdle &&
|
||||
@@ -228,23 +235,15 @@ export default function PageEditor({
|
||||
resetIdle();
|
||||
socket.connect();
|
||||
}
|
||||
}, [isIdle, documentState, providersReady, resetIdle]);
|
||||
|
||||
// Attach here, to make sure the connection gets properly established
|
||||
providersRef.current?.remote.attach();
|
||||
}, [isIdle, documentState, provider, resetIdle]);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
if (!providersReady || !providersRef.current || !currentUser?.user) {
|
||||
if (!currentUser?.user) {
|
||||
return mainExtensions;
|
||||
}
|
||||
|
||||
const remoteProvider = providersRef.current.remote;
|
||||
|
||||
return [
|
||||
...mainExtensions,
|
||||
...collabExtensions(remoteProvider, currentUser?.user),
|
||||
];
|
||||
}, [providersReady, currentUser?.user]);
|
||||
return [...mainExtensions, ...collabExtensions(provider, currentUser.user)];
|
||||
}, [provider, currentUser?.user]);
|
||||
|
||||
const editor = useEditor(
|
||||
{
|
||||
@@ -326,6 +325,16 @@ export default function PageEditor({
|
||||
[pageId, editable, extensions],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (editor && !editor.isDestroyed) {
|
||||
// @ts-ignore
|
||||
setEditor(editor);
|
||||
// @ts-ignore
|
||||
editor.storage.pageId = pageId;
|
||||
editorRef.current = editor;
|
||||
}
|
||||
}, [editor, pageId, setEditor]);
|
||||
|
||||
const editorIsEditable = useEditorState({
|
||||
editor,
|
||||
selector: (ctx) => {
|
||||
@@ -416,65 +425,72 @@ export default function PageEditor({
|
||||
}
|
||||
}, [yjsConnectionStatus, isSynced]);
|
||||
|
||||
if (showStatic) {
|
||||
return <StaticPageEditor content={content} ariaLabel={t("Page content")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TransclusionLookupProvider>
|
||||
{showStatic ? (
|
||||
<EditorProvider
|
||||
editable={false}
|
||||
immediatelyRender={true}
|
||||
extensions={mainExtensions}
|
||||
content={content}
|
||||
editorProps={{
|
||||
attributes: {
|
||||
"aria-label": t("Page content"),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="editor-container" style={{ position: "relative" }}>
|
||||
<div ref={menuContainerRef}>
|
||||
<EditorContent editor={editor} />
|
||||
<div className="editor-container" style={{ position: "relative" }}>
|
||||
<div ref={menuContainerRef}>
|
||||
<EditorContent editor={editor} />
|
||||
|
||||
{editor && (
|
||||
<SearchAndReplaceDialog editor={editor} editable={editable} />
|
||||
)}
|
||||
{editor && (
|
||||
<SearchAndReplaceDialog editor={editor} editable={editable} />
|
||||
)}
|
||||
|
||||
{editor && editorIsEditable && (
|
||||
<div>
|
||||
<EditorAiMenu editor={editor} />
|
||||
<EditorLinkMenu editor={editor} />
|
||||
<EditorBubbleMenu editor={editor} />
|
||||
<TableMenu editor={editor} />
|
||||
<TableHandlesLayer editor={editor} />
|
||||
<ImageMenu editor={editor} />
|
||||
<VideoMenu editor={editor} />
|
||||
<PdfMenu editor={editor} />
|
||||
<CalloutMenu editor={editor} />
|
||||
<SubpagesMenu editor={editor} />
|
||||
<ExcalidrawMenu editor={editor} />
|
||||
<DrawioMenu editor={editor} />
|
||||
<ColumnsMenu editor={editor} />
|
||||
</div>
|
||||
)}
|
||||
{editor &&
|
||||
!editorIsEditable &&
|
||||
(editable || canComment) &&
|
||||
providersRef.current && <ReadonlyBubbleMenu editor={editor} />}
|
||||
{showCommentPopup && (
|
||||
<CommentDialog editor={editor} pageId={pageId} />
|
||||
)}
|
||||
{showReadOnlyCommentPopup && (
|
||||
<CommentDialog editor={editor} pageId={pageId} readOnly />
|
||||
)}
|
||||
{editor && editorIsEditable && (
|
||||
<div>
|
||||
<EditorAiMenu editor={editor} />
|
||||
<EditorLinkMenu editor={editor} />
|
||||
<EditorBubbleMenu editor={editor} />
|
||||
<TableMenu editor={editor} />
|
||||
<TableHandlesLayer editor={editor} />
|
||||
<ImageMenu editor={editor} />
|
||||
<VideoMenu editor={editor} />
|
||||
<PdfMenu editor={editor} />
|
||||
<CalloutMenu editor={editor} />
|
||||
<SubpagesMenu editor={editor} />
|
||||
<ExcalidrawMenu editor={editor} />
|
||||
<DrawioMenu editor={editor} />
|
||||
<ColumnsMenu editor={editor} />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (editor && !editor.isDestroyed) editor.commands.focus("end");
|
||||
}}
|
||||
style={{ paddingBottom: "20vh" }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</TransclusionLookupProvider>
|
||||
)}
|
||||
{editor && !editorIsEditable && (editable || canComment) && (
|
||||
<ReadonlyBubbleMenu editor={editor} />
|
||||
)}
|
||||
{showCommentPopup && <CommentDialog editor={editor} pageId={pageId} />}
|
||||
{showReadOnlyCommentPopup && (
|
||||
<CommentDialog editor={editor} pageId={pageId} readOnly />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
if (editor && !editor.isDestroyed) editor.commands.focus("end");
|
||||
}}
|
||||
style={{ paddingBottom: "20vh" }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StaticPageEditor({
|
||||
content,
|
||||
ariaLabel,
|
||||
}: {
|
||||
content: any;
|
||||
ariaLabel: string;
|
||||
}) {
|
||||
return (
|
||||
<EditorProvider
|
||||
editable={false}
|
||||
immediatelyRender={true}
|
||||
extensions={mainExtensions}
|
||||
content={content}
|
||||
editorProps={{
|
||||
attributes: {
|
||||
"aria-label": ariaLabel,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user