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}
/>
);
};

View File

@@ -3,7 +3,6 @@ import type {
WorkspaceFlavour,
WorkspaceUISchema,
} from '@affine/env/workspace';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { lazy, useCallback } from 'react';
import type { OnLoadEditor } from '../../components/page-detail-editor';
@@ -50,7 +49,6 @@ export const UI = {
return (
<PageDetailEditor
pageId={currentPageId}
onInit={useCallback(async page => initEmptyPage(page), [])}
onLoad={onLoad}
workspace={workspace.blockSuiteWorkspace}
/>

View File

@@ -25,7 +25,6 @@ import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { buildShowcaseWorkspace } from '@toeverything/infra/blocksuite';
import { useAtomValue } from 'jotai';
import { nanoid } from 'nanoid';
import { useCallback } from 'react';
import { setPageModeAtom } from '../../atoms';
import {
@@ -94,7 +93,6 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
return (
<PageDetailEditor
pageId={currentPageId}
onInit={useCallback(async page => initEmptyPage(page), [])}
onLoad={onLoadEditor}
workspace={workspace}
/>

View File

@@ -1,8 +1,6 @@
import { PageNotFoundError } from '@affine/env/constant';
import type { WorkspaceFlavour } from '@affine/env/workspace';
import { type WorkspaceUISchema } from '@affine/env/workspace';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { useCallback } from 'react';
import { useWorkspace } from '../../hooks/use-workspace';
import { PageDetailEditor, Provider } from '../shared';
@@ -18,7 +16,6 @@ export const UI = {
return (
<PageDetailEditor
pageId={currentPageId}
onInit={useCallback(async page => initEmptyPage(page), [])}
onLoad={onLoadEditor}
workspace={workspace.blockSuiteWorkspace}
/>

View File

@@ -1,8 +1,9 @@
import './page-detail-editor.css';
import { PageNotFoundError } from '@affine/env/constant';
import type { LayoutNode } from '@affine/sdk//entry';
import type { LayoutNode } from '@affine/sdk/entry';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import type { BlockHub } from '@blocksuite/blocks';
import type { EditorContainer } from '@blocksuite/editor';
import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
import type { Page, Workspace } from '@blocksuite/store';
@@ -40,8 +41,9 @@ import * as styles from './page-detail-editor.css';
import { editorContainer, pluginContainer } from './page-detail-editor.css';
import { TrashButtonGroup } from './pure/trash-button-group';
function useRouterHash() {
return useLocation().hash.substring(1);
declare global {
// eslint-disable-next-line no-var
var currentEditor: EditorContainer | undefined;
}
export type OnLoadEditor = (page: Page, editor: EditorContainer) => () => void;
@@ -50,17 +52,43 @@ export interface PageDetailEditorProps {
isPublic?: boolean;
workspace: Workspace;
pageId: string;
onInit: (
page: Page,
editor: Readonly<EditorContainer>
) => Promise<void> | void;
onLoad?: OnLoadEditor;
}
function useRouterHash() {
return useLocation().hash.substring(1);
}
function useCreateAndSetRootBlockHub(
editor?: EditorContainer,
showBlockHub?: boolean
) {
const setBlockHub = useSetAtom(rootBlockHubAtom);
useEffect(() => {
let canceled = false;
let blockHub: BlockHub | undefined;
if (editor && showBlockHub) {
editor
.createBlockHub()
.then(bh => {
if (canceled) {
return;
}
blockHub = bh;
setBlockHub(blockHub);
})
.catch(console.error);
}
return () => {
canceled = true;
blockHub?.remove();
};
}, [editor, showBlockHub, setBlockHub]);
}
const EditorWrapper = memo(function EditorWrapper({
workspace,
pageId,
onInit,
onLoad,
isPublic,
}: PageDetailEditorProps) {
@@ -79,7 +107,6 @@ const EditorWrapper = memo(function EditorWrapper({
const pageSetting = useAtomValue(pageSettingAtom);
const currentMode = pageSetting?.mode ?? 'page';
const setBlockHub = useSetAtom(rootBlockHubAtom);
const { appSettings } = useAppSettingHelper();
assertExists(meta);
@@ -91,29 +118,6 @@ const EditorWrapper = memo(function EditorWrapper({
return fontStyle.value;
}, [appSettings.fontStyle]);
const [loading, setLoading] = useState(true);
const blockId = useRouterHash();
const blockElement = useMemo(() => {
if (!blockId || loading) {
return null;
}
return document.querySelector(`[data-block-id="${blockId}"]`);
}, [blockId, loading]);
useEffect(() => {
if (blockElement) {
setTimeout(
() =>
blockElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
}),
0
);
}
}, [blockElement]);
const setEditorMode = useCallback(
(mode: 'page' | 'edgeless') => {
if (mode === 'edgeless') {
@@ -125,6 +129,56 @@ const EditorWrapper = memo(function EditorWrapper({
[switchToEdgelessMode, switchToPageMode, pageId]
);
const [editor, setEditor] = useState<EditorContainer>();
const blockId = useRouterHash();
useCreateAndSetRootBlockHub(editor, !meta.trash);
const onLoadEditor = useCallback(
(editor: EditorContainer) => {
// debug current detail editor
globalThis.currentEditor = editor;
setEditor(editor);
const disposableGroup = new DisposableGroup();
disposableGroup.add(
page.slots.blockUpdated.once(() => {
page.workspace.setPageMeta(page.id, {
updatedDate: Date.now(),
});
})
);
localStorage.setItem('last_page_id', page.id);
if (onLoad) {
disposableGroup.add(onLoad(page, editor));
}
const rootStore = getCurrentStore();
const editorItems = rootStore.get(pluginEditorAtom);
let disposes: (() => void)[] = [];
const renderTimeout = window.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 () => {
disposableGroup.dispose();
clearTimeout(renderTimeout);
window.setTimeout(() => {
disposes.forEach(dispose => dispose());
});
};
},
[onLoad, page]
);
return (
<>
<Editor
@@ -140,55 +194,8 @@ const EditorWrapper = memo(function EditorWrapper({
mode={isPublic ? 'page' : currentMode}
page={page}
onModeChange={setEditorMode}
onInit={useCallback(
(page: Page, editor: Readonly<EditorContainer>) => {
onInit(page, editor);
},
[onInit]
)}
setBlockHub={setBlockHub}
onLoad={useCallback(
(page: Page, editor: EditorContainer) => {
const disposableGroup = new DisposableGroup();
disposableGroup.add(
page.slots.blockUpdated.once(() => {
page.workspace.setPageMeta(page.id, {
updatedDate: Date.now(),
});
})
);
localStorage.setItem('last_page_id', page.id);
if (onLoad) {
disposableGroup.add(onLoad(page, editor));
}
const rootStore = getCurrentStore();
const editorItems = rootStore.get(pluginEditorAtom);
let disposes: (() => void)[] = [];
const renderTimeout = window.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 () => {
disposableGroup.dispose();
clearTimeout(renderTimeout);
window.setTimeout(() => {
disposes.forEach(dispose => dispose());
});
setLoading(false);
};
},
[onLoad]
)}
defaultSelectedBlockId={blockId}
onLoadEditor={onLoadEditor}
/>
{meta.trash && <TrashButtonGroup />}
<Bookmark page={page} />

View File

@@ -2,7 +2,7 @@ import {
AppSidebarFallback,
appSidebarResizingAtom,
} from '@affine/component/app-sidebar';
import { BlockHubWrapper } from '@affine/component/block-hub';
import { RootBlockHub } from '@affine/component/block-hub';
import {
type DraggableTitleCellData,
PageListDragOverlay,
@@ -13,10 +13,7 @@ import {
WorkspaceFallback,
} from '@affine/component/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
rootBlockHubAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store';
import type { DragEndEvent } from '@dnd-kit/core';
@@ -294,7 +291,7 @@ export const WorkspaceLayoutInner = ({
>
{incompatible ? <WorkspaceUpgrade /> : children}
<ToolContainer inTrashPage={inTrashPage}>
<BlockHubWrapper blockHubAtom={rootBlockHubAtom} />
<RootBlockHub />
<HelpIsland showList={pageId ? undefined : showList} />
</ToolContainer>
</MainContainer>

View File

@@ -70,7 +70,6 @@ export const Component = (): ReactElement => {
isPublic
workspace={page.workspace}
pageId={page.id}
onInit={noop}
onLoad={useCallback(() => noop, [])}
/>
</MainContainer>