From fcc2ec9d667de8b329eb575a9eb6468c0d753e65 Mon Sep 17 00:00:00 2001 From: doouding Date: Sat, 29 Mar 2025 04:13:28 +0000 Subject: [PATCH] feat: use block card to render edgeless dnd preview (#11261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related issue [BS-2610](https://linear.app/affine-design/issue/BS-2610/多选的拖拽:如果保护不支持预览的-block,则直接显示-icon-block-名称的方式做-fallback). Use simpler way to render edgeless dnd preview. ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/57vl0IUaypligEBYUOO0/845d43ac-27e0-45fe-8289-2e6467c59108.png) --- .../components/edgeless-preview/preview.ts | 133 +++++++++++++ .../widgets/widget-drag-handle/src/effects.ts | 8 + .../src/helpers/preview-helper.ts | 186 ++++++++---------- .../src/watchers/drag-event-watcher.ts | 4 +- 4 files changed, 225 insertions(+), 106 deletions(-) create mode 100644 blocksuite/affine/widgets/widget-drag-handle/src/components/edgeless-preview/preview.ts diff --git a/blocksuite/affine/widgets/widget-drag-handle/src/components/edgeless-preview/preview.ts b/blocksuite/affine/widgets/widget-drag-handle/src/components/edgeless-preview/preview.ts new file mode 100644 index 000000000..65ac5f7c0 --- /dev/null +++ b/blocksuite/affine/widgets/widget-drag-handle/src/components/edgeless-preview/preview.ts @@ -0,0 +1,133 @@ +import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import { + EmbedIcon, + FrameIcon, + ImageIcon, + PageIcon, + ShapeIcon, +} from '@blocksuite/icons/lit'; +import { css, html, LitElement } from 'lit'; +import { property } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { styleMap } from 'lit/directives/style-map.js'; + +const BLOCK_PREVIEW_ICON_MAP: Record< + string, + { + icon: typeof ShapeIcon; + name: string; + } +> = { + shape: { + icon: ShapeIcon, + name: 'Edgeless shape', + }, + 'affine:image': { + icon: ImageIcon, + name: 'Image block', + }, + 'affine:note': { + icon: PageIcon, + name: 'Note block', + }, + 'affine:frame': { + icon: FrameIcon, + name: 'Frame block', + }, + 'affine:embed-': { + icon: EmbedIcon, + name: 'Embed block', + }, +}; + +declare global { + interface HTMLElementTagNameMap { + 'edgeless-dnd-preview-element': EdgelessDndPreviewElement; + } +} + +export const EDGELESS_DND_PREVIEW_ELEMENT = 'edgeless-dnd-preview-element'; + +export class EdgelessDndPreviewElement extends LitElement { + static override styles = css` + .edgeless-dnd-preview-container { + position: relative; + padding: 12px; + width: 264px; + height: 80px; + } + + .edgeless-dnd-preview-block { + display: flex; + position: absolute; + + width: 234px; + + align-items: flex-start; + box-sizing: border-box; + + border-radius: 8px; + background-color: ${unsafeCSSVarV2( + 'layer/background/overlayPanel', + '#FBFBFC' + )}; + + padding: 8px 20px; + gap: 8px; + + transform-origin: center; + + font-family: var(--affine-font-family); + box-shadow: 0px 0px 0px 0.5px #e3e3e4 inset; + } + + .edgeless-dnd-preview-block > svg { + color: ${unsafeCSSVarV2('icon/primary', '#77757D')}; + } + + .edgeless-dnd-preview-block > .text { + color: ${unsafeCSSVarV2('text/primary', '#121212')}; + font-size: 14px; + line-height: 24px; + } + `; + + @property({ type: Array }) + accessor elementTypes: { + type: string; + }[] = []; + + private _getPreviewIcon(type: string) { + if (BLOCK_PREVIEW_ICON_MAP[type]) { + return BLOCK_PREVIEW_ICON_MAP[type]; + } + + if (type.startsWith('affine:embed-')) { + return BLOCK_PREVIEW_ICON_MAP['affine:embed-']; + } + + return { + icon: ShapeIcon, + name: 'Edgeless content', + }; + } + + override render() { + const blocks = repeat(this.elementTypes.slice(0, 3), ({ type }, index) => { + const { icon, name } = this._getPreviewIcon(type); + + return html`
+ ${icon({ width: '24px', height: '24px' })} + ${name} +
`; + }); + + return html`
${blocks}
`; + } +} diff --git a/blocksuite/affine/widgets/widget-drag-handle/src/effects.ts b/blocksuite/affine/widgets/widget-drag-handle/src/effects.ts index c6f33e644..70330667b 100644 --- a/blocksuite/affine/widgets/widget-drag-handle/src/effects.ts +++ b/blocksuite/affine/widgets/widget-drag-handle/src/effects.ts @@ -1,6 +1,14 @@ +import { + EDGELESS_DND_PREVIEW_ELEMENT, + EdgelessDndPreviewElement, +} from './components/edgeless-preview/preview'; import { AFFINE_DRAG_HANDLE_WIDGET } from './consts'; import { AffineDragHandleWidget } from './drag-handle'; export function effects() { customElements.define(AFFINE_DRAG_HANDLE_WIDGET, AffineDragHandleWidget); + customElements.define( + EDGELESS_DND_PREVIEW_ELEMENT, + EdgelessDndPreviewElement + ); } diff --git a/blocksuite/affine/widgets/widget-drag-handle/src/helpers/preview-helper.ts b/blocksuite/affine/widgets/widget-drag-handle/src/helpers/preview-helper.ts index 430f8ff85..50aceaeef 100644 --- a/blocksuite/affine/widgets/widget-drag-handle/src/helpers/preview-helper.ts +++ b/blocksuite/affine/widgets/widget-drag-handle/src/helpers/preview-helper.ts @@ -1,19 +1,11 @@ -import { SurfaceBlockModel } from '@blocksuite/affine-block-surface'; -import { RootBlockModel } from '@blocksuite/affine-model'; import { DocModeExtension, DocModeProvider, EditorSettingExtension, EditorSettingProvider, } from '@blocksuite/affine-shared/services'; -import { matchModels, SpecProvider } from '@blocksuite/affine-shared/utils'; -import { - type BlockComponent, - BlockStdScope, - BlockViewIdentifier, - LifeCycleWatcher, -} from '@blocksuite/std'; -import { GfxControllerIdentifier } from '@blocksuite/std/gfx'; +import { SpecProvider } from '@blocksuite/affine-shared/utils'; +import { BlockStdScope, BlockViewIdentifier } from '@blocksuite/std'; import type { BlockModel, BlockViewType, @@ -24,14 +16,11 @@ import type { import { signal } from '@preact/signals-core'; import { literal } from 'lit/static-html.js'; +import { EdgelessDndPreviewElement } from '../components/edgeless-preview/preview.js'; import type { AffineDragHandleWidget } from '../drag-handle.js'; -import { getSnapshotRect } from '../utils.js'; export class PreviewHelper { - private readonly _calculateQuery = ( - selectedIds: string[], - mode: 'block' | 'gfx' - ): Query => { + private readonly _calculateQuery = (selectedIds: string[]): Query => { const ids: Array<{ id: string; viewType: BlockViewType }> = selectedIds.map( id => ({ id, @@ -58,22 +47,10 @@ export class PreviewHelper { } const children = model.children ?? []; - if ( - mode === 'gfx' && - 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); - }); - } + children.forEach(child => { + ids.push({ viewType: 'display', id: child.id }); + addChildren(child.id); + }); }; selectedIds.forEach(addChildren); @@ -83,28 +60,16 @@ export class PreviewHelper { }; }; - getPreviewStd = ( - blockIds: string[], - snapshot: SliceSnapshot, - mode: 'block' | 'gfx' - ) => { + getPreviewStd = (blockIds: string[]) => { const widget = this.widget; const std = widget.std; - const sourceGfx = std.get(GfxControllerIdentifier); - const isEdgeless = mode === 'gfx'; 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[], mode); + const query = this._calculateQuery(blockIds as string[]); const store = widget.doc.doc.getStore({ query }); - const previewSpec = SpecProvider._.getSpec( - isEdgeless ? 'preview:edgeless' : 'preview:page' - ); + const previewSpec = SpecProvider._.getSpec('preview:page'); const settingSignal = signal({ ...editorSetting }); const extensions = [ DocModeExtension(docModeService), @@ -134,35 +99,6 @@ export class PreviewHelper { } as ExtensionType, ]; - if (isEdgeless) { - class PreviewViewportInitializer extends LifeCycleWatcher { - static override key = 'preview-viewport-initializer'; - - override mounted(): void { - const rect = getSnapshotRect(snapshot); - if (!rect) { - return; - } - - this.std.view.viewUpdated.subscribe(payload => { - if (payload.type !== 'block') return; - - if (payload.view.model.flavour === 'affine:page') { - const gfx = this.std.get(GfxControllerIdentifier); - - ( - payload.view as BlockComponent & { overrideBackground: string } - ).overrideBackground = 'transparent'; - - gfx.viewport.setViewportByBound(rect); - } - }); - } - } - - extensions.push(PreviewViewportInitializer); - } - previewSpec.extend(extensions); settingSignal.value = { @@ -177,50 +113,92 @@ export class PreviewHelper { let width: number = 500; let height; - let scale = 1; - if (isEdgeless) { - const rect = getSnapshotRect(snapshot); - if (rect) { - width = rect.w; - height = rect.h; - } else { - height = 500; - } - scale = sourceGfx.viewport.zoom; - } else { - const noteBlock = this.widget.host.querySelector('affine-note'); - width = noteBlock?.offsetWidth ?? noteBlock?.clientWidth ?? 500; - } + const noteBlock = this.widget.host.querySelector('affine-note'); + width = noteBlock?.offsetWidth ?? noteBlock?.clientWidth ?? 500; return { - scale, previewStd, width, height, }; }; + private _extractBlockTypes(snapshot: SliceSnapshot) { + const blockTypes: { + type: string; + }[] = []; + + snapshot.content.forEach(block => { + if (block.flavour === 'affine:surface') { + Object.values( + block.props.elements as Record + ).forEach(elem => { + blockTypes.push({ + type: elem.type, + }); + }); + } else { + blockTypes.push({ + type: block.flavour, + }); + } + }); + + return blockTypes; + } + + getPreviewElement = (options: { + blockIds: string[]; + snapshot: SliceSnapshot; + mode: 'block' | 'gfx'; + }) => { + const { blockIds, snapshot, mode } = options; + + if (mode === 'block') { + const { previewStd, width, height } = this.getPreviewStd(blockIds); + const previewTemplate = previewStd.render(); + + return { + width, + height, + element: previewTemplate, + }; + } else { + const blockTypes = this._extractBlockTypes(snapshot); + + const edgelessPreview = new EdgelessDndPreviewElement(); + edgelessPreview.elementTypes = blockTypes; + + return { + left: 12, + top: 12, + element: edgelessPreview, + }; + } + }; + renderDragPreview = (options: { blockIds: string[]; snapshot: SliceSnapshot; container: HTMLElement; mode: 'block' | 'gfx'; - }): void => { - const { blockIds, snapshot, container, mode } = options; - const { previewStd, width, height, scale } = this.getPreviewStd( - blockIds, - snapshot, - mode - ); - const previewTemplate = previewStd.render(); + }): { x: number; y: number } => { + const { container } = options; + const { width, height, element, left, top } = + this.getPreviewElement(options); - container.style.transform = `scale(${scale})`; - container.style.width = `${width}px`; - if (height) { - container.style.height = `${height}px`; - } - container.append(previewTemplate); + container.style.position = 'absolute'; + container.style.left = left ? `${left}px` : ''; + container.style.top = top ? `${top}px` : ''; + container.style.width = width ? `${width}px` : ''; + container.style.height = height ? `${height}px` : ''; + container.append(element); + + return { + x: left ?? 0, + y: top ?? 0, + }; }; constructor(readonly widget: AffineDragHandleWidget) {} diff --git a/blocksuite/affine/widgets/widget-drag-handle/src/watchers/drag-event-watcher.ts b/blocksuite/affine/widgets/widget-drag-handle/src/watchers/drag-event-watcher.ts index 5e316bad4..5fbaf5f3c 100644 --- a/blocksuite/affine/widgets/widget-drag-handle/src/watchers/drag-event-watcher.ts +++ b/blocksuite/affine/widgets/widget-drag-handle/src/watchers/drag-event-watcher.ts @@ -1403,14 +1403,14 @@ export class DragEventWatcher { const { snapshot, fromMode } = source.data.bsEntity; - this.previewHelper.renderDragPreview({ + const offset = this.previewHelper.renderDragPreview({ blockIds: source.data?.bsEntity?.modelIds, snapshot, container, mode: fromMode ?? 'block', }); - setOffset({ x: 0, y: 0 }); + setOffset(offset); }, setDragData: () => { const { fromMode, snapshot } = this._getDraggedSnapshot();