mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
feat(core): auto select block when jump to block (#4858)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user