refactor(component): adjust active editor atom (#5457)

before:

set global `blocksuiteEditorAtom` state in `<BlocksuiteEditorImpl />`

after:

Rename `blocksuiteEditorAtom` to `activeBlocksuiteEditorAtom`

And move the logic of setting this atom to `<PageDetailEditor />`.

benefit:

* make BlocksuiteEditor pure
* keep @toeverything/component clear
* Clarify the purpose of `activeBlocksuiteEditorAtom`
This commit is contained in:
EYHN
2024-01-02 07:30:09 +00:00
parent 1eefd712dd
commit 6844b282ac
6 changed files with 140 additions and 108 deletions

View File

@@ -1,11 +1,11 @@
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { AffineEditorContainer } from '@blocksuite/presets'; import { AffineEditorContainer } from '@blocksuite/presets';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { useBlocksuiteEditor } from '@toeverything/hooks/use-block-suite-editor';
import clsx from 'clsx'; import clsx from 'clsx';
import { use } from 'foxact/use'; import { use } from 'foxact/use';
import type { CSSProperties, ReactElement } from 'react'; import type { CSSProperties, ReactElement } from 'react';
import { import {
forwardRef,
memo, memo,
Suspense, Suspense,
useEffect, useEffect,
@@ -137,104 +137,115 @@ function usePageRoot(page: Page) {
return page.root; return page.root;
} }
const BlockSuiteEditorImpl = ({ const BlockSuiteEditorImpl = forwardRef<AffineEditorContainer, EditorProps>(
mode, (
page, {
className, mode,
defaultSelectedBlockId, page,
onLoadEditor, className,
onModeChange, defaultSelectedBlockId,
style, onLoadEditor,
}: EditorProps): ReactElement => { onModeChange,
usePageRoot(page); style,
},
ref
): ReactElement => {
usePageRoot(page);
const [, setEditorContainer] = useBlocksuiteEditor(); assertExists(page, 'page should not be null');
assertExists(page, 'page should not be null'); const editorRef = useRef<AffineEditorContainer | null>(null);
const editorRef = useRef<AffineEditorContainer | null>(null); if (editorRef.current === null) {
if (editorRef.current === null) { editorRef.current = new AffineEditorContainer();
editorRef.current = new AffineEditorContainer(); editorRef.current.autofocus = true;
editorRef.current.autofocus = true; }
} const editor = editorRef.current;
const editor = editorRef.current; assertExists(editorRef, 'editorRef.current should not be null');
assertExists(editorRef, 'editorRef.current should not be null');
if (editor.mode !== mode) { if (editor.mode !== mode) {
editor.mode = mode; editor.mode = mode;
} }
if (editor.page !== page) { if (editor.page !== page) {
editor.page = page; editor.page = page;
editor.docSpecs = editorSpecs.docModeSpecs; editor.docSpecs = editorSpecs.docModeSpecs;
editor.edgelessSpecs = editorSpecs.edgelessModeSpecs; editor.edgelessSpecs = editorSpecs.edgelessModeSpecs;
} }
useLayoutEffect(() => { if (ref) {
if (editor) { if (typeof ref === 'function') {
const disposes: (() => void)[] = []; ref(editor);
const disposeModeSwitch = editor.slots.pageModeSwitched.on(mode => { } else {
onModeChange?.(mode); ref.current = editor;
});
disposes.push(() => disposeModeSwitch?.dispose());
if (onLoadEditor) {
disposes.push(onLoadEditor(editor));
} }
return () => {
disposes.forEach(dispose => dispose());
};
} }
return;
}, [editor, onModeChange, onLoadEditor]);
const containerRef = useRef<HTMLDivElement>(null); useLayoutEffect(() => {
if (editor) {
useEffect(() => { const disposes: (() => void)[] = [];
const container = containerRef.current; const disposeModeSwitch = editor.slots.pageModeSwitched.on(mode => {
if (!container) { onModeChange?.(mode);
return;
}
container.append(editor);
setEditorContainer(editor);
return () => {
editor.remove();
setEditorContainer(null);
};
}, [editor, setEditorContainer]);
const blockElement = useBlockElementById(
containerRef.current,
defaultSelectedBlockId
);
useEffect(() => {
if (blockElement) {
requestIdleCallback(() => {
blockElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
}); });
const selectManager = editor.root?.selection; disposes.push(() => disposeModeSwitch?.dispose());
if (!blockElement.path.length || !selectManager) { if (onLoadEditor) {
return; disposes.push(onLoadEditor(editor));
} }
const newSelection = selectManager.getInstance('block', { return () => {
path: blockElement.path, disposes.forEach(dispose => dispose());
}); };
selectManager.set([newSelection]); }
}); return;
} }, [editor, onModeChange, onLoadEditor]);
}, [editor, blockElement]);
// issue: https://github.com/toeverything/AFFiNE/issues/2004 const containerRef = useRef<HTMLDivElement>(null);
return (
<div useEffect(() => {
data-testid={`editor-${page.id}`} const container = containerRef.current;
className={clsx(`editor-wrapper ${editor.mode}-mode`, className)} if (!container) {
style={style} return;
ref={containerRef} }
/> container.append(editor);
); return () => {
}; editor.remove();
};
}, [editor]);
const blockElement = useBlockElementById(
containerRef.current,
defaultSelectedBlockId
);
useEffect(() => {
if (blockElement) {
requestIdleCallback(() => {
blockElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
const selectManager = editor.root?.selection;
if (!blockElement.path.length || !selectManager) {
return;
}
const newSelection = selectManager.getInstance('block', {
path: blockElement.path,
});
selectManager.set([newSelection]);
});
}
}, [editor, blockElement]);
// issue: https://github.com/toeverything/AFFiNE/issues/2004
return (
<div
data-testid={`editor-${page.id}`}
className={clsx(`editor-wrapper ${editor.mode}-mode`, className)}
style={style}
ref={containerRef}
/>
);
}
);
BlockSuiteEditorImpl.displayName = 'BlockSuiteEditorImpl';
export const EditorLoading = memo(function EditorLoading() { export const EditorLoading = memo(function EditorLoading() {
return ( return (
@@ -249,14 +260,16 @@ export const EditorLoading = memo(function EditorLoading() {
); );
}); });
export const BlockSuiteEditor = memo(function BlockSuiteEditor( export const BlockSuiteEditor = memo(
props: EditorProps forwardRef<AffineEditorContainer, EditorProps>(
): ReactElement { function BlockSuiteEditor(props, ref): ReactElement {
return ( return (
<Suspense fallback={<EditorLoading />}> <Suspense fallback={<EditorLoading />}>
<BlockSuiteEditorImpl key={props.page.id} {...props} /> <BlockSuiteEditorImpl key={props.page.id} ref={ref} {...props} />
</Suspense> </Suspense>
); );
}); }
)
);
BlockSuiteEditor.displayName = 'BlockSuiteEditor'; BlockSuiteEditor.displayName = 'BlockSuiteEditor';

View File

@@ -3,12 +3,21 @@ import './page-detail-editor.css';
import { assertExists, DisposableGroup } from '@blocksuite/global/utils'; import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
import type { AffineEditorContainer } from '@blocksuite/presets'; import type { AffineEditorContainer } from '@blocksuite/presets';
import type { Page, Workspace } from '@blocksuite/store'; import type { Page, Workspace } from '@blocksuite/store';
import { useActiveBlocksuiteEditor } from '@toeverything/hooks/use-block-suite-editor';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page'; import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { fontStyleOptions } from '@toeverything/infra/atom'; import { fontStyleOptions } from '@toeverything/infra/atom';
import clsx from 'clsx'; import clsx from 'clsx';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { memo, Suspense, useCallback, useMemo, useState } from 'react'; import {
memo,
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { type PageMode, pageSettingFamily } from '../atoms'; import { type PageMode, pageSettingFamily } from '../atoms';
@@ -115,6 +124,13 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
[onLoad, page] [onLoad, page]
); );
const [, setActiveBlocksuiteEditor] = useActiveBlocksuiteEditor();
const editor = useRef<AffineEditorContainer>(null);
useEffect(() => {
setActiveBlocksuiteEditor(editor.current);
}, [setActiveBlocksuiteEditor]);
return ( return (
<Editor <Editor
className={clsx(styles.editor, { className={clsx(styles.editor, {
@@ -131,6 +147,7 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
onModeChange={setEditorMode} onModeChange={setEditorMode}
defaultSelectedBlockId={blockId} defaultSelectedBlockId={blockId}
onLoadEditor={onLoadEditor} onLoadEditor={onLoadEditor}
ref={editor}
/> />
); );
}); });

View File

@@ -1,7 +1,7 @@
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { AiIcon } from '@blocksuite/icons'; import { AiIcon } from '@blocksuite/icons';
import { CopilotPanel } from '@blocksuite/presets'; import { CopilotPanel } from '@blocksuite/presets';
import { useBlocksuiteEditor } from '@toeverything/hooks/use-block-suite-editor'; import { useActiveBlocksuiteEditor } from '@toeverything/hooks/use-block-suite-editor';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import type { EditorExtension } from '../types'; import type { EditorExtension } from '../types';
@@ -10,7 +10,7 @@ import * as styles from './outline.css';
// A wrapper for CopilotPanel // A wrapper for CopilotPanel
const EditorCopilotPanel = () => { const EditorCopilotPanel = () => {
const copilotPanelRef = useRef<CopilotPanel | null>(null); const copilotPanelRef = useRef<CopilotPanel | null>(null);
const [editor] = useBlocksuiteEditor(); const [editor] = useActiveBlocksuiteEditor();
const onRefChange = useCallback((container: HTMLDivElement | null) => { const onRefChange = useCallback((container: HTMLDivElement | null) => {
if (container) { if (container) {

View File

@@ -1,7 +1,7 @@
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { FrameIcon } from '@blocksuite/icons'; import { FrameIcon } from '@blocksuite/icons';
import { FramePanel } from '@blocksuite/presets'; import { FramePanel } from '@blocksuite/presets';
import { useBlocksuiteEditor } from '@toeverything/hooks/use-block-suite-editor'; import { useActiveBlocksuiteEditor } from '@toeverything/hooks/use-block-suite-editor';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import type { EditorExtension } from '../types'; import type { EditorExtension } from '../types';
@@ -11,7 +11,7 @@ import * as styles from './frame.css';
const EditorFramePanel = () => { const EditorFramePanel = () => {
const framePanelRef = useRef<FramePanel | null>(null); const framePanelRef = useRef<FramePanel | null>(null);
const [editor] = useBlocksuiteEditor(); const [editor] = useActiveBlocksuiteEditor();
const onRefChange = useCallback((container: HTMLDivElement | null) => { const onRefChange = useCallback((container: HTMLDivElement | null) => {
if (container) { if (container) {

View File

@@ -1,7 +1,7 @@
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { TocIcon } from '@blocksuite/icons'; import { TocIcon } from '@blocksuite/icons';
import { TOCPanel } from '@blocksuite/presets'; import { TOCPanel } from '@blocksuite/presets';
import { useBlocksuiteEditor } from '@toeverything/hooks/use-block-suite-editor'; import { useActiveBlocksuiteEditor } from '@toeverything/hooks/use-block-suite-editor';
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import type { EditorExtension } from '../types'; import type { EditorExtension } from '../types';
@@ -10,7 +10,7 @@ import * as styles from './outline.css';
// A wrapper for TOCNotesPanel // A wrapper for TOCNotesPanel
const EditorOutline = () => { const EditorOutline = () => {
const tocPanelRef = useRef<TOCPanel | null>(null); const tocPanelRef = useRef<TOCPanel | null>(null);
const [editor] = useBlocksuiteEditor(); const [editor] = useActiveBlocksuiteEditor();
const onRefChange = useCallback((container: HTMLDivElement | null) => { const onRefChange = useCallback((container: HTMLDivElement | null) => {
if (container) { if (container) {

View File

@@ -1,13 +1,15 @@
import type { AffineEditorContainer } from '@blocksuite/presets'; import type { AffineEditorContainer } from '@blocksuite/presets';
import { atom, type SetStateAction, useAtom } from 'jotai'; import { atom, type SetStateAction, useAtom } from 'jotai';
const editorContainerAtom = atom<AffineEditorContainer | null>(null); const activeEditorContainerAtom = atom<AffineEditorContainer | null>(null);
export function useBlocksuiteEditor(): [ export function useActiveBlocksuiteEditor(): [
AffineEditorContainer | null, AffineEditorContainer | null,
React.Dispatch<SetStateAction<AffineEditorContainer | null>>, React.Dispatch<SetStateAction<AffineEditorContainer | null>>,
] { ] {
const [editorContainer, setEditorContainer] = useAtom(editorContainerAtom); const [editorContainer, setEditorContainer] = useAtom(
activeEditorContainerAtom
);
return [editorContainer, setEditorContainer]; return [editorContainer, setEditorContainer];
} }