diff --git a/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/configs/edgeless-interaction.ts b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/configs/edgeless-interaction.ts new file mode 100644 index 000000000..a4ed7a764 --- /dev/null +++ b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/configs/edgeless-interaction.ts @@ -0,0 +1,85 @@ +import { + EmbedSyncedDocBlockSchema, + SYNCED_MIN_HEIGHT, + SYNCED_MIN_WIDTH, +} from '@blocksuite/affine-model'; +import { clamp } from '@blocksuite/global/gfx'; +import { GfxViewInteractionExtension } from '@blocksuite/std/gfx'; + +import type { EmbedEdgelessSyncedDocBlockComponent } from '../embed-edgeless-synced-doc-block'; +import { calcSyncedDocFullHeight } from '../utils'; + +export const EmbedSyncedDocInteraction = + GfxViewInteractionExtension( + EmbedSyncedDocBlockSchema.model.flavour, + { + resizeConstraint: { + minWidth: SYNCED_MIN_WIDTH, + minHeight: SYNCED_MIN_HEIGHT, + }, + + handleRotate: () => { + return { + beforeRotate(context) { + context.set({ + rotatable: false, + }); + }, + }; + }, + + handleResize: ({ view, model }) => { + const initialScale = model.props.scale ?? 1; + const initHeight = model.elementBound.h; + const maxHeight = calcSyncedDocFullHeight(view); + + return { + beforeResize: context => { + context.set({ maxHeight }); + }, + onResizeStart: context => { + context.default(context); + model.stash('scale'); + model.stash('preFoldHeight'); + }, + onResizeMove: context => { + const { lockRatio, originalBound, constraint, newBound } = context; + + let scale = initialScale; + const realWidth = originalBound.w / initialScale; + + if (lockRatio) { + scale = newBound.w / realWidth; + } + + const newWidth = newBound.w / scale; + + newBound.w = + clamp(newWidth, constraint.minWidth, constraint.maxWidth) * scale; + newBound.h = + clamp(newBound.h, constraint.minHeight, constraint.maxHeight) * + scale; + + const newHeight = newBound.h / scale; + + // only adjust height check the fold state + if (originalBound.w === newBound.w) { + let preFoldHeight = 0; + if (newHeight === constraint.minHeight) { + preFoldHeight = initHeight; + } + model.props.preFoldHeight = preFoldHeight; + } + + model.props.scale = scale; + model.xywh = newBound.serialize(); + }, + onResizeEnd: context => { + context.default(context); + model.pop('scale'); + model.pop('preFoldHeight'); + }, + }; + }, + } + ); diff --git a/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/embed-edgeless-synced-doc-block.ts b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/embed-edgeless-synced-doc-block.ts index 3122770fa..176c18637 100644 --- a/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/embed-edgeless-synced-doc-block.ts +++ b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/embed-edgeless-synced-doc-block.ts @@ -3,12 +3,7 @@ import { EdgelessCRUDIdentifier, reassociateConnectorsCommand, } from '@blocksuite/affine-block-surface'; -import { - type AliasInfo, - EmbedSyncedDocBlockSchema, - SYNCED_MIN_HEIGHT, - SYNCED_MIN_WIDTH, -} from '@blocksuite/affine-model'; +import { type AliasInfo } from '@blocksuite/affine-model'; import { EMBED_CARD_HEIGHT, EMBED_CARD_WIDTH, @@ -17,11 +12,10 @@ import { ThemeExtensionIdentifier, ThemeProvider, } from '@blocksuite/affine-shared/services'; -import { Bound, clamp } from '@blocksuite/global/gfx'; +import { Bound } from '@blocksuite/global/gfx'; import { type BlockComponent, BlockStdScope } from '@blocksuite/std'; -import { GfxViewInteractionExtension } from '@blocksuite/std/gfx'; import { html, nothing } from 'lit'; -import { query, queryAsync } from 'lit/decorators.js'; +import { query } from 'lit/decorators.js'; import { choose } from 'lit/directives/choose.js'; import { classMap } from 'lit/directives/class-map.js'; import { guard } from 'lit/directives/guard.js'; @@ -37,8 +31,8 @@ export class EmbedEdgelessSyncedDocBlockComponent extends toEdgelessEmbedBlock( @query('.affine-embed-synced-doc-edgeless-header-wrapper') accessor headerWrapper: HTMLDivElement | null = null; - @queryAsync('affine-preview-root') - accessor contentElement!: Promise; + @query('affine-preview-root') + accessor contentElement: BlockComponent | null = null; protected override _renderSyncedView = () => { const { syncedDoc, editorMode } = this; @@ -205,60 +199,3 @@ export class EmbedEdgelessSyncedDocBlockComponent extends toEdgelessEmbedBlock( override accessor useCaptionEditor = true; } - -export const EmbedSyncedDocInteraction = - GfxViewInteractionExtension( - EmbedSyncedDocBlockSchema.model.flavour, - { - resizeConstraint: { - minWidth: SYNCED_MIN_WIDTH, - minHeight: SYNCED_MIN_HEIGHT, - }, - - handleRotate: () => { - return { - beforeRotate(context) { - context.set({ - rotatable: false, - }); - }, - }; - }, - - handleResize: ({ model }) => { - const initialScale = model.props.scale ?? 1; - - return { - onResizeStart: context => { - context.default(context); - model.stash('scale'); - }, - onResizeMove: context => { - const { lockRatio, originalBound, constraint, newBound } = context; - - let scale = initialScale; - const realWidth = originalBound.w / initialScale; - - if (lockRatio) { - scale = newBound.w / realWidth; - } - - const newWidth = newBound.w / scale; - - newBound.w = - clamp(newWidth, constraint.minWidth, constraint.maxWidth) * scale; - newBound.h = - clamp(newBound.h, constraint.minHeight, constraint.maxHeight) * - scale; - - model.props.scale = scale; - model.xywh = newBound.serialize(); - }, - onResizeEnd: context => { - context.default(context); - model.pop('scale'); - }, - }; - }, - } - ); diff --git a/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/embed-synced-doc-spec.ts b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/embed-synced-doc-spec.ts index a4594ec5b..2ccae6cbd 100644 --- a/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/embed-synced-doc-spec.ts +++ b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/embed-synced-doc-spec.ts @@ -5,7 +5,6 @@ import { literal } from 'lit/static-html.js'; import { EmbedSyncedDocBlockAdapterExtensions } from './adapters/extension'; import { createBuiltinToolbarConfigExtension } from './configs/toolbar'; -import { EmbedSyncedDocInteraction } from './embed-edgeless-synced-doc-block'; import { HeightInitializationExtension } from './init-height-extension'; const flavour = EmbedSyncedDocBlockSchema.model.flavour; @@ -30,5 +29,4 @@ export const EmbedSyncedDocViewExtensions: ExtensionType[] = [ }), createBuiltinToolbarConfigExtension(flavour), HeightInitializationExtension, - EmbedSyncedDocInteraction, ].flat(); diff --git a/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/init-height-extension.ts b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/init-height-extension.ts index 9ad5b9f1e..77fc1c5ca 100644 --- a/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/init-height-extension.ts +++ b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/init-height-extension.ts @@ -1,13 +1,9 @@ -import { - EmbedSyncedDocBlockSchema, - SYNCED_DEFAULT_MAX_HEIGHT, - SYNCED_MIN_HEIGHT, -} from '@blocksuite/affine-model'; +import { EmbedSyncedDocBlockSchema } from '@blocksuite/affine-model'; import { DisposableGroup } from '@blocksuite/global/disposable'; -import { clamp } from '@blocksuite/global/gfx'; import { LifeCycleWatcher } from '@blocksuite/std'; import { EmbedEdgelessSyncedDocBlockComponent } from './embed-edgeless-synced-doc-block'; +import { calcSyncedDocFullHeight } from './utils'; export class HeightInitializationExtension extends LifeCycleWatcher { static override key = 'embed-synced-doc-block-height-initialization'; @@ -41,26 +37,17 @@ export class HeightInitializationExtension extends LifeCycleWatcher { } const block = payload.view; - block.contentElement - .then(contentEl => { - if (!contentEl) return; - + block.updateComplete + .then(() => { + if (!block.contentElement) return; const resizeObserver = new ResizeObserver(() => { - const headerHeight = - block.headerWrapper?.getBoundingClientRect().height ?? 0; - const contentHeight = contentEl.getBoundingClientRect().height; - const { x, y, w } = block.model.elementBound; - const h = clamp( - (headerHeight + contentHeight) / block.gfx.viewport.zoom, - SYNCED_MIN_HEIGHT, - SYNCED_DEFAULT_MAX_HEIGHT - ); + const h = calcSyncedDocFullHeight(block); block.model.xywh$.value = `[${x},${y},${w},${h}]`; - resizeObserver.unobserve(contentEl); + resizeObserver.disconnect(); }); - resizeObserver.observe(contentEl); + resizeObserver.observe(block.contentElement); }) .catch(console.error); } diff --git a/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/utils.ts b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/utils.ts index 8b17bbd11..2e032deca 100644 --- a/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/utils.ts +++ b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/utils.ts @@ -5,8 +5,10 @@ import { ReloadIcon, } from '@blocksuite/affine-components/icons'; import { ColorScheme } from '@blocksuite/affine-model'; +import type { BlockComponent } from '@blocksuite/std'; import type { TemplateResult } from 'lit'; +import { EmbedEdgelessSyncedDocBlockComponent } from './embed-edgeless-synced-doc-block.js'; import { DarkSyncedDocDeletedBanner, DarkSyncedDocEmptyBanner, @@ -58,3 +60,22 @@ export function getSyncedDocIcons( }; } } + +/** + * This function will return the height of the synced doc block + */ +export function calcSyncedDocFullHeight(block: BlockComponent) { + if (!(block instanceof EmbedEdgelessSyncedDocBlockComponent)) { + return 0; + } + const headerHeight = block.headerWrapper?.getBoundingClientRect().height ?? 0; + // When the content is not found, we use a default height to display empty information + const contentHeight = + block.contentElement?.getBoundingClientRect().height ?? 200; + + const bottomPadding = 8; + + return ( + (headerHeight + contentHeight + bottomPadding) / block.gfx.viewport.zoom + ); +} diff --git a/blocksuite/affine/blocks/embed-doc/src/view.ts b/blocksuite/affine/blocks/embed-doc/src/view.ts index 4ae4e2293..98a187012 100644 --- a/blocksuite/affine/blocks/embed-doc/src/view.ts +++ b/blocksuite/affine/blocks/embed-doc/src/view.ts @@ -11,9 +11,9 @@ import { } from './embed-linked-doc-block'; import { EdgelessClipboardEmbedSyncedDocConfig, - EmbedSyncedDocInteraction, EmbedSyncedDocViewExtensions, } from './embed-synced-doc-block'; +import { EmbedSyncedDocInteraction } from './embed-synced-doc-block/configs/edgeless-interaction'; export class EmbedDocViewExtension extends ViewExtensionProvider { override name = 'affine-embed-doc-block'; diff --git a/blocksuite/affine/model/src/blocks/embed/synced-doc/synced-doc-schema.ts b/blocksuite/affine/model/src/blocks/embed/synced-doc/synced-doc-schema.ts index ad61998f9..e56386445 100644 --- a/blocksuite/affine/model/src/blocks/embed/synced-doc/synced-doc-schema.ts +++ b/blocksuite/affine/model/src/blocks/embed/synced-doc/synced-doc-schema.ts @@ -8,7 +8,8 @@ import { } from './synced-doc-model.js'; export const SYNCED_MIN_WIDTH = 370; -export const SYNCED_MIN_HEIGHT = 64; +export const SYNCED_MIN_HEIGHT = 48; +export const SYNCED_DEFAULT_WIDTH = 800; // the default max height of embed doc, user can adjust height by selected rect over this value export const SYNCED_DEFAULT_MAX_HEIGHT = 800; @@ -22,7 +23,7 @@ export const defaultEmbedSyncedDocBlockProps: EmbedSyncedDocBlockProps = { title: undefined, description: undefined, index: 'a0', - xywh: `[0,0,${SYNCED_MIN_WIDTH},100]`, + xywh: `[0,0,${SYNCED_DEFAULT_WIDTH},100]`, lockedBySelf: undefined, }; diff --git a/blocksuite/affine/shared/src/consts/index.ts b/blocksuite/affine/shared/src/consts/index.ts index 1b78dc015..84bd79308 100644 --- a/blocksuite/affine/shared/src/consts/index.ts +++ b/blocksuite/affine/shared/src/consts/index.ts @@ -7,6 +7,7 @@ import { EmbedLoomModel, EmbedSyncedDocModel, EmbedYoutubeModel, + SYNCED_DEFAULT_WIDTH, } from '@blocksuite/affine-model'; export const BLOCK_CHILDREN_CONTAINER_PADDING_LEFT = 24; @@ -29,7 +30,7 @@ export const EMBED_CARD_WIDTH: Record = { video: 752, figma: 752, html: 752, - syncedDoc: 800, + syncedDoc: SYNCED_DEFAULT_WIDTH, pdf: 537 + 24 + 2, citation: 752, }; diff --git a/blocksuite/affine/shared/src/services/drag-handle-config.ts b/blocksuite/affine/shared/src/services/drag-handle-config.ts index 07e7f3858..c26baabee 100644 --- a/blocksuite/affine/shared/src/services/drag-handle-config.ts +++ b/blocksuite/affine/shared/src/services/drag-handle-config.ts @@ -47,6 +47,7 @@ export class DNDAPIExtension extends Extension { ...options.props, ...(blockId ? { blockId } : {}), pageId: docId, + style: flavour === 'affine:embed-synced-doc' ? 'syncedDoc' : 'vertical', }; return { ...snapshot, diff --git a/blocksuite/affine/widgets/drag-handle/src/watchers/drag-event-watcher.ts b/blocksuite/affine/widgets/drag-handle/src/watchers/drag-event-watcher.ts index 48e09322f..74ac7bb15 100644 --- a/blocksuite/affine/widgets/drag-handle/src/watchers/drag-event-watcher.ts +++ b/blocksuite/affine/widgets/drag-handle/src/watchers/drag-event-watcher.ts @@ -1089,7 +1089,7 @@ export class DragEventWatcher { block.flavour === 'affine:bookmark' || block.flavour.startsWith('affine:embed-') ) { - const style = 'vertical' as EmbedCardStyle; + const style = (block.props.style ?? 'vertical') as EmbedCardStyle; block.props.style = style; blockBound.w = EMBED_CARD_WIDTH[style]; diff --git a/packages/frontend/core/src/blocksuite/view-extensions/edgeless-block-header/edgeless-embed-synced-doc-header.tsx b/packages/frontend/core/src/blocksuite/view-extensions/edgeless-block-header/edgeless-embed-synced-doc-header.tsx index 96a118a85..cdb9e7fe3 100644 --- a/packages/frontend/core/src/blocksuite/view-extensions/edgeless-block-header/edgeless-embed-synced-doc-header.tsx +++ b/packages/frontend/core/src/blocksuite/view-extensions/edgeless-block-header/edgeless-embed-synced-doc-header.tsx @@ -5,8 +5,6 @@ import { stopPropagation } from '@affine/core/utils'; import { useI18n } from '@affine/i18n'; import { EmbedSyncedDocBlockComponent } from '@blocksuite/affine/blocks/embed-doc'; import { isPeekable, peek } from '@blocksuite/affine/components/peek'; -import { DisposableGroup } from '@blocksuite/affine/global/disposable'; -import { Bound } from '@blocksuite/affine/global/gfx'; import type { EmbedSyncedDocModel } from '@blocksuite/affine-model'; import { ArrowDownSmallIcon, @@ -30,28 +28,10 @@ const ToggleButton = ({ model }: { model: EmbedSyncedDocModel }) => { const [isFolded, setIsFolded] = useState(model.isFolded); const t = useI18n(); - useEffect(() => { - const disposables = new DisposableGroup(); - disposables.add( - model.props.preFoldHeight$.subscribe(value => setIsFolded(!!value)) - ); - // the height may be changed by dragging selected rect - disposables.add( - model.xywh$.subscribe(value => { - const bound = Bound.deserialize(value); - const preFoldHeight = model.props.preFoldHeight$.peek(); - if ( - bound.h !== styles.headerHeight && - preFoldHeight !== undefined && - bound.h !== preFoldHeight - ) { - model.props.preFoldHeight$.value = 0; - } - }) - ); - - return () => disposables.dispose(); - }, [model.props.preFoldHeight$, model.xywh$]); + useEffect( + () => model.props.preFoldHeight$.subscribe(value => setIsFolded(!!value)), + [model.props.preFoldHeight$] + ); const toggle = useCallback(() => { model.store.captureSync(); diff --git a/tests/affine-local/e2e/blocksuite/embed/synced.spec.ts b/tests/affine-local/e2e/blocksuite/embed/synced.spec.ts index ec4305261..2efa29727 100644 --- a/tests/affine-local/e2e/blocksuite/embed/synced.spec.ts +++ b/tests/affine-local/e2e/blocksuite/embed/synced.spec.ts @@ -4,7 +4,9 @@ import { clickView, createEdgelessNoteBlock, fitViewportToContent, + getSelectedXYWH, locateEditorContainer, + resizeElementByHandle, } from '@affine-test/kit/utils/editor'; import { pressEnter } from '@affine-test/kit/utils/keyboard'; import { openHomePage } from '@affine-test/kit/utils/load-page'; @@ -72,6 +74,7 @@ test.describe('edgeless', () => { .getByTestId('cmdk-quick-search') .getByText(/^Synced Block Test$/) .click(); + await fitViewportToContent(page); }); @@ -100,4 +103,61 @@ test.describe('edgeless', () => { await expect(headerTitle).toHaveText(title); }); }); + + test.describe('size adjustment of embed synced doc', () => { + test('should fold embed synced doc when adjust height to smallest', async ({ + page, + }) => { + const [, , , h] = await getSelectedXYWH(page); + await resizeElementByHandle(page, [0, -(h - 10)], 'bottom-right'); + + const embedBlock = page.locator('affine-embed-edgeless-synced-doc-block'); + const foldButton = embedBlock.getByTestId( + 'edgeless-embed-synced-doc-fold-button' + ); + const content = embedBlock.locator('editor-host'); + + await expect(foldButton).toHaveAttribute('data-folded', 'true'); + await expect(content).toBeHidden(); + + await foldButton.click(); + await expect(foldButton).toHaveAttribute('data-folded', 'false'); + await expect(content).toBeVisible(); + + await embedBlock.click(); + const [, , , h2] = await getSelectedXYWH(page); + expect( + h2, + 'should recover height when unfold embed synced doc which was resized to smallest height directly' + ).toEqual(h); + }); + + test('should be able to adjust height the folded embed synced doc', async ({ + page, + }) => { + const embedBlock = page.locator('affine-embed-edgeless-synced-doc-block'); + + const content = embedBlock.locator('editor-host'); + const foldButton = embedBlock.getByTestId( + 'edgeless-embed-synced-doc-fold-button' + ); + await foldButton.click(); + + await resizeElementByHandle(page, [50, 0], 'bottom-right'); + await expect(content).toBeHidden(); + await expect(foldButton).toHaveAttribute('data-folded', 'true'); + + await resizeElementByHandle(page, [-50, 0], 'bottom-right'); + await expect(content).toBeHidden(); + await expect(foldButton).toHaveAttribute('data-folded', 'true'); + + await resizeElementByHandle(page, [0, 50], 'bottom-right'); + await expect( + content, + 'should unfold the embed synced doc when adjust height to greater' + ).toBeVisible(); + await expect(foldButton).toHaveAttribute('data-folded', 'false'); + await expect(content).toBeVisible(); + }); + }); });