diff --git a/packages/frontend/component/src/components/block-hub/index.tsx b/packages/frontend/component/src/components/block-hub/index.tsx index 537ddaf771..b6157b0521 100644 --- a/packages/frontend/component/src/components/block-hub/index.tsx +++ b/packages/frontend/component/src/components/block-hub/index.tsx @@ -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 { - blockHubAtom: Atom | null>; -} - -export const BlockHubWrapper = (props: BlockHubProps): ReactElement => { - const blockHub = useAtomValue(props.blockHubAtom); +export const RootBlockHub = () => { const ref = useRef(null); + const blockHub = useAtomValue(rootBlockHubAtom); useEffect(() => { if (ref.current) { const div = ref.current; diff --git a/packages/frontend/component/src/components/block-suite-editor/index.tsx b/packages/frontend/component/src/components/block-suite-editor/index.tsx index fba1ee9a71..3d2062458b 100644 --- a/packages/frontend/component/src/components/block-suite-editor/index.tsx +++ b/packages/frontend/component/src/components/block-suite-editor/index.tsx @@ -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) => 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(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(null); - const blockHubRef = useRef(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(null); - - const setBlockHub = props.setBlockHub; + const containerRef = useRef(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 (
); }; diff --git a/packages/frontend/core/src/adapters/cloud/ui.tsx b/packages/frontend/core/src/adapters/cloud/ui.tsx index f129c020eb..08e113719a 100644 --- a/packages/frontend/core/src/adapters/cloud/ui.tsx +++ b/packages/frontend/core/src/adapters/cloud/ui.tsx @@ -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 ( initEmptyPage(page), [])} onLoad={onLoad} workspace={workspace.blockSuiteWorkspace} /> diff --git a/packages/frontend/core/src/adapters/local/index.tsx b/packages/frontend/core/src/adapters/local/index.tsx index ca87fb7ff8..a6a84d5b96 100644 --- a/packages/frontend/core/src/adapters/local/index.tsx +++ b/packages/frontend/core/src/adapters/local/index.tsx @@ -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 = { return ( initEmptyPage(page), [])} onLoad={onLoadEditor} workspace={workspace} /> diff --git a/packages/frontend/core/src/adapters/public-cloud/ui.tsx b/packages/frontend/core/src/adapters/public-cloud/ui.tsx index 4d32298cec..120be63620 100644 --- a/packages/frontend/core/src/adapters/public-cloud/ui.tsx +++ b/packages/frontend/core/src/adapters/public-cloud/ui.tsx @@ -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 ( initEmptyPage(page), [])} onLoad={onLoadEditor} workspace={workspace.blockSuiteWorkspace} /> diff --git a/packages/frontend/core/src/components/page-detail-editor.tsx b/packages/frontend/core/src/components/page-detail-editor.tsx index 8050890d11..084843a2bc 100644 --- a/packages/frontend/core/src/components/page-detail-editor.tsx +++ b/packages/frontend/core/src/components/page-detail-editor.tsx @@ -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 - ) => Promise | 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(); + 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 ( <> ) => { - 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 && } diff --git a/packages/frontend/core/src/layouts/workspace-layout.tsx b/packages/frontend/core/src/layouts/workspace-layout.tsx index 3e28b5cbab..94017fb8e1 100644 --- a/packages/frontend/core/src/layouts/workspace-layout.tsx +++ b/packages/frontend/core/src/layouts/workspace-layout.tsx @@ -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 ? : children} - + diff --git a/packages/frontend/core/src/pages/share/detail-page.tsx b/packages/frontend/core/src/pages/share/detail-page.tsx index 70fd48689e..ab9bb26a58 100644 --- a/packages/frontend/core/src/pages/share/detail-page.tsx +++ b/packages/frontend/core/src/pages/share/detail-page.tsx @@ -70,7 +70,6 @@ export const Component = (): ReactElement => { isPublic workspace={page.workspace} pageId={page.id} - onInit={noop} onLoad={useCallback(() => noop, [])} /> diff --git a/tests/affine-local/e2e/quick-search.spec.ts b/tests/affine-local/e2e/quick-search.spec.ts index 385c767239..a35bb61740 100644 --- a/tests/affine-local/e2e/quick-search.spec.ts +++ b/tests/affine-local/e2e/quick-search.spec.ts @@ -372,7 +372,7 @@ test('show not found item', async ({ page }) => { await expect(notFoundItem).toHaveText('Search for "test123456"'); }); -test('can use cmdk to search page content and scroll to it', async ({ +test('can use cmdk to search page content and scroll to it, then the block will be selected', async ({ page, }) => { await openHomePage(page); @@ -382,8 +382,8 @@ test('can use cmdk to search page content and scroll to it', async ({ await getBlockSuiteEditorTitle(page).fill( 'this is a new page to search for content' ); - for (let i = 0; i < 50; i++) { - await page.keyboard.press('Enter'); + for (let i = 0; i < 30; i++) { + await page.keyboard.press('Enter', { delay: 10 }); } await page.keyboard.insertText('123456'); await clickSideBarAllPageButton(page); @@ -398,4 +398,6 @@ test('can use cmdk to search page content and scroll to it', async ({ await waitForScrollToFinish(page); const isVisitable = await checkElementIsInView(page, '123456'); expect(isVisitable).toBe(true); + const selectionElement = page.locator('affine-block-selection'); + await expect(selectionElement).toBeVisible(); }); diff --git a/tests/storybook/src/stories/image-preview-modal.stories.tsx b/tests/storybook/src/stories/image-preview-modal.stories.tsx index 9aa738a27d..26f798495c 100644 --- a/tests/storybook/src/stories/image-preview-modal.stories.tsx +++ b/tests/storybook/src/stories/image-preview-modal.stories.tsx @@ -1,13 +1,11 @@ -import { BlockHubWrapper } from '@affine/component/block-hub'; +import { RootBlockHub } from '@affine/component/block-hub'; import { BlockSuiteEditor } from '@affine/component/block-suite-editor'; import { WorkspaceFlavour } from '@affine/env/workspace'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { ImagePreviewModal } from '@affine/image-preview-plugin/src/component'; -import { rootBlockHubAtom } from '@affine/workspace/atom'; import { getOrCreateWorkspace } from '@affine/workspace/manager'; import type { Meta } from '@storybook/react'; import { initEmptyPage } from '@toeverything/infra/blocksuite'; -import { useCallback } from 'react'; import { createPortal } from 'react-dom'; export default { @@ -54,24 +52,13 @@ export const Default = () => { overflow: 'auto', }} > - initEmptyPage(page), [])} - /> + {createPortal( , document.body )}
- + ); };