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();