mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-23 21:38:44 +08:00
refactor(core): better block link (#8082)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
+21
-47
@@ -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
-12
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user