refactor(core): better block link (#8082)

This commit is contained in:
EYHN
2024-09-04 08:13:21 +00:00
parent 2799e8cd43
commit 01b2339173
17 changed files with 282 additions and 253 deletions
@@ -6,7 +6,6 @@ import {
} from '@affine/core/modules/peek-view';
import { WorkbenchLink } from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import type { BlockStdScope } from '@blocksuite/block-std';
import type { DocMode } from '@blocksuite/blocks';
import {
BlockLinkIcon,
@@ -17,16 +16,16 @@ import {
} from '@blocksuite/icons/rc';
import type { DocCollection } from '@blocksuite/store';
import { useService } from '@toeverything/infra';
import { nanoid } from 'nanoid';
import {
type PropsWithChildren,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import * as styles from './styles.css';
import { scrollAnchoring } from './utils';
export interface PageReferenceRendererOptions {
pageId: string;
@@ -90,8 +89,6 @@ export function AffinePageReference({
wrapper: Wrapper,
mode = 'page',
params = {},
isSameDoc = false,
std,
}: {
pageId: string;
docCollection: DocCollection;
@@ -102,16 +99,10 @@ export function AffinePageReference({
blockIds?: string[];
elementIds?: string[];
};
isSameDoc?: boolean;
std?: BlockStdScope;
}) {
const pageMetaHelper = useDocMetaHelper(docCollection);
const journalHelper = useJournalHelper(docCollection);
const t = useI18n();
const [anchor, setAnchor] = useState<{
mode: DocMode;
id: string;
} | null>(null);
const { mode: linkedWithMode, blockIds, elementIds } = params;
@@ -128,21 +119,11 @@ export function AffinePageReference({
const ref = useRef<HTMLAnchorElement>(null);
const [refreshKey, setRefreshKey] = useState<string>(() => nanoid());
const peekView = useService(PeekViewService).peekView;
const isInPeekView = useInsidePeekView();
useEffect(() => {
if (isSameDoc) {
if (mode === 'edgeless' && elementIds?.length) {
setAnchor({ mode, id: elementIds[0] });
} else if (blockIds?.length) {
setAnchor({ mode, id: blockIds[0] });
}
} else {
setAnchor(null);
}
}, [std, isSameDoc, mode, blockIds, elementIds]);
const onClick = useCallback(
(e: React.MouseEvent) => {
if (e.shiftKey && ref.current) {
@@ -151,33 +132,34 @@ export function AffinePageReference({
peekView.open(ref.current).catch(console.error);
}
if (std && anchor) {
e.preventDefault();
e.stopPropagation();
const { mode, id } = anchor;
scrollAnchoring(std, mode, id);
} else if (isInPeekView) {
if (isInPeekView) {
peekView.close();
}
// update refresh key
setRefreshKey(nanoid());
return;
},
[isInPeekView, peekView, anchor, std]
[isInPeekView, peekView]
);
// A block/element reference link
const search = new URLSearchParams();
if (linkedWithMode) {
search.set('mode', linkedWithMode);
}
if (blockIds?.length) {
search.set('blockIds', blockIds.join(','));
}
if (elementIds?.length) {
search.set('elementIds', elementIds.join(','));
}
const query = useMemo(() => {
// A block/element reference link
const search = new URLSearchParams();
if (linkedWithMode) {
search.set('mode', linkedWithMode);
}
if (blockIds?.length) {
search.set('blockIds', blockIds.join(','));
}
if (elementIds?.length) {
search.set('elementIds', elementIds.join(','));
}
search.set('refreshKey', refreshKey);
const query = search.size > 0 ? `?${search.toString()}` : '';
return search.size > 0 ? `?${search.toString()}` : '';
}, [blockIds, elementIds, linkedWithMode, refreshKey]);
return (
<WorkbenchLink
@@ -1,108 +0,0 @@
import type { BlockStdScope } from '@blocksuite/block-std';
import type {
DocMode,
EdgelessRootService,
PageRootService,
} from '@blocksuite/blocks';
import { Bound, deserializeXYWH } from '@blocksuite/global/utils';
function scrollAnchoringInEdgelessMode(
service: EdgelessRootService,
id: string
) {
requestAnimationFrame(() => {
let isNotInNote = true;
let bounds: Bound | null = null;
const blockComponent = service.std.view.getBlock(id);
const parentComponent = blockComponent?.parentComponent;
if (parentComponent && parentComponent.flavour === 'affine:note') {
isNotInNote = false;
const selection = parentComponent.std.selection;
if (!selection) return;
selection.set([
selection.create('block', {
blockId: id,
}),
]);
const { left: x, width: w } = parentComponent.getBoundingClientRect();
const { top: y, height: h } = blockComponent.getBoundingClientRect();
const coord = service.viewport.toModelCoordFromClientCoord([x, y]);
bounds = new Bound(
coord[0],
coord[1],
w / service.viewport.zoom,
h / service.viewport.zoom
);
} else {
const model = service.getElementById(id);
if (!model) return;
bounds = Bound.fromXYWH(deserializeXYWH(model.xywh));
}
if (!bounds) return;
if (isNotInNote) {
service.selection.set({
elements: [id],
editing: false,
});
}
const { zoom, centerX, centerY } = service.getFitToScreenData(
[20, 20, 20, 20],
[bounds]
);
service.viewport.setViewport(zoom, [centerX, centerY]);
// const surfaceComponent = service.std.view.getBlock(service.surface.id);
// if (!surfaceComponent) return;
// (surfaceComponent as SurfaceBlockComponent).refresh();
// TODO(@fundon): toolbar should be hidden
});
}
function scrollAnchoringInPageMode(service: PageRootService, id: string) {
const blockComponent = service.std.view.getBlock(id);
if (!blockComponent) return;
blockComponent.scrollIntoView({
behavior: 'instant',
block: 'center',
});
const selection = service.std.selection;
if (!selection) return;
selection.set([
selection.create('block', {
blockId: id,
}),
]);
// TODO(@fundon): toolbar should be hidden
}
// TODO(@fundon): it should be a command
export function scrollAnchoring(std: BlockStdScope, mode: DocMode, id: string) {
if (mode === 'edgeless') {
const service = std.getService<EdgelessRootService>('affine:page');
if (!service) return;
scrollAnchoringInEdgelessMode(service, id);
return;
}
const service = std.getService<PageRootService>('affine:page');
if (!service) return;
scrollAnchoringInPageMode(service, id);
}
@@ -1,7 +1,6 @@
import type { EditorSelector } from '@affine/core/modules/editor';
import { EditorService } from '@affine/core/modules/editor';
import type { ReferenceInfo } from '@blocksuite/affine-model';
import type { DocMode } from '@blocksuite/blocks';
import type { InlineEditor } from '@blocksuite/inline/inline-editor';
import type {
AffineEditorContainer,
DocTitle,
@@ -9,6 +8,7 @@ import type {
PageEditor,
} from '@blocksuite/presets';
import { type Doc, Slot } from '@blocksuite/store';
import { useService } from '@toeverything/infra';
import clsx from 'clsx';
import type React from 'react';
import {
@@ -18,10 +18,8 @@ import {
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { scrollAnchoring } from '../../affine/reference-link/utils';
import { BlocksuiteDocEditor, BlocksuiteEdgelessEditor } from './lit-adaper';
import * as styles from './styles.css';
@@ -46,7 +44,6 @@ interface BlocksuiteEditorContainerProps {
shared?: boolean;
className?: string;
style?: React.CSSProperties;
defaultEditorSelector?: EditorSelector;
}
// mimic the interface of the webcomponent and expose slots & host
@@ -60,21 +57,14 @@ export const BlocksuiteEditorContainer = forwardRef<
AffineEditorContainer,
BlocksuiteEditorContainerProps
>(function AffineEditorContainer(
{ page, mode, className, style, shared, defaultEditorSelector },
{ page, mode, className, style, shared },
ref
) {
const editorService = useService(EditorService);
const rootRef = useRef<HTMLDivElement>(null);
const docRef = useRef<PageEditor>(null);
const docTitleRef = useRef<DocTitle>(null);
const edgelessRef = useRef<EdgelessEditor>(null);
const [anchor] = useState<string | null>(() => {
if (mode === 'edgeless' && defaultEditorSelector?.elementIds?.length) {
return defaultEditorSelector.elementIds[0];
} else if (defaultEditorSelector?.blockIds?.length) {
return defaultEditorSelector.blockIds[0];
}
return null;
});
const slots: BlocksuiteEditorContainerRef['slots'] = useMemo(() => {
return {
@@ -180,40 +170,24 @@ export const BlocksuiteEditorContainer = forwardRef<
}, [affineEditorContainerProxy, ref]);
useEffect(() => {
if (anchor) {
let canceled = false;
affineEditorContainerProxy.updateComplete
.then(() => {
if (!canceled) {
const std = affineEditorContainerProxy.host?.std;
if (std) {
scrollAnchoring(std, mode, anchor);
}
}
})
.catch(console.error);
return () => {
canceled = true;
};
} else {
// if no anchor, focus the title
let canceled = false;
let canceled = false;
let unsubscribe: () => void = () => {};
affineEditorContainerProxy.updateComplete
.then(() => {
if (!canceled) {
const title = docTitleRef.current?.querySelector<
HTMLElement & { inlineEditor: InlineEditor }
>('rich-text');
title?.inlineEditor.focusEnd();
}
})
.catch(console.error);
return () => {
canceled = true;
};
}
}, [anchor, affineEditorContainerProxy, mode]);
affineEditorContainerProxy.updateComplete
.then(() => {
if (!canceled) {
unsubscribe = editorService.editor.bindEditorContainer(
affineEditorContainerProxy,
docTitleRef.current as DocTitle
);
}
})
.catch(console.error);
return () => {
canceled = true;
unsubscribe();
};
}, [affineEditorContainerProxy, mode, editorService]);
const handleClickPageModeBlank = useCallback(() => {
affineEditorContainerProxy.host?.std.command.exec(
@@ -1,5 +1,4 @@
import { EditorLoading } from '@affine/component/page-detail-skeleton';
import type { EditorSelector } from '@affine/core/modules/editor';
import type { DocMode } from '@blocksuite/blocks';
import { assertExists } from '@blocksuite/global/utils';
import type { AffineEditorContainer } from '@blocksuite/presets';
@@ -30,7 +29,6 @@ export type EditorProps = {
onLoadEditor?: (editor: AffineEditorContainer) => () => void;
style?: CSSProperties;
className?: string;
defaultEditorSelector?: EditorSelector;
};
function usePageRoot(page: Doc) {
@@ -57,15 +55,7 @@ function usePageRoot(page: Doc) {
const BlockSuiteEditorImpl = forwardRef<AffineEditorContainer, EditorProps>(
function BlockSuiteEditorImpl(
{
mode,
page,
className,
onLoadEditor,
shared,
style,
defaultEditorSelector,
},
{ mode, page, className, onLoadEditor, shared, style },
ref
) {
usePageRoot(page);
@@ -115,7 +105,6 @@ const BlockSuiteEditorImpl = forwardRef<AffineEditorContainer, EditorProps>(
ref={onRefChange}
className={className}
style={style}
defaultEditorSelector={defaultEditorSelector}
/>
);
}
@@ -77,20 +77,16 @@ const usePatchSpecs = (page: Doc, shared: boolean, mode: DocMode) => {
const pageId = data.pageId;
if (!pageId) return <span />;
const isSameDoc = pageId === page.id;
return (
<AffinePageReference
docCollection={page.collection}
pageId={pageId}
mode={mode}
params={data.params}
std={reference.std}
isSameDoc={isSameDoc}
/>
);
};
}, [mode, page.collection, page.id]);
}, [mode, page.collection]);
const specs = useMemo(() => {
return mode === 'edgeless'
@@ -40,6 +40,7 @@ export const EditorModeSwitch = () => {
const togglePage = useCallback(() => {
if (currentMode === 'page' || isSharedMode || trash) return;
editor.setMode('page');
editor.setSelector(undefined);
editor.doc.setPrimaryMode('page');
toast(t['com.affine.toastMessage.pageMode']());
track.$.header.actions.switchPageMode({ mode: 'page' });
@@ -48,6 +49,7 @@ export const EditorModeSwitch = () => {
const toggleEdgeless = useCallback(() => {
if (currentMode === 'edgeless' || isSharedMode || trash) return;
editor.setMode('edgeless');
editor.setSelector(undefined);
editor.doc.setPrimaryMode('edgeless');
toast(t['com.affine.toastMessage.edgelessMode']());
track.$.header.actions.switchPageMode({ mode: 'edgeless' });
@@ -11,7 +11,7 @@ import clsx from 'clsx';
import type { CSSProperties } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { type EditorSelector, EditorService } from '../modules/editor';
import { EditorService } from '../modules/editor';
import {
EditorSettingService,
fontStyleOptions,
@@ -35,13 +35,11 @@ export interface PageDetailEditorProps {
docCollection: DocCollection;
pageId: string;
onLoad?: OnLoadEditor;
defaultEditorSelector?: EditorSelector;
}
const PageDetailEditorMain = memo(function PageDetailEditorMain({
page,
onLoad,
defaultEditorSelector,
}: PageDetailEditorProps & { page: BlockSuiteDoc }) {
const editor = useService(EditorService).editor;
const mode = useLiveData(editor.mode$);
@@ -108,7 +106,6 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
mode={mode}
page={page}
shared={isSharedMode}
defaultEditorSelector={defaultEditorSelector}
onLoadEditor={onLoadEditor}
/>
);