feat(core): auto select block when jump to block (#4858)

Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
JimmFly
2023-11-10 11:02:56 +08:00
committed by GitHub
parent f1e32aab66
commit 1fe5a0fffa
10 changed files with 199 additions and 185 deletions

View File

@@ -1,16 +1,10 @@
import type { BlockHub } from '@blocksuite/blocks';
import type { Atom } from 'jotai';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import { useAtomValue } from 'jotai';
import type { HTMLAttributes, ReactElement } from 'react';
import { useEffect, useRef } from 'react';
export interface BlockHubProps extends HTMLAttributes<HTMLDivElement> {
blockHubAtom: Atom<Readonly<BlockHub> | null>;
}
export const BlockHubWrapper = (props: BlockHubProps): ReactElement => {
const blockHub = useAtomValue(props.blockHubAtom);
export const RootBlockHub = () => {
const ref = useRef<HTMLDivElement>(null);
const blockHub = useAtomValue(rootBlockHubAtom);
useEffect(() => {
if (ref.current) {
const div = ref.current;

View File

@@ -1,11 +1,19 @@
import type { BlockHub } from '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store';
import { Skeleton } from '@mui/material';
import clsx from 'clsx';
import { use } from 'foxact/use';
import type { CSSProperties, ReactElement } from 'react';
import { memo, Suspense, useCallback, useEffect, useRef } from 'react';
import {
memo,
Suspense,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';
@@ -15,13 +23,17 @@ import {
} from './index.css';
import { getPresets } from './preset';
interface BlockElement extends Element {
path: string[];
}
export type EditorProps = {
page: Page;
mode: 'page' | 'edgeless';
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
defaultSelectedBlockId?: string;
onModeChange?: (mode: 'page' | 'edgeless') => void;
setBlockHub?: (blockHub: BlockHub | null) => void;
onLoad?: (page: Page, editor: EditorContainer) => () => void;
// on Editor instance instantiated
onLoadEditor?: (editor: EditorContainer) => () => void;
style?: CSSProperties;
className?: string;
};
@@ -30,28 +42,62 @@ export type ErrorBoundaryProps = {
onReset?: () => void;
};
declare global {
// eslint-disable-next-line no-var
var currentPage: Page | undefined;
// eslint-disable-next-line no-var
var currentEditor: EditorContainer | undefined;
}
// a workaround for returning the webcomponent for the given block id
// by iterating over the children of the rendered dom tree
const useBlockElementById = (
container: HTMLElement | null,
blockId: string | undefined,
timeout = 1000
) => {
const [blockElement, setBlockElement] = useState<BlockElement | null>(null);
useEffect(() => {
if (!blockId) {
return;
}
let canceled = false;
const start = Date.now();
function run() {
if (canceled || !container) {
return;
}
const element = container.querySelector(
`[data-block-id="${blockId}"]`
) as BlockElement | null;
if (element) {
setBlockElement(element);
} else if (Date.now() - start < timeout) {
setTimeout(run, 100);
}
}
run();
return () => {
canceled = true;
};
}, [container, blockId, timeout]);
return blockElement;
};
const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
const { onLoad, onModeChange, page, mode, style } = props;
const BlockSuiteEditorImpl = ({
mode,
page,
className,
defaultSelectedBlockId,
onLoadEditor,
onModeChange,
style,
}: EditorProps): ReactElement => {
if (!page.loaded) {
use(page.waitForLoaded());
}
assertExists(page, 'page should not be null');
const editorRef = useRef<EditorContainer | null>(null);
const blockHubRef = useRef<BlockHub | null>(null);
if (editorRef.current === null) {
editorRef.current = new EditorContainer();
editorRef.current.autofocus = true;
globalThis.currentEditor = editorRef.current;
}
const editor = editorRef.current;
assertExists(editorRef, 'editorRef.current should not be null');
if (editor.mode !== mode) {
editor.mode = mode;
}
@@ -64,36 +110,29 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
editor.pagePreset = presets.pageModePreset;
editor.edgelessPreset = presets.edgelessModePreset;
useEffect(() => {
const disposes = [] as ((() => void) | undefined)[];
useLayoutEffect(() => {
if (editor) {
const dispose = editor.slots.pageModeSwitched.on(mode => {
const disposes: (() => void)[] = [];
const disposeModeSwitch = editor.slots.pageModeSwitched.on(mode => {
onModeChange?.(mode);
});
disposes.push(() => dispose?.dispose());
if (editor.page && onLoad) {
disposes.push(onLoad?.(page, editor));
disposes.push(() => disposeModeSwitch?.dispose());
if (onLoadEditor) {
disposes.push(onLoadEditor(editor));
}
return () => {
disposes.forEach(dispose => dispose());
};
}
return;
}, [editor, onModeChange, onLoadEditor]);
return () => {
disposes
.filter((dispose): dispose is () => void => !!dispose)
.forEach(dispose => dispose());
};
}, [editor, editor.page, page, onLoad, onModeChange]);
const ref = useRef<HTMLDivElement>(null);
const setBlockHub = props.setBlockHub;
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const editor = editorRef.current;
assertExists(editor);
const container = ref.current;
const container = containerRef.current;
if (!container) {
return;
}
@@ -103,42 +142,38 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
};
}, [editor]);
const blockElement = useBlockElementById(
containerRef.current,
defaultSelectedBlockId
);
useEffect(() => {
if (page.meta.trash) {
return;
}
editor
.createBlockHub()
.then(blockHub => {
if (blockHubRef.current) {
blockHubRef.current.remove();
if (blockElement) {
requestIdleCallback(() => {
blockElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
const selectManager = editor.root.value?.selection;
if (!blockElement.path.length || !selectManager) {
return;
}
blockHubRef.current = blockHub;
if (setBlockHub) {
setBlockHub(blockHub);
}
})
.catch(err => {
console.error(err);
const newSelection = selectManager.getInstance('block', {
path: blockElement.path,
});
selectManager.set([newSelection]);
});
return () => {
if (setBlockHub) {
setBlockHub(null);
}
blockHubRef.current?.remove();
};
}, [editor, page.awarenessStore, page.meta.trash, setBlockHub]);
}
}, [editor, blockElement]);
// issue: https://github.com/toeverything/AFFiNE/issues/2004
const className = `editor-wrapper ${editor.mode}-mode ${
props.className || ''
}`;
return (
<div
data-testid={`editor-${page.id}`}
className={className}
className={clsx(`editor-wrapper ${editor.mode}-mode`, className)}
style={style}
ref={ref}
ref={containerRef}
/>
);
};