From e085b927f6aab4018e9e5abaee3a6083fb4ca223 Mon Sep 17 00:00:00 2001 From: pengx17 Date: Fri, 21 Jun 2024 07:38:42 +0000 Subject: [PATCH] feat(core): peek view api enhancements (#7288) upstream https://github.com/toeverything/blocksuite/pull/7390 fix AF-917 --- .../frontend/component/src/lit-react/index.ts | 1 + .../component/src/lit-react/to-react-node.ts | 35 ++++ .../affine/reference-link/index.tsx | 2 +- .../specs/custom/spec-patchers.tsx | 45 +---- .../modules/peek-view/entities/peek-view.ts | 35 ++-- .../modules/peek-view/view/doc-peek-view.tsx | 176 ++++++------------ .../peek-view/view/modal-container.tsx | 6 +- ...trols.css.ts => peek-view-controls.css.ts} | 0 ...ek-controls.tsx => peek-view-controls.tsx} | 27 ++- .../peek-view/view/peek-view-manager.tsx | 64 +++---- .../src/pages/workspace/all-page/all-page.tsx | 2 +- .../workspace/detail-page/detail-page.tsx | 2 +- .../core/src/pages/workspace/index.tsx | 2 +- packages/frontend/electron/renderer/app.tsx | 2 +- packages/frontend/web/src/app.tsx | 2 +- 15 files changed, 182 insertions(+), 219 deletions(-) create mode 100644 packages/frontend/component/src/lit-react/to-react-node.ts rename packages/frontend/core/src/modules/peek-view/view/{doc-peek-controls.css.ts => peek-view-controls.css.ts} (100%) rename packages/frontend/core/src/modules/peek-view/view/{doc-peek-controls.tsx => peek-view-controls.tsx} (83%) diff --git a/packages/frontend/component/src/lit-react/index.ts b/packages/frontend/component/src/lit-react/index.ts index e3afb1b203c21..fa5dc3777b9d8 100644 --- a/packages/frontend/component/src/lit-react/index.ts +++ b/packages/frontend/component/src/lit-react/index.ts @@ -1,2 +1,3 @@ export { createComponent as createReactComponentFromLit } from './create-component'; export * from './lit-portal'; +export { toReactNode } from './to-react-node'; diff --git a/packages/frontend/component/src/lit-react/to-react-node.ts b/packages/frontend/component/src/lit-react/to-react-node.ts new file mode 100644 index 0000000000000..88b7c17b39fcd --- /dev/null +++ b/packages/frontend/component/src/lit-react/to-react-node.ts @@ -0,0 +1,35 @@ +import { LitElement, type TemplateResult } from 'lit'; +import React, { createElement, type ReactNode } from 'react'; + +import { createComponent } from './create-component'; + +export class LitTemplateWrapper extends LitElement { + static override get properties() { + return { + template: { type: Object }, + }; + } + template: TemplateResult | null = null; + // do not enable shadow root + override createRenderRoot() { + return this; + } + + override render() { + return this.template; + } +} + +window.customElements.define('affine-lit-template-wrapper', LitTemplateWrapper); + +const TemplateWrapper = createComponent({ + elementClass: LitTemplateWrapper, + react: React, +}); + +export const toReactNode = (template?: TemplateResult | string): ReactNode => { + if (!template) return null; + return typeof template === 'string' + ? template + : createElement(TemplateWrapper, { template }); +}; diff --git a/packages/frontend/core/src/components/affine/reference-link/index.tsx b/packages/frontend/core/src/components/affine/reference-link/index.tsx index 3efe9b0a651c5..06e1b5306df91 100644 --- a/packages/frontend/core/src/components/affine/reference-link/index.tsx +++ b/packages/frontend/core/src/components/affine/reference-link/index.tsx @@ -101,7 +101,7 @@ export function AffinePageReference({ if (e.shiftKey && ref.current) { e.preventDefault(); e.stopPropagation(); - peekView.open(ref.current); + peekView.open(ref.current).catch(console.error); return false; // means this click is handled } if (isInPeekView) { diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx index 92c619d4a9dd4..02734c11bd1bc 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx @@ -1,10 +1,10 @@ import { - createReactComponentFromLit, type ElementOrFactory, Input, notify, toast, type ToastOptions, + toReactNode, type useConfirmModal, } from '@affine/component'; import type { @@ -27,47 +27,15 @@ import { type RootService, } from '@blocksuite/blocks'; import type { DocMode, DocService, DocsService } from '@toeverything/infra'; -import { html, LitElement, type TemplateResult } from 'lit'; +import { html, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators.js'; import { literal } from 'lit/static-html.js'; -import React, { createElement, type ReactNode } from 'react'; - -const logger = new DebugLogger('affine::spec-patchers'); export type ReferenceReactRenderer = ( reference: AffineReference ) => React.ReactElement; -export class LitTemplateWrapper extends LitElement { - static override get properties() { - return { - template: { type: Object }, - }; - } - template: TemplateResult | null = null; - // do not enable shadow root - override createRenderRoot() { - return this; - } - - override render() { - return this.template; - } -} - -window.customElements.define('affine-lit-template-wrapper', LitTemplateWrapper); - -const TemplateWrapper = createReactComponentFromLit({ - elementClass: LitTemplateWrapper, - react: React, -}); - -const toReactNode = (template?: TemplateResult | string): ReactNode => { - if (!template) return null; - return typeof template === 'string' - ? template - : createElement(TemplateWrapper, { template }); -}; +const logger = new DebugLogger('affine::spec-patchers'); function patchSpecService( spec: Spec, @@ -274,10 +242,9 @@ export function patchPeekViewService( patchSpecService(rootSpec, pageService => { pageService.peekViewService = { - peek: (target: ActivePeekView['target']) => { - logger.debug('center peek', target); - service.peekView.open(target); - return Promise.resolve(); + peek: (target: ActivePeekView['target'], template?: TemplateResult) => { + logger.debug('center peek', target, template); + return service.peekView.open(target, template); }, }; }); diff --git a/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts b/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts index e7c23a1bc6690..e190c4166968d 100644 --- a/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts +++ b/packages/frontend/core/src/modules/peek-view/entities/peek-view.ts @@ -6,7 +6,12 @@ import { type SurfaceRefBlockComponent, type SurfaceRefBlockModel, } from '@blocksuite/blocks'; +import type { BlockModel } from '@blocksuite/store'; import { type DocMode, Entity, LiveData } from '@toeverything/infra'; +import type { TemplateResult } from 'lit'; +import { firstValueFrom, map, race } from 'rxjs'; + +import { resolveLinkToDoc } from '../../navigation'; export type PeekViewTarget = | HTMLElement @@ -24,13 +29,10 @@ export type DocPeekViewInfo = { export type ActivePeekView = { target: PeekViewTarget; - info: DocPeekViewInfo; + info?: DocPeekViewInfo; + template?: TemplateResult; }; -import type { BlockModel } from '@blocksuite/store'; - -import { resolveLinkToDoc } from '../../navigation'; - const EMBED_DOC_FLAVOURS = [ 'affine:embed-linked-doc', 'affine:embed-synced-doc', @@ -50,8 +52,8 @@ const isSurfaceRefModel = ( function resolvePeekInfoFromPeekTarget( peekTarget?: PeekViewTarget -): DocPeekViewInfo | null { - if (!peekTarget) return null; +): DocPeekViewInfo | undefined { + if (!peekTarget) return; if (peekTarget instanceof AffineReference) { if (peekTarget.refMeta) { return { @@ -91,14 +93,10 @@ function resolvePeekInfoFromPeekTarget( blockId: peekTarget.blockId, }; } - return null; + return; } export class PeekViewEntity extends Entity { - constructor() { - super(); - } - private readonly _active$ = new LiveData(null); private readonly _show$ = new LiveData(false); @@ -108,14 +106,17 @@ export class PeekViewEntity extends Entity { .distinctUntilChanged(); // return true if the peek view will be handled - open = (target: ActivePeekView['target']) => { + open = async ( + target: ActivePeekView['target'], + template?: TemplateResult + ) => { const resolvedInfo = resolvePeekInfoFromPeekTarget(target); - if (!resolvedInfo) { - return false; + if (!resolvedInfo && !template) { + return; } - this._active$.next({ target, info: resolvedInfo }); + this._active$.next({ target, info: resolvedInfo, template }); this._show$.next(true); - return true; + return firstValueFrom(race(this._active$, this.show$).pipe(map(() => {}))); }; close = () => { diff --git a/packages/frontend/core/src/modules/peek-view/view/doc-peek-view.tsx b/packages/frontend/core/src/modules/peek-view/view/doc-peek-view.tsx index 3cc503e6a9aab..f5270bd6f20a4 100644 --- a/packages/frontend/core/src/modules/peek-view/view/doc-peek-view.tsx +++ b/packages/frontend/core/src/modules/peek-view/view/doc-peek-view.tsx @@ -10,28 +10,52 @@ import { DisposableGroup } from '@blocksuite/global/utils'; import type { AffineEditorContainer } from '@blocksuite/presets'; import type { DocMode } from '@toeverything/infra'; import { DocsService, FrameworkScope, useService } from '@toeverything/infra'; -import { - forwardRef, - useCallback, - useEffect, - useImperativeHandle, - useState, -} from 'react'; +import { useEffect, useState } from 'react'; import { WorkbenchService } from '../../workbench'; import { PeekViewService } from '../services/peek-view'; import * as styles from './doc-peek-view.css'; import { useDoc } from './utils'; -export type DocPreviewRef = { - getEditor: () => AffineEditorContainer | null; - fitViewportToTarget: () => void; -}; +function fitViewport( + editor: AffineEditorContainer, + xywh?: `[${number},${number},${number},${number}]` +) { + const rootService = + editor.host.std.spec.getService('affine:page'); + rootService.viewport.onResize(); -const DocPreview = forwardRef< - DocPreviewRef, - { docId: string; blockId?: string; mode?: DocMode } ->(function DocPreview({ docId, blockId, mode }, ref) { + if (xywh) { + const viewport = { + xywh: xywh, + padding: [60, 20, 20, 20] as [number, number, number, number], + }; + rootService.viewport.setViewportByBound( + Bound.deserialize(viewport.xywh), + viewport.padding, + false + ); + } else { + const data = rootService.getFitToScreenData(); + rootService.viewport.setViewport( + data.zoom, + [data.centerX, data.centerY], + false + ); + } +} + +export function DocPeekPreview({ + docId, + blockId, + mode, + xywh, +}: { + docId: string; + blockId?: string; + mode?: DocMode; + xywh?: `[${number},${number},${number},${number}]`; +}) { const { doc, workspace, loading } = useDoc(docId); const { jumpToTag } = useNavigateHelper(); const workbench = useService(WorkbenchService).workbench; @@ -45,26 +69,22 @@ const DocPreview = forwardRef< const docs = useService(DocsService); const [resolvedMode, setResolvedMode] = useState(mode); - useImperativeHandle( - ref, - () => ({ - getEditor: () => editor, - fitViewportToTarget: () => { - if (editor && resolvedMode === 'edgeless') { - const rootService = - editor.host.std.spec.getService('affine:page'); - rootService.viewport.onResize(); - const data = rootService.getFitToScreenData(); - rootService.viewport.setViewport( - data.zoom, - [data.centerX, data.centerY], - false - ); - } - }, - }), - [editor, resolvedMode] - ); + useEffect(() => { + if (editor && resolvedMode === 'edgeless') { + editor.host + .closest('[data-testid="peek-view-modal-animation-container"]') + ?.addEventListener( + 'animationend', + () => { + fitViewport(editor, xywh); + }, + { + once: true, + } + ); + } + return; + }, [editor, resolvedMode, xywh]); useEffect(() => { if (!mode || !resolvedMode) { @@ -95,7 +115,7 @@ const DocPreview = forwardRef< // doc change event inside peek view should be handled by peek view disposableGroup.add( rootService.slots.docLinkClicked.on(({ docId, blockId }) => { - peekView.open({ docId, blockId }); + peekView.open({ docId, blockId }).catch(console.error); }) ); // todo: no tag peek view yet @@ -140,86 +160,4 @@ const DocPreview = forwardRef< ); -}); -DocPreview.displayName = 'DocPreview'; - -export const DocPeekView = forwardRef< - DocPreviewRef, - { - docId: string; - blockId?: string; - mode?: DocMode; - } ->(function DocPeekView({ docId, blockId, mode }, ref) { - return ; -}); - -export type SurfaceRefPeekViewRef = { - fitViewportToTarget: () => void; -}; - -export const SurfaceRefPeekView = forwardRef< - SurfaceRefPeekViewRef, - { docId: string; xywh: `[${number},${number},${number},${number}]` } ->(function SurfaceRefPeekView({ docId, xywh }, ref) { - const [editorRef, setEditorRef] = useState( - null - ); - const onRef = (editor: AffineEditorContainer | null) => { - setEditorRef(editor); - }; - const fitViewportToTarget = useCallback(() => { - if (!editorRef) { - return; - } - - const viewport = { - xywh: xywh, - padding: [60, 20, 20, 20] as [number, number, number, number], - }; - const rootService = - editorRef.host.std.spec.getService('affine:page'); - rootService.viewport.onResize(); - rootService.viewport.setViewportByBound( - Bound.deserialize(viewport.xywh), - viewport.padding - ); - }, [editorRef, xywh]); - - useImperativeHandle( - ref, - () => ({ - fitViewportToTarget, - }), - [fitViewportToTarget] - ); - - useEffect(() => { - let mounted = true; - if (editorRef) { - editorRef.host?.updateComplete - .then(() => { - if (mounted) { - fitViewportToTarget(); - } - }) - .catch(e => { - console.error(e); - }); - } - return () => { - mounted = false; - }; - }, [editorRef, fitViewportToTarget]); - - return ( - { - onRef(ref?.getEditor() ?? null); - }} - docId={docId} - mode={'edgeless'} - /> - ); -}); -SurfaceRefPeekView.displayName = 'SurfaceRefPeekView'; +} diff --git a/packages/frontend/core/src/modules/peek-view/view/modal-container.tsx b/packages/frontend/core/src/modules/peek-view/view/modal-container.tsx index a8d21a8ffbf53..740bc6d8f2e48 100644 --- a/packages/frontend/core/src/modules/peek-view/view/modal-container.tsx +++ b/packages/frontend/core/src/modules/peek-view/view/modal-container.tsx @@ -112,7 +112,11 @@ export const PeekViewModalContainer = ({ [styles.animationTimeout]: `${animationTimeout}ms`, })} > -
+
& { mode?: DocMode; }; +export const DefaultPeekViewControls = ({ + className, + ...rest +}: HTMLAttributes) => { + const peekView = useService(PeekViewService).peekView; + const t = useI18n(); + const controls = useMemo(() => { + return [ + { + icon: , + nameKey: 'close', + name: t['com.affine.peek-view-controls.close'](), + onClick: peekView.close, + }, + ].filter((opt): opt is ControlButtonProps => Boolean(opt)); + }, [peekView, t]); + return ( +
+ {controls.map(option => ( + + ))} +
+ ); +}; + export const DocPeekViewControls = ({ docId, blockId, diff --git a/packages/frontend/core/src/modules/peek-view/view/peek-view-manager.tsx b/packages/frontend/core/src/modules/peek-view/view/peek-view-manager.tsx index 084adb7640aaa..059d2a69442e5 100644 --- a/packages/frontend/core/src/modules/peek-view/view/peek-view-manager.tsx +++ b/packages/frontend/core/src/modules/peek-view/view/peek-view-manager.tsx @@ -1,32 +1,30 @@ +import { toReactNode } from '@affine/component'; import { BlockElement } from '@blocksuite/block-std'; import { useLiveData, useService } from '@toeverything/infra'; -import { useEffect, useMemo, useRef } from 'react'; +import { useEffect, useMemo } from 'react'; import type { ActivePeekView } from '../entities/peek-view'; import { PeekViewService } from '../services/peek-view'; -import { DocPeekViewControls } from './doc-peek-controls'; -import type { DocPreviewRef, SurfaceRefPeekViewRef } from './doc-peek-view'; -import { DocPeekView, SurfaceRefPeekView } from './doc-peek-view'; +import { DocPeekPreview } from './doc-peek-view'; import { PeekViewModalContainer } from './modal-container'; +import { + DefaultPeekViewControls, + DocPeekViewControls, +} from './peek-view-controls'; -function renderPeekView( - { info }: ActivePeekView, - refCallback: (editor: SurfaceRefPeekViewRef | DocPreviewRef | null) => void -) { - if (info.mode === 'edgeless' && info.xywh) { - return ( - - ); +function renderPeekView({ info, template }: ActivePeekView) { + if (template) { + return toReactNode(template); + } + + if (!info) { + return null; } return ( - @@ -34,29 +32,26 @@ function renderPeekView( } const renderControls = ({ info }: ActivePeekView) => { - return ( - - ); + if (info && 'docId' in info) { + return ( + + ); + } + + return ; }; export const PeekViewManagerModal = () => { const peekViewEntity = useService(PeekViewService).peekView; const activePeekView = useLiveData(peekViewEntity.active$); const show = useLiveData(peekViewEntity.show$); - const peekViewRef = useRef( - null - ); const preview = useMemo(() => { - return activePeekView - ? renderPeekView(activePeekView, editor => { - peekViewRef.current = editor; - }) - : null; + return activePeekView ? renderPeekView(activePeekView) : null; }, [activePeekView]); const controls = useMemo(() => { @@ -89,9 +84,6 @@ export const PeekViewManagerModal = () => { peekViewEntity.close(); } }} - onAnimateEnd={() => { - peekViewRef.current?.fitViewportToTarget(); - }} > {preview} diff --git a/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx b/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx index d0b15b71bb067..aa5632e66ba7a 100644 --- a/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx +++ b/packages/frontend/core/src/pages/workspace/all-page/all-page.tsx @@ -56,7 +56,7 @@ export const AllPage = () => { }; export const Component = () => { - performanceRenderLogger.info('AllPage'); + performanceRenderLogger.debug('AllPage'); return ; }; diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index a1dd2de98bb50..234300805bdc5 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -362,7 +362,7 @@ export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => { }; export const Component = () => { - performanceRenderLogger.info('DetailPage'); + performanceRenderLogger.debug('DetailPage'); const params = useParams(); const recentPages = useService(RecentPagesService); diff --git a/packages/frontend/core/src/pages/workspace/index.tsx b/packages/frontend/core/src/pages/workspace/index.tsx index 093b57952c62f..8b25b8811920c 100644 --- a/packages/frontend/core/src/pages/workspace/index.tsx +++ b/packages/frontend/core/src/pages/workspace/index.tsx @@ -37,7 +37,7 @@ declare global { } export const Component = (): ReactElement => { - performanceRenderLogger.info('WorkspaceLayout'); + performanceRenderLogger.debug('WorkspaceLayout'); const params = useParams(); diff --git a/packages/frontend/electron/renderer/app.tsx b/packages/frontend/electron/renderer/app.tsx index aa8512e2bb75c..cf05fa14aab67 100644 --- a/packages/frontend/electron/renderer/app.tsx +++ b/packages/frontend/electron/renderer/app.tsx @@ -93,7 +93,7 @@ window.addEventListener('focus', () => { frameworkProvider.get(LifecycleService).applicationStart(); export function App() { - performanceRenderLogger.info('App'); + performanceRenderLogger.debug('App'); if (!languageLoadingPromise) { languageLoadingPromise = loadLanguage().catch(console.error); diff --git a/packages/frontend/web/src/app.tsx b/packages/frontend/web/src/app.tsx index 6bf5b8efded6b..95068b362d587 100644 --- a/packages/frontend/web/src/app.tsx +++ b/packages/frontend/web/src/app.tsx @@ -81,7 +81,7 @@ window.addEventListener('focus', () => { frameworkProvider.get(LifecycleService).applicationStart(); export function App() { - performanceRenderLogger.info('App'); + performanceRenderLogger.debug('App'); if (!languageLoadingPromise) { languageLoadingPromise = loadLanguage().catch(console.error);