mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-26 02:35:58 +08:00
feat: use block card to render edgeless dnd preview (#11261)
Related issue [BS-2610](https://linear.app/affine-design/issue/BS-2610/多选的拖拽:如果保护不支持预览的-block,则直接显示-icon-block-名称的方式做-fallback). Use simpler way to render edgeless dnd preview. 
This commit is contained in:
@@ -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`<div
|
||||||
|
class="edgeless-dnd-preview-block"
|
||||||
|
style=${styleMap({
|
||||||
|
transform: `rotate(${index * -2}deg)`,
|
||||||
|
zIndex: 3 - index,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${icon({ width: '24px', height: '24px' })}
|
||||||
|
<span class="text">${name}</span>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`<div class="edgeless-dnd-preview-container">${blocks}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
|
import {
|
||||||
|
EDGELESS_DND_PREVIEW_ELEMENT,
|
||||||
|
EdgelessDndPreviewElement,
|
||||||
|
} from './components/edgeless-preview/preview';
|
||||||
import { AFFINE_DRAG_HANDLE_WIDGET } from './consts';
|
import { AFFINE_DRAG_HANDLE_WIDGET } from './consts';
|
||||||
import { AffineDragHandleWidget } from './drag-handle';
|
import { AffineDragHandleWidget } from './drag-handle';
|
||||||
|
|
||||||
export function effects() {
|
export function effects() {
|
||||||
customElements.define(AFFINE_DRAG_HANDLE_WIDGET, AffineDragHandleWidget);
|
customElements.define(AFFINE_DRAG_HANDLE_WIDGET, AffineDragHandleWidget);
|
||||||
|
customElements.define(
|
||||||
|
EDGELESS_DND_PREVIEW_ELEMENT,
|
||||||
|
EdgelessDndPreviewElement
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
|
|
||||||
import { RootBlockModel } from '@blocksuite/affine-model';
|
|
||||||
import {
|
import {
|
||||||
DocModeExtension,
|
DocModeExtension,
|
||||||
DocModeProvider,
|
DocModeProvider,
|
||||||
EditorSettingExtension,
|
EditorSettingExtension,
|
||||||
EditorSettingProvider,
|
EditorSettingProvider,
|
||||||
} from '@blocksuite/affine-shared/services';
|
} from '@blocksuite/affine-shared/services';
|
||||||
import { matchModels, SpecProvider } from '@blocksuite/affine-shared/utils';
|
import { SpecProvider } from '@blocksuite/affine-shared/utils';
|
||||||
import {
|
import { BlockStdScope, BlockViewIdentifier } from '@blocksuite/std';
|
||||||
type BlockComponent,
|
|
||||||
BlockStdScope,
|
|
||||||
BlockViewIdentifier,
|
|
||||||
LifeCycleWatcher,
|
|
||||||
} from '@blocksuite/std';
|
|
||||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
|
||||||
import type {
|
import type {
|
||||||
BlockModel,
|
BlockModel,
|
||||||
BlockViewType,
|
BlockViewType,
|
||||||
@@ -24,14 +16,11 @@ import type {
|
|||||||
import { signal } from '@preact/signals-core';
|
import { signal } from '@preact/signals-core';
|
||||||
import { literal } from 'lit/static-html.js';
|
import { literal } from 'lit/static-html.js';
|
||||||
|
|
||||||
|
import { EdgelessDndPreviewElement } from '../components/edgeless-preview/preview.js';
|
||||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||||
import { getSnapshotRect } from '../utils.js';
|
|
||||||
|
|
||||||
export class PreviewHelper {
|
export class PreviewHelper {
|
||||||
private readonly _calculateQuery = (
|
private readonly _calculateQuery = (selectedIds: string[]): Query => {
|
||||||
selectedIds: string[],
|
|
||||||
mode: 'block' | 'gfx'
|
|
||||||
): Query => {
|
|
||||||
const ids: Array<{ id: string; viewType: BlockViewType }> = selectedIds.map(
|
const ids: Array<{ id: string; viewType: BlockViewType }> = selectedIds.map(
|
||||||
id => ({
|
id => ({
|
||||||
id,
|
id,
|
||||||
@@ -58,22 +47,10 @@ export class PreviewHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const children = model.children ?? [];
|
const children = model.children ?? [];
|
||||||
if (
|
children.forEach(child => {
|
||||||
mode === 'gfx' &&
|
ids.push({ viewType: 'display', id: child.id });
|
||||||
matchModels(model, [RootBlockModel, SurfaceBlockModel])
|
addChildren(child.id);
|
||||||
) {
|
});
|
||||||
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);
|
selectedIds.forEach(addChildren);
|
||||||
|
|
||||||
@@ -83,28 +60,16 @@ export class PreviewHelper {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
getPreviewStd = (
|
getPreviewStd = (blockIds: string[]) => {
|
||||||
blockIds: string[],
|
|
||||||
snapshot: SliceSnapshot,
|
|
||||||
mode: 'block' | 'gfx'
|
|
||||||
) => {
|
|
||||||
const widget = this.widget;
|
const widget = this.widget;
|
||||||
const std = widget.std;
|
const std = widget.std;
|
||||||
const sourceGfx = std.get(GfxControllerIdentifier);
|
|
||||||
const isEdgeless = mode === 'gfx';
|
|
||||||
blockIds = blockIds.slice();
|
blockIds = blockIds.slice();
|
||||||
|
|
||||||
if (isEdgeless) {
|
|
||||||
blockIds.push(sourceGfx.surface!.id, std.store.root!.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const docModeService = std.get(DocModeProvider);
|
const docModeService = std.get(DocModeProvider);
|
||||||
const editorSetting = std.get(EditorSettingProvider).peek();
|
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 store = widget.doc.doc.getStore({ query });
|
||||||
const previewSpec = SpecProvider._.getSpec(
|
const previewSpec = SpecProvider._.getSpec('preview:page');
|
||||||
isEdgeless ? 'preview:edgeless' : 'preview:page'
|
|
||||||
);
|
|
||||||
const settingSignal = signal({ ...editorSetting });
|
const settingSignal = signal({ ...editorSetting });
|
||||||
const extensions = [
|
const extensions = [
|
||||||
DocModeExtension(docModeService),
|
DocModeExtension(docModeService),
|
||||||
@@ -134,35 +99,6 @@ export class PreviewHelper {
|
|||||||
} as ExtensionType,
|
} 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);
|
previewSpec.extend(extensions);
|
||||||
|
|
||||||
settingSignal.value = {
|
settingSignal.value = {
|
||||||
@@ -177,50 +113,92 @@ export class PreviewHelper {
|
|||||||
|
|
||||||
let width: number = 500;
|
let width: number = 500;
|
||||||
let height;
|
let height;
|
||||||
let scale = 1;
|
|
||||||
|
|
||||||
if (isEdgeless) {
|
const noteBlock = this.widget.host.querySelector('affine-note');
|
||||||
const rect = getSnapshotRect(snapshot);
|
width = noteBlock?.offsetWidth ?? noteBlock?.clientWidth ?? 500;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scale,
|
|
||||||
previewStd,
|
previewStd,
|
||||||
width,
|
width,
|
||||||
height,
|
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<string, { id: string; type: string }>
|
||||||
|
).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: {
|
renderDragPreview = (options: {
|
||||||
blockIds: string[];
|
blockIds: string[];
|
||||||
snapshot: SliceSnapshot;
|
snapshot: SliceSnapshot;
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
mode: 'block' | 'gfx';
|
mode: 'block' | 'gfx';
|
||||||
}): void => {
|
}): { x: number; y: number } => {
|
||||||
const { blockIds, snapshot, container, mode } = options;
|
const { container } = options;
|
||||||
const { previewStd, width, height, scale } = this.getPreviewStd(
|
const { width, height, element, left, top } =
|
||||||
blockIds,
|
this.getPreviewElement(options);
|
||||||
snapshot,
|
|
||||||
mode
|
|
||||||
);
|
|
||||||
const previewTemplate = previewStd.render();
|
|
||||||
|
|
||||||
container.style.transform = `scale(${scale})`;
|
container.style.position = 'absolute';
|
||||||
container.style.width = `${width}px`;
|
container.style.left = left ? `${left}px` : '';
|
||||||
if (height) {
|
container.style.top = top ? `${top}px` : '';
|
||||||
container.style.height = `${height}px`;
|
container.style.width = width ? `${width}px` : '';
|
||||||
}
|
container.style.height = height ? `${height}px` : '';
|
||||||
container.append(previewTemplate);
|
container.append(element);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: left ?? 0,
|
||||||
|
y: top ?? 0,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(readonly widget: AffineDragHandleWidget) {}
|
constructor(readonly widget: AffineDragHandleWidget) {}
|
||||||
|
|||||||
@@ -1403,14 +1403,14 @@ export class DragEventWatcher {
|
|||||||
|
|
||||||
const { snapshot, fromMode } = source.data.bsEntity;
|
const { snapshot, fromMode } = source.data.bsEntity;
|
||||||
|
|
||||||
this.previewHelper.renderDragPreview({
|
const offset = this.previewHelper.renderDragPreview({
|
||||||
blockIds: source.data?.bsEntity?.modelIds,
|
blockIds: source.data?.bsEntity?.modelIds,
|
||||||
snapshot,
|
snapshot,
|
||||||
container,
|
container,
|
||||||
mode: fromMode ?? 'block',
|
mode: fromMode ?? 'block',
|
||||||
});
|
});
|
||||||
|
|
||||||
setOffset({ x: 0, y: 0 });
|
setOffset(offset);
|
||||||
},
|
},
|
||||||
setDragData: () => {
|
setDragData: () => {
|
||||||
const { fromMode, snapshot } = this._getDraggedSnapshot();
|
const { fromMode, snapshot } = this._getDraggedSnapshot();
|
||||||
|
|||||||
Reference in New Issue
Block a user