From 8ece812017dc3ea9d781641001eb3eb693800b8b Mon Sep 17 00:00:00 2001 From: doouding Date: Fri, 14 Feb 2025 16:02:03 +0000 Subject: [PATCH] feat: dnd image preview && edgeless dnd preview issue (#10177) ### Changed - Add new image block to render dnd preview - Fixed the bug that dragging uploaded image does not have width and height - Fixed the bug that drag image block from page to edgeless does not have width and height - Better edgeless dnd preview --- blocksuite/affine/block-image/src/effects.ts | 10 ++ .../block-image/src/image-edgeless-block.ts | 2 +- .../block-image/src/preview-image/edgeless.ts | 25 ++++ .../block-image/src/preview-image/page.ts | 58 ++++++++++ blocksuite/affine/block-image/src/utils.ts | 46 ++++++-- .../src/helpers/preview-helper.ts | 107 +++++++++++++----- .../src/watchers/drag-event-watcher.ts | 30 +++-- .../src/watchers/edgeless-watcher.ts | 2 +- .../edgeless/edgeless-root-preview-block.ts | 10 +- .../blocks/src/root-block/edgeless/index.ts | 1 + .../block-std/src/gfx/viewport-element.ts | 7 +- 11 files changed, 245 insertions(+), 53 deletions(-) create mode 100644 blocksuite/affine/block-image/src/preview-image/edgeless.ts create mode 100644 blocksuite/affine/block-image/src/preview-image/page.ts diff --git a/blocksuite/affine/block-image/src/effects.ts b/blocksuite/affine/block-image/src/effects.ts index 07776fd00..0ba3bbd9b 100644 --- a/blocksuite/affine/block-image/src/effects.ts +++ b/blocksuite/affine/block-image/src/effects.ts @@ -2,10 +2,20 @@ import { ImageBlockFallbackCard } from './components/image-block-fallback.js'; import { ImageBlockPageComponent } from './components/page-image-block.js'; import { ImageBlockComponent } from './image-block.js'; import { ImageEdgelessBlockComponent } from './image-edgeless-block.js'; +import { ImageEdgelessPlaceholderBlockComponent } from './preview-image/edgeless.js'; +import { ImagePlaceholderBlockComponent } from './preview-image/page.js'; export function effects() { customElements.define('affine-image', ImageBlockComponent); customElements.define('affine-edgeless-image', ImageEdgelessBlockComponent); customElements.define('affine-page-image', ImageBlockPageComponent); customElements.define('affine-image-fallback-card', ImageBlockFallbackCard); + customElements.define( + 'affine-placeholder-preview-image', + ImagePlaceholderBlockComponent + ); + customElements.define( + 'affine-edgeless-placeholder-preview-image', + ImageEdgelessPlaceholderBlockComponent + ); } diff --git a/blocksuite/affine/block-image/src/image-edgeless-block.ts b/blocksuite/affine/block-image/src/image-edgeless-block.ts index 5ede90d61..9cd53082d 100644 --- a/blocksuite/affine/block-image/src/image-edgeless-block.ts +++ b/blocksuite/affine/block-image/src/image-edgeless-block.ts @@ -43,7 +43,7 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent { const { width, height } = this.model; - if (!width || !height) { + if ((!width || !height) && this.blob) { return resetImageSize(this); } diff --git a/blocksuite/affine/block-image/src/preview-image/edgeless.ts b/blocksuite/affine/block-image/src/preview-image/edgeless.ts new file mode 100644 index 000000000..56b2c55f4 --- /dev/null +++ b/blocksuite/affine/block-image/src/preview-image/edgeless.ts @@ -0,0 +1,25 @@ +import { toGfxBlockComponent } from '@blocksuite/block-std'; +import { css } from 'lit'; + +import { ImagePlaceholderBlockComponent } from './page.js'; + +export class ImageEdgelessPlaceholderBlockComponent extends toGfxBlockComponent( + ImagePlaceholderBlockComponent +) { + static override styles = css` + affine-edgeless-placeholder-preview-image + .affine-placeholder-preview-container { + border: 1px solid var(--affine-background-tertiary-color); + } + `; + + override renderGfxBlock(): unknown { + return super.renderGfxBlock(); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-edgeless-placeholder-preview-image': ImageEdgelessPlaceholderBlockComponent; + } +} diff --git a/blocksuite/affine/block-image/src/preview-image/page.ts b/blocksuite/affine/block-image/src/preview-image/page.ts new file mode 100644 index 000000000..8b20173da --- /dev/null +++ b/blocksuite/affine/block-image/src/preview-image/page.ts @@ -0,0 +1,58 @@ +import type { ImageBlockModel } from '@blocksuite/affine-model'; +import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import { BlockComponent } from '@blocksuite/block-std'; +import { ImageIcon } from '@blocksuite/icons/lit'; +import { css, html } from 'lit'; + +export class ImagePlaceholderBlockComponent extends BlockComponent { + static override styles = css` + .affine-placeholder-preview-container { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 4px; + box-sizing: border-box; + + border-radius: 8px; + background-color: ${unsafeCSSVarV2( + 'layer/background/overlayPanel', + '#FBFBFC' + )}; + } + + .placeholder-preview-content { + padding: 4px 16px; + display: flex; + gap: 8px; + } + + .placeholder-preview-content > svg { + color: ${unsafeCSSVarV2('icon/primary', '#77757D')}; + } + + .placeholder-preview-content > .text { + color: var(--affine-text-primary-color); + color: ${unsafeCSSVarV2('text/primary', '#121212')}; + font-size: 14px; + line-height: 24px; + } + `; + + override renderBlock() { + return html`
+
+ ${ImageIcon({ width: '24px', height: '24px' })} + Image Block +
+
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-placeholder-preview-image': ImagePlaceholderBlockComponent; + } +} diff --git a/blocksuite/affine/block-image/src/utils.ts b/blocksuite/affine/block-image/src/utils.ts index 7b08ad2bc..eb5d43027 100644 --- a/blocksuite/affine/block-image/src/utils.ts +++ b/blocksuite/affine/block-image/src/utils.ts @@ -68,15 +68,30 @@ export async function uploadBlobForImage( setImageUploaded(blockId); const imageModel = doc.getBlockById(blockId) as ImageBlockModel | null; - - doc.withoutTransact(() => { - if (!imageModel) { - return; - } - doc.updateBlock(imageModel, { + if (sourceId && imageModel) { + const props: Partial = { sourceId, - } satisfies Partial); - }); + // Assign a default size to make sure the image can be displayed correctly. + width: 100, + height: 100, + }; + + const blob = await doc.blobSync.get(sourceId); + if (blob) { + try { + const size = await readImageSize(blob); + props.width = size.width; + props.height = size.height; + } catch { + // Ignore the error + console.warn('Failed to read image size'); + } + } + + doc.withoutTransact(() => { + doc.updateBlock(imageModel, props); + }); + } } } @@ -198,10 +213,19 @@ export async function resetImageSize( const file = new File([blob], 'image.png', { type: blob.type }); const size = await readImageSize(file); - block.doc.updateBlock(model, { + const bound = model.elementBound; + const props: Partial = { width: size.width, height: size.height, - }); + }; + + if (!bound.w || !bound.h) { + bound.w = size.width; + bound.h = size.height; + props.xywh = bound.serialize(); + } + + block.doc.updateBlock(model, props); } function convertToPng(blob: Blob): Promise { @@ -401,7 +425,7 @@ export async function turnImageIntoCardView( transformModel(model, 'affine:attachment', attachmentProp); } -export function readImageSize(file: File) { +export function readImageSize(file: File | Blob) { return new Promise<{ width: number; height: number }>(resolve => { const size = { width: 0, height: 0 }; const img = new Image(); diff --git a/blocksuite/affine/widget-drag-handle/src/helpers/preview-helper.ts b/blocksuite/affine/widget-drag-handle/src/helpers/preview-helper.ts index 5c776f815..3bd0ff70b 100644 --- a/blocksuite/affine/widget-drag-handle/src/helpers/preview-helper.ts +++ b/blocksuite/affine/widget-drag-handle/src/helpers/preview-helper.ts @@ -1,24 +1,37 @@ +import { SurfaceBlockModel } from '@blocksuite/affine-block-surface'; +import { RootBlockModel } from '@blocksuite/affine-model'; import { DocModeExtension, DocModeProvider, EditorSettingExtension, EditorSettingProvider, } from '@blocksuite/affine-shared/services'; -import { SpecProvider } from '@blocksuite/affine-shared/utils'; -import { BlockStdScope, LifeCycleWatcher } from '@blocksuite/block-std'; -import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; +import { matchModels, SpecProvider } from '@blocksuite/affine-shared/utils'; import { - type BlockViewType, - type Query, - type SliceSnapshot, + type BlockComponent, + BlockStdScope, + BlockViewIdentifier, + LifeCycleWatcher, +} from '@blocksuite/block-std'; +import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; +import type { + BlockModel, + BlockViewType, + ExtensionType, + Query, + SliceSnapshot, } from '@blocksuite/store'; import { signal } from '@preact/signals-core'; +import { literal } from 'lit/static-html.js'; import type { AffineDragHandleWidget } from '../drag-handle.js'; import { getSnapshotRect } from '../utils.js'; export class PreviewHelper { - private readonly _calculateQuery = (selectedIds: string[]): Query => { + private readonly _calculateQuery = ( + selectedIds: string[], + mode: 'page' | 'edgeless' + ): Query => { const ids: Array<{ id: string; viewType: BlockViewType }> = selectedIds.map( id => ({ id, @@ -39,11 +52,28 @@ export class PreviewHelper { // The children of the selected blocks should be rendered as Display const addChildren = (id: string) => { - const children = this.widget.doc.getBlock(id)?.model.children ?? []; - children.forEach(child => { - ids.push({ viewType: 'display', id: child.id }); - addChildren(child.id); - }); + const model = this.widget.doc.getBlock(id)?.model; + if (!model) { + return; + } + + const children = model.children ?? []; + if ( + mode === 'edgeless' && + matchModels(model, [RootBlockModel, SurfaceBlockModel]) + ) { + children.forEach(child => { + if (selectedIds.includes(child.id)) { + ids.push({ viewType: 'display', id: child.id }); + addChildren(child.id); + } + }); + } else { + children.forEach(child => { + ids.push({ viewType: 'display', id: child.id }); + addChildren(child.id); + }); + } }; selectedIds.forEach(addChildren); @@ -61,23 +91,42 @@ export class PreviewHelper { const widget = this.widget; const std = widget.std; const sourceGfx = std.get(GfxControllerIdentifier); + const isEdgeless = mode === 'edgeless'; + blockIds = blockIds.slice(); + + if (isEdgeless) { + blockIds.push(sourceGfx.surface!.id, std.store.root!.id); + } + const docModeService = std.get(DocModeProvider); const editorSetting = std.get(EditorSettingProvider).peek(); - const query = this._calculateQuery(blockIds as string[]); + const query = this._calculateQuery(blockIds as string[], mode); const store = widget.doc.doc.getStore({ query }); const previewSpec = SpecProvider.getInstance().getSpec( - mode === 'edgeless' ? 'edgeless:preview' : 'page:preview' + isEdgeless ? 'edgeless:preview' : 'page:preview' ); const settingSignal = signal({ ...editorSetting }); const extensions = [ DocModeExtension(docModeService), EditorSettingExtension(settingSignal), + { + setup(di) { + di.override(BlockViewIdentifier('affine:image'), () => { + return (model: BlockModel) => { + const parent = model.doc.getParent(model.id); + + if (parent?.flavour === 'affine:surface') { + return literal`affine-edgeless-placeholder-preview-image`; + } + + return literal`affine-placeholder-preview-image`; + }; + }); + }, + } as ExtensionType, ]; - if (mode === 'edgeless') { - if (!blockIds.includes(sourceGfx.surface!.id)) { - blockIds.push(sourceGfx.surface!.id); - } + if (isEdgeless) { class PreviewViewportInitializer extends LifeCycleWatcher { static override key = 'preview-viewport-initializer'; @@ -91,9 +140,11 @@ export class PreviewHelper { if (payload.view.model.flavour === 'affine:page') { const gfx = this.std.get(GfxControllerIdentifier); - queueMicrotask(() => { - gfx.viewport.setViewportByBound(rect); - }); + ( + payload.view as BlockComponent & { overrideBackground: string } + ).overrideBackground = 'transparent'; + + gfx.viewport.setViewportByBound(rect); } }); } @@ -116,11 +167,9 @@ export class PreviewHelper { let width: number = 500; let height; + let scale = 1; - if (mode === 'page') { - const noteBlock = this.widget.host.querySelector('affine-note'); - width = noteBlock?.offsetWidth ?? noteBlock?.clientWidth ?? 500; - } else { + if (isEdgeless) { const rect = getSnapshotRect(snapshot); if (rect) { width = rect.w; @@ -128,9 +177,14 @@ export class PreviewHelper { } else { height = 500; } + scale = sourceGfx.viewport.zoom; + } else { + const noteBlock = this.widget.host.querySelector('affine-note'); + width = noteBlock?.offsetWidth ?? noteBlock?.clientWidth ?? 500; } return { + scale, previewStd, width, height, @@ -144,13 +198,14 @@ export class PreviewHelper { mode: 'page' | 'edgeless'; }): void => { const { blockIds, snapshot, container, mode } = options; - const { previewStd, width, height } = this.getPreviewStd( + const { previewStd, width, height, scale } = this.getPreviewStd( blockIds, snapshot, mode ); const previewTemplate = previewStd.render(); + container.style.transform = `scale(${scale})`; container.style.width = `${width}px`; if (height) { container.style.height = `${height}px`; diff --git a/blocksuite/affine/widget-drag-handle/src/watchers/drag-event-watcher.ts b/blocksuite/affine/widget-drag-handle/src/watchers/drag-event-watcher.ts index d85be2793..693f51edf 100644 --- a/blocksuite/affine/widget-drag-handle/src/watchers/drag-event-watcher.ts +++ b/blocksuite/affine/widget-drag-handle/src/watchers/drag-event-watcher.ts @@ -349,11 +349,12 @@ export class DragEventWatcher { return selectedElements; }; - const toSnapshotRequiredBlocks = (models: string[]) => { + const toSnapshotRequiredBlocks = (selectedModels: string[]) => { let surfaceAdded = false; const blocks: BlockModel[] = []; + const blocksUnderSurface: BlockModel[] = []; - models.forEach(id => { + selectedModels.forEach(id => { const model = this.gfx.getElementById(id); if (!model) { @@ -368,16 +369,19 @@ export class DragEventWatcher { const parentModel = this.std.store.getParent(model); if (matchModels(parentModel, [SurfaceBlockModel])) { - if (surfaceAdded) return; - surfaceAdded = true; - blocks.push(this.gfx.surface!); + blocksUnderSurface.push(model); } else { blocks.push(model); } } }); - return blocks; + if (surfaceAdded) { + // surface children are included, so no need to add the blocksUnderSurface + return blocks; + } else { + return blocks.concat(blocksUnderSurface); + } }; const selectedElements = getElementsInContainer( @@ -953,13 +957,19 @@ export class DragEventWatcher { block.flavour === 'affine:attachment' || block.flavour.startsWith('affine:embed-') ) { - const style = (block.props.style ?? 'vertical') as EmbedCardStyle; + const style = 'vertical' as EmbedCardStyle; block.props.style = style; blockBound.w = EMBED_CARD_WIDTH[style]; blockBound.h = EMBED_CARD_HEIGHT[style]; } + if (block.flavour === 'affine:image') { + assertType<{ width: number; height: number }>(block.props); + blockBound.w = blockBound.w || block.props.width || 100; + blockBound.h = blockBound.h || block.props.height || 100; + } + if (ignoreOriginalPos) { blockBound.x = modelX; blockBound.y = modelY; @@ -1013,8 +1023,10 @@ export class DragEventWatcher { .then(slices => { slices?.content.forEach((block, idx) => { if ( - block.flavour === 'affine:attachment' || - block.flavour.startsWith('affine:embed-') + block.id === content[idx].id && + (block.flavour === 'affine:image' || + block.flavour === 'affine:attachment' || + block.flavour.startsWith('affine:embed-')) ) { store.updateBlock(block as BlockModel, { xywh: content[idx].props.xywh, diff --git a/blocksuite/affine/widget-drag-handle/src/watchers/edgeless-watcher.ts b/blocksuite/affine/widget-drag-handle/src/watchers/edgeless-watcher.ts index 20d806a4f..fdc0d8333 100644 --- a/blocksuite/affine/widget-drag-handle/src/watchers/edgeless-watcher.ts +++ b/blocksuite/affine/widget-drag-handle/src/watchers/edgeless-watcher.ts @@ -27,7 +27,7 @@ export class EdgelessWatcher { private readonly _handleEdgelessToolUpdated = ( newTool: GfxToolsFullOptionValue ) => { - // @ts-expect-error FIXME: resolve after gfx tool refactor + // @ts-expect-error GfxToolsFullOptionValue is extended in other packages if (newTool.type === 'default') { this.updateAnchorElement(); } else { diff --git a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-preview-block.ts b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-preview-block.ts index 6c2db0772..392289e86 100644 --- a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-preview-block.ts +++ b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-preview-block.ts @@ -19,6 +19,7 @@ import type { GfxViewportElement } from '@blocksuite/block-std/gfx'; import { assertExists } from '@blocksuite/global/utils'; import { css, html } from 'lit'; import { query, state } from 'lit/decorators.js'; +import { styleMap } from 'lit/directives/style-map.js'; import type { EdgelessRootBlockWidgetName } from '../types.js'; import type { EdgelessRootService } from './edgeless-root-service.js'; @@ -229,8 +230,12 @@ export class EdgelessRootPreviewBlockComponent } override renderBlock() { + const background = styleMap({ + background: this.overrideBackground, + }); + return html` -
+
{ + private readonly _hideOutsideBlock = () => { if (this.getModelsInViewport && this.host) { const host = this.host; const modelsInViewport = this.getModelsInViewport(); @@ -73,7 +73,7 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) { this._lastVisibleModels = modelsInViewport; } - }, this); + }; private _lastVisibleModels?: Set; @@ -95,14 +95,13 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) { const viewportUpdateCallback = () => { this._refreshViewport(); - this._hideOutsideBlock(); }; if (!this.enableChildrenSchedule) { delete this.scheduleUpdateChildren; } - viewportUpdateCallback(); + this._hideOutsideBlock(); this.disposables.add( this.viewport.viewportUpdated.on(() => viewportUpdateCallback()) );