doouding
2025-03-29 04:13:28 +00:00
parent 317d3e7ea6
commit fcc2ec9d66
4 changed files with 225 additions and 106 deletions

View File

@@ -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>`;
}
}

View File

@@ -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
);
} }

View File

@@ -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) {}

View File

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