Files
AFFiNE/apps/core/src/components/page-detail-editor.tsx
T

219 lines
6.6 KiB
TypeScript

import './page-detail-editor.css';
import { PageNotFoundError } from '@affine/env/constant';
import type { LayoutNode } from '@affine/sdk//entry';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import type { Page, Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import {
addCleanup,
pluginEditorAtom,
pluginWindowAtom,
} from '@toeverything/infra/__internal__/plugin';
import { contentLayoutAtom, rootStore } from '@toeverything/infra/atom';
import clsx from 'clsx';
import { useAtomValue, useSetAtom } from 'jotai';
import type { CSSProperties, FC, ReactElement } from 'react';
import { memo, Suspense, useCallback, useMemo } from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { pageSettingFamily } from '../atoms';
import { fontStyleOptions, useAppSetting } from '../atoms/settings';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import { TrashButtonGroup } from './blocksuite/workspace-header/header-right-items/trash-button-group';
import * as styles from './page-detail-editor.css';
import { pluginContainer } from './page-detail-editor.css';
export type PageDetailEditorProps = {
isPublic?: boolean;
workspace: Workspace;
pageId: string;
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
onLoad?: (page: Page, editor: EditorContainer) => () => void;
};
const EditorWrapper = memo(function EditorWrapper({
workspace,
pageId,
onInit,
onLoad,
isPublic,
}: PageDetailEditorProps) {
const page = useBlockSuiteWorkspacePage(workspace, pageId);
if (!page) {
throw new PageNotFoundError(workspace, pageId);
}
const meta = useBlockSuitePageMeta(workspace).find(
meta => meta.id === pageId
);
const pageSettingAtom = pageSettingFamily(pageId);
const pageSetting = useAtomValue(pageSettingAtom);
const currentMode = pageSetting?.mode ?? 'page';
const setBlockHub = useSetAtom(rootBlockHubAtom);
const [appSettings] = useAppSetting();
assertExists(meta);
const value = useMemo(() => {
const fontStyle = fontStyleOptions.find(
option => option.key === appSettings.fontStyle
);
assertExists(fontStyle);
return fontStyle.value;
}, [appSettings.fontStyle]);
return (
<>
<Editor
className={clsx(styles.editor, {
'full-screen': appSettings.fullWidthLayout,
})}
style={
{
'--affine-font-family': value,
} as CSSProperties
}
key={`${workspace.id}-${pageId}`}
mode={isPublic ? 'page' : currentMode}
page={page}
onInit={useCallback(
(page: Page, editor: Readonly<EditorContainer>) => {
onInit(page, editor);
},
[onInit]
)}
setBlockHub={setBlockHub}
onLoad={useCallback(
(page: Page, editor: EditorContainer) => {
page.workspace.setPageMeta(page.id, {
updatedDate: Date.now(),
});
localStorage.setItem('last_page_id', page.id);
let dispose = () => {};
if (onLoad) {
dispose = onLoad(page, editor);
}
const editorItems = rootStore.get(pluginEditorAtom);
let disposes: (() => void)[] = [];
const renderTimeout = setTimeout(() => {
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
const div = document.createElement('div');
div.setAttribute('plugin-id', id);
const cleanup = editorItem(div, editor);
assertExists(parent);
document.body.appendChild(div);
return () => {
cleanup();
document.body.removeChild(div);
};
});
});
return () => {
dispose();
clearTimeout(renderTimeout);
setTimeout(() => {
disposes.forEach(dispose => dispose());
});
};
},
[onLoad]
)}
/>
{meta.trash && <TrashButtonGroup />}
</>
);
});
const PluginContentAdapter = memo<{
windowItem: (div: HTMLDivElement) => () => void;
pluginName: string;
}>(function PluginContentAdapter({ windowItem, pluginName }) {
return (
<div
className={pluginContainer}
ref={useCallback(
(ref: HTMLDivElement | null) => {
if (ref) {
const div = document.createElement('div');
const cleanup = windowItem(div);
ref.appendChild(div);
addCleanup(pluginName, () => {
cleanup();
ref.removeChild(div);
});
}
},
[pluginName, windowItem]
)}
/>
);
});
type LayoutPanelProps = {
node: LayoutNode;
editorProps: PageDetailEditorProps;
};
const LayoutPanel = memo(function LayoutPanel(
props: LayoutPanelProps
): ReactElement {
const node = props.node;
const windowItems = useAtomValue(pluginWindowAtom);
if (typeof node === 'string') {
if (node === 'editor') {
return <EditorWrapper {...props.editorProps} />;
} else {
const windowItem = windowItems[node];
return <PluginContentAdapter pluginName={node} windowItem={windowItem} />;
}
} else {
return (
<PanelGroup
style={{
height: 'calc(100% - 52px)',
}}
direction={node.direction}
>
<Panel defaultSize={node.splitPercentage}>
<Suspense>
<LayoutPanel node={node.first} editorProps={props.editorProps} />
</Suspense>
</Panel>
<PanelResizeHandle />
<Panel
defaultSize={100 - node.splitPercentage}
style={{
overflow: 'scroll',
}}
>
<Suspense>
<LayoutPanel node={node.second} editorProps={props.editorProps} />
</Suspense>
</Panel>
</PanelGroup>
);
}
});
export const PageDetailEditor: FC<PageDetailEditorProps> = props => {
const { workspace, pageId } = props;
const page = useBlockSuiteWorkspacePage(workspace, pageId);
if (!page) {
throw new PageNotFoundError(workspace, pageId);
}
const layout = useAtomValue(contentLayoutAtom);
return (
<>
<Suspense>
<LayoutPanel node={layout} editorProps={props} />
</Suspense>
</>
);
};