diff --git a/packages/frontend/component/src/ui/modal/confirm-modal.tsx b/packages/frontend/component/src/ui/modal/confirm-modal.tsx index bdff9e96e..935b159d7 100644 --- a/packages/frontend/component/src/ui/modal/confirm-modal.tsx +++ b/packages/frontend/component/src/ui/modal/confirm-modal.tsx @@ -37,8 +37,17 @@ export const ConfirmModal = ({ }, [onConfirm]); return ( { + e.stopPropagation(); + onCancel?.(); + }, + }} width={width} + closeButtonOptions={{ + onClick: onCancel, + }} {...props} > {children ? ( diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx index 414425294..d1d413160 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx @@ -22,7 +22,6 @@ import { } from 'react'; import { BlocksuiteDocEditor, BlocksuiteEdgelessEditor } from './lit-adaper'; -import type { ReferenceReactRenderer } from './specs/custom/patch-reference-renderer'; import * as styles from './styles.css'; // copy forwardSlot from blocksuite, but it seems we need to dispose the pipe @@ -46,7 +45,6 @@ interface BlocksuiteEditorContainerProps { className?: string; style?: React.CSSProperties; defaultSelectedBlockId?: string; - referenceRenderer?: ReferenceReactRenderer; } // mimic the interface of the webcomponent and expose slots & host @@ -100,7 +98,7 @@ export const BlocksuiteEditorContainer = forwardRef< AffineEditorContainer, BlocksuiteEditorContainerProps >(function AffineEditorContainer( - { page, mode, className, style, defaultSelectedBlockId, referenceRenderer }, + { page, mode, className, style, defaultSelectedBlockId }, ref ) { const scrolledRef = useRef(false); @@ -303,17 +301,9 @@ export const BlocksuiteEditorContainer = forwardRef< ref={rootRef} > {mode === 'page' ? ( - + ) : ( - + )} ); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor.tsx index f820f1363..8f39e7c0f 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor.tsx @@ -10,14 +10,11 @@ import { Suspense, useCallback, useEffect, - useMemo, useRef, } from 'react'; -import { AffinePageReference } from '../../affine/reference-link'; import { BlocksuiteEditorContainer } from './blocksuite-editor-container'; import { NoPageRootError } from './no-page-error'; -import type { ReferenceReactRenderer } from './specs/custom/patch-reference-renderer'; export type ErrorBoundaryProps = { onReset?: () => void; @@ -88,19 +85,6 @@ const BlockSuiteEditorImpl = forwardRef( }; }, []); - const referenceRenderer: ReferenceReactRenderer = useMemo(() => { - return function customReference(reference) { - const pageId = reference.delta.attributes?.reference?.pageId; - if (!pageId) return ; - return ( - - ); - }; - }, [page.collection]); - return ( ( ref={onRefChange} className={className} style={style} - referenceRenderer={referenceRenderer} defaultSelectedBlockId={defaultSelectedBlockId} /> ); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx index 272e2dbc0..8b52be1ad 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx @@ -1,9 +1,11 @@ import { createReactComponentFromLit, + useConfirmModal, useLitPortalFactory, } from '@affine/component'; import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; import { WorkbenchService } from '@affine/core/modules/workbench'; +import type { BlockSpec } from '@blocksuite/block-std'; import { BiDirectionalLinkPanel, DocMetaTags, @@ -24,11 +26,13 @@ import React, { } from 'react'; import { PagePropertiesTable } from '../../affine/page-properties'; +import { AffinePageReference } from '../../affine/reference-link'; import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title'; import { + patchNotificationService, patchReferenceRenderer, type ReferenceReactRenderer, -} from './specs/custom/patch-reference-renderer'; +} from './specs/custom/spec-patchers'; import { EdgelessModeSpecs } from './specs/edgeless'; import { PageModeSpecs } from './specs/page'; import * as styles from './styles.css'; @@ -58,14 +62,49 @@ const adapted = { interface BlocksuiteEditorProps { page: Doc; - referenceRenderer?: ReferenceReactRenderer; - // todo: add option to replace docTitle with custom component (e.g., for journal page) } +const usePatchSpecs = (page: Doc, specs: BlockSpec[]) => { + const [reactToLit, portals] = useLitPortalFactory(); + const referenceRenderer: ReferenceReactRenderer = useMemo(() => { + return function customReference(reference) { + const pageId = reference.delta.attributes?.reference?.pageId; + if (!pageId) return ; + return ( + + ); + }; + }, [page.collection]); + + const confirmModal = useConfirmModal(); + const patchedSpecs = useMemo(() => { + let patched = patchReferenceRenderer(specs, reactToLit, referenceRenderer); + patched = patchNotificationService( + patchReferenceRenderer(patched, reactToLit, referenceRenderer), + confirmModal + ); + return patched; + }, [confirmModal, reactToLit, referenceRenderer, specs]); + + return [ + patchedSpecs, + useMemo( + () => ( + <> + {portals.map(p => ( + {p.portal} + ))} + + ), + [portals] + ), + ] as const; +}; + export const BlocksuiteDocEditor = forwardRef< PageEditor, BlocksuiteEditorProps ->(function BlocksuiteDocEditor({ page, referenceRenderer }, ref) { +>(function BlocksuiteDocEditor({ page }, ref) { const titleRef = useRef(null); const docRef = useRef(null); const [docPage, setDocPage] = @@ -90,13 +129,6 @@ export const BlocksuiteDocEditor = forwardRef< [ref] ); - const [reactToLit, portals] = useLitPortalFactory(); - - const specs = useMemo(() => { - if (!referenceRenderer) return PageModeSpecs; - return patchReferenceRenderer(PageModeSpecs, reactToLit, referenceRenderer); - }, [reactToLit, referenceRenderer]); - useEffect(() => { // auto focus the title setTimeout(() => { @@ -114,6 +146,8 @@ export const BlocksuiteDocEditor = forwardRef< // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const [specs, portals] = usePatchSpecs(page, PageModeSpecs); + return ( <>
@@ -142,32 +176,19 @@ export const BlocksuiteDocEditor = forwardRef< ) : null}
- {portals.map(p => ( - {p.portal} - ))} + {portals} ); }); - export const BlocksuiteEdgelessEditor = forwardRef< EdgelessEditor, BlocksuiteEditorProps ->(function BlocksuiteEdgelessEditor({ page, referenceRenderer }, ref) { - const [reactToLit, portals] = useLitPortalFactory(); - const specs = useMemo(() => { - if (!referenceRenderer) return EdgelessModeSpecs; - return patchReferenceRenderer( - EdgelessModeSpecs, - reactToLit, - referenceRenderer - ); - }, [reactToLit, referenceRenderer]); +>(function BlocksuiteEdgelessEditor({ page }, ref) { + const [specs, portals] = usePatchSpecs(page, EdgelessModeSpecs); return ( <> - {portals.map(p => ( - {p.portal} - ))} + {portals} ); }); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/patch-reference-renderer.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/patch-reference-renderer.ts deleted file mode 100644 index be7e542a9..000000000 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/patch-reference-renderer.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { ElementOrFactory } from '@affine/component'; -import type { BlockSpec } from '@blocksuite/block-std'; -import type { - AffineReference, - ParagraphBlockService, -} from '@blocksuite/blocks'; -import type { TemplateResult } from 'lit'; - -export type ReferenceReactRenderer = ( - reference: AffineReference -) => React.ReactElement; - -/** - * Patch the block specs with custom renderers. - */ -export function patchReferenceRenderer( - specs: BlockSpec[], - reactToLit: (element: ElementOrFactory) => TemplateResult, - reactRenderer: ReferenceReactRenderer -) { - const litRenderer = (reference: AffineReference) => { - const node = reactRenderer(reference); - return reactToLit(node); - }; - - return specs.map(spec => { - if ( - ['affine:paragraph', 'affine:list', 'affine:database'].includes( - spec.schema.model.flavour - ) - ) { - // todo: remove these type assertions - spec.service = class extends ( - (spec.service as typeof ParagraphBlockService) - ) { - override mounted() { - super.mounted(); - this.referenceNodeConfig.setCustomContent(litRenderer); - } - }; - } - - return spec; - }); -} 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 new file mode 100644 index 000000000..d55a82433 --- /dev/null +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx @@ -0,0 +1,140 @@ +import { + createReactComponentFromLit, + type ElementOrFactory, + toast, + type ToastOptions, + type useConfirmModal, +} from '@affine/component'; +import type { BlockSpec } from '@blocksuite/block-std'; +import type { + AffineReference, + ParagraphBlockService, + RootService, +} from '@blocksuite/blocks'; +import { LitElement, type TemplateResult } from 'lit'; +import React from 'react'; + +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, +}); + +/** + * Patch the block specs with custom renderers. + */ +export function patchReferenceRenderer( + specs: BlockSpec[], + reactToLit: (element: ElementOrFactory) => TemplateResult, + reactRenderer: ReferenceReactRenderer +) { + const litRenderer = (reference: AffineReference) => { + const node = reactRenderer(reference); + return reactToLit(node); + }; + + return specs.map(spec => { + if ( + ['affine:paragraph', 'affine:list', 'affine:database'].includes( + spec.schema.model.flavour + ) + ) { + // todo: remove these type assertions + spec.service = class extends ( + (spec.service as typeof ParagraphBlockService) + ) { + override mounted() { + super.mounted(); + this.referenceNodeConfig.setCustomContent(litRenderer); + } + }; + } + + return spec; + }); +} + +export function patchNotificationService( + specs: BlockSpec[], + { closeConfirmModal, openConfirmModal }: ReturnType +) { + const rootSpec = specs.find( + spec => spec.schema.model.flavour === 'affine:page' + ); + + if (!rootSpec) { + return specs; + } + + rootSpec.service = class extends (rootSpec.service as typeof RootService) { + override notificationService = { + confirm: async ({ + title, + message, + confirmText, + cancelText, + abort, + }: { + title: string; + message: string | TemplateResult; + confirmText: string; + cancelText: string; + abort?: AbortSignal; + }) => { + return new Promise(resolve => { + openConfirmModal({ + title, + description: + typeof message === 'string' ? ( + message + ) : ( + + ), + confirmButtonOptions: { + children: confirmText, + type: 'primary', + }, + cancelText, + onConfirm: () => { + resolve(true); + }, + onCancel: () => { + resolve(false); + }, + }); + abort?.addEventListener('abort', () => { + resolve(false); + closeConfirmModal(); + }); + }); + }, + toast: (message: string, options: ToastOptions) => { + return toast(message, options); + }, + notify: async () => {}, + }; + }; + return specs; +}