From e0308c5815f5c98d5dad41b495ed9839de3e7f5b Mon Sep 17 00:00:00 2001 From: L-Sun Date: Wed, 30 Apr 2025 18:48:54 +0000 Subject: [PATCH] feat(editor): make height of edgeless embed doc to fit content (#12089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close [BS-3388](https://linear.app/affine-design/issue/BS-3388/embed-doc-拖入后的初始高度不要超过800,不要限制用户随后的调整空间) This PR impl a extension which initialize the height of added `affine-embed-edgeless-synced-doc-block` to fit its content ## Summary by CodeRabbit - **New Features** - Improved handling of embedded synced document block height to better fit content within edgeless mode. - **Tests** - Added an end-to-end test to verify correct height adjustment for embedded synced documents in edgeless mode. --- .../embed-edgeless-synced-doc-block.ts | 9 ++- .../embed-synced-doc-spec.ts | 2 + .../init-height-extension.ts | 78 +++++++++++++++++++ .../embed/synced-doc/synced-doc-schema.ts | 2 + .../e2e/embed-synced-doc/edgeless.spec.ts | 62 ++++++++++++++- 5 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/init-height-extension.ts 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 c85649ae57..30a163d449 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 @@ -13,8 +13,9 @@ import { ThemeProvider, } from '@blocksuite/affine-shared/services'; import { Bound } from '@blocksuite/global/gfx'; -import { BlockStdScope } from '@blocksuite/std'; +import { type BlockComponent, BlockStdScope } from '@blocksuite/std'; import { html, nothing } from 'lit'; +import { query, queryAsync } 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'; @@ -27,6 +28,12 @@ import { EmbedSyncedDocBlockComponent } from './embed-synced-doc-block'; export class EmbedEdgelessSyncedDocBlockComponent extends toEdgelessEmbedBlock( EmbedSyncedDocBlockComponent ) { + @query('.affine-embed-synced-doc-edgeless-header-wrapper') + accessor headerWrapper: HTMLDivElement | null = null; + + @queryAsync('affine-preview-root') + accessor contentElement!: Promise; + protected override _renderSyncedView = () => { const { syncedDoc, editorMode } = this; 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 ea714f2a35..2ccae6cbdb 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,6 +5,7 @@ import { literal } from 'lit/static-html.js'; import { EmbedSyncedDocBlockAdapterExtensions } from './adapters/extension'; import { createBuiltinToolbarConfigExtension } from './configs/toolbar'; +import { HeightInitializationExtension } from './init-height-extension'; const flavour = EmbedSyncedDocBlockSchema.model.flavour; @@ -27,4 +28,5 @@ export const EmbedSyncedDocViewExtensions: ExtensionType[] = [ : literal`affine-embed-synced-doc-block`; }), createBuiltinToolbarConfigExtension(flavour), + HeightInitializationExtension, ].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 new file mode 100644 index 0000000000..9ad5b9f1ed --- /dev/null +++ b/blocksuite/affine/blocks/embed-doc/src/embed-synced-doc-block/init-height-extension.ts @@ -0,0 +1,78 @@ +import { + EmbedSyncedDocBlockSchema, + SYNCED_DEFAULT_MAX_HEIGHT, + SYNCED_MIN_HEIGHT, +} 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'; + +export class HeightInitializationExtension extends LifeCycleWatcher { + static override key = 'embed-synced-doc-block-height-initialization'; + + override mounted() { + super.mounted(); + + this._disposables.add( + this.std.store.slots.blockUpdated.subscribe(payload => { + if ( + payload.type === 'add' && + payload.isLocal && + payload.flavour === EmbedSyncedDocBlockSchema.model.flavour && + payload.model.parent?.flavour === 'affine:surface' + ) { + this._initQueue.add(payload.id); + } + }) + ); + + this._disposables.add( + this.std.view.viewUpdated.subscribe(payload => { + if ( + payload.type === 'block' && + payload.method === 'add' && + this._initQueue.has(payload.id) + ) { + this._initQueue.delete(payload.id); + if (!(payload.view instanceof EmbedEdgelessSyncedDocBlockComponent)) { + return; + } + const block = payload.view; + + block.contentElement + .then(contentEl => { + if (!contentEl) 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 + ); + block.model.xywh$.value = `[${x},${y},${w},${h}]`; + + resizeObserver.unobserve(contentEl); + }); + resizeObserver.observe(contentEl); + }) + .catch(console.error); + } + }) + ); + } + + override unmounted(): void { + this._disposables.dispose(); + } + + private readonly _initQueue = new Set(); + + private readonly _disposables = new DisposableGroup(); +} 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 b7c66c40b3..ad61998f90 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 @@ -9,6 +9,8 @@ import { export const SYNCED_MIN_WIDTH = 370; export const SYNCED_MIN_HEIGHT = 64; +// the default max height of embed doc, user can adjust height by selected rect over this value +export const SYNCED_DEFAULT_MAX_HEIGHT = 800; export const defaultEmbedSyncedDocBlockProps: EmbedSyncedDocBlockProps = { pageId: '', diff --git a/tests/blocksuite/e2e/embed-synced-doc/edgeless.spec.ts b/tests/blocksuite/e2e/embed-synced-doc/edgeless.spec.ts index 4571adcc5f..577c223959 100644 --- a/tests/blocksuite/e2e/embed-synced-doc/edgeless.spec.ts +++ b/tests/blocksuite/e2e/embed-synced-doc/edgeless.spec.ts @@ -1,4 +1,5 @@ import type { AffineReference } from '@blocksuite/affine/inlines/reference'; +import type { EmbedSyncedDocBlockProps } from '@blocksuite/affine/model'; import { expect, type Page } from '@playwright/test'; import { clickView } from '../utils/actions/click.js'; @@ -12,7 +13,7 @@ import { isIntersected, switchEditorMode, } from '../utils/actions/edgeless'; -import { pressEscape } from '../utils/actions/keyboard.js'; +import { pressBackspace, pressEscape } from '../utils/actions/keyboard.js'; import { enterPlaygroundRoom, waitNextFrame } from '../utils/actions/misc'; import { test } from '../utils/playwright'; import { initEmbedSyncedDocState } from './utils'; @@ -75,6 +76,65 @@ test.describe('Embed synced doc in edgeless mode', () => { } ); + test('new edgeless embed synced doc should fit in height', async ({ + page, + }) => { + const [_, embedDocId] = await initEmbedSyncedDocState(page, [ + { title: 'Root Doc', content: 'hello root doc' }, + { title: 'Page 1', content: '1\n2\n3\n4\n5\n6\n7' }, + ]); + await switchEditorMode(page); + + const paragraphHeight = ( + await page + .locator('affine-embed-synced-doc-block affine-paragraph') + .boundingBox() + )?.height; + if (!paragraphHeight) { + test.fail(); + return; + } + + const createEmbedDocWithHeight = async (height: number) => { + await page.evaluate( + ({ embedDocId, height }) => { + const std = window.editor.std; + const surface = std.store.getModelsByFlavour('affine:surface')[0]; + std.store.addBlock( + 'affine:embed-synced-doc', + { + pageId: embedDocId, + xywh: `[0,100,370,${height}]`, + } satisfies Partial, + surface.id + ); + }, + { embedDocId, height } + ); + }; + + const embedSyncedBlockInNote = page.locator( + 'affine-embed-edgeless-synced-doc-block' + ); + + { + const initHeight = paragraphHeight - 50; + await createEmbedDocWithHeight(initHeight); + const embedSyncedBoxInNote = await embedSyncedBlockInNote.boundingBox(); + expect(embedSyncedBoxInNote?.height).toBeGreaterThan(initHeight); + } + + await embedSyncedBlockInNote.click(); + await pressBackspace(page); + + { + const initHeight = paragraphHeight + 50; + await createEmbedDocWithHeight(initHeight); + const embedSyncedBoxInNote = await embedSyncedBlockInNote.boundingBox(); + expect(embedSyncedBoxInNote?.height).toBeLessThan(initHeight); + } + }); + test.describe('edgeless element toolbar', () => { test.beforeEach(async ({ page }) => { await initEmbedSyncedDocState(page, [