Skip to content

Commit

Permalink
feat(edgeless): support consistent dragging behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
AyushAgrawal-A2 committed Dec 13, 2023
1 parent df545d0 commit 1bead15
Show file tree
Hide file tree
Showing 47 changed files with 1,888 additions and 1,078 deletions.
5 changes: 0 additions & 5 deletions packages/blocks/src/_common/components/file-drop-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export type onDropProps = {

export type FileDropOptions = {
flavour: string;
maxFileSize?: number;
onDrop?: ({ files, targetModel, place, point }: onDropProps) => void;
};

Expand Down Expand Up @@ -81,10 +80,6 @@ export class FileDropManager {
return targetModel;
}

get maxFileSize(): number {
return this._fileDropOptions.maxFileSize ?? 10 * 1000 * 1000; // default to 10MB
}

onDragOver = (event: DragEvent) => {
event.preventDefault();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import {
focusTitle,
} from '../../../_common/utils/selection.js';
import type { ExtendedModel } from '../../../_common/utils/types.js';
import { type ListBlockModel, type PageBlockModel } from '../../../models.js';
import type { ListBlockModel } from '../../../list-block/list-model.js';
import type { PageBlockModel } from '../../../page-block/page-model.js';

/**
* Whether the block supports rendering its children.
Expand Down
116 changes: 111 additions & 5 deletions packages/blocks/src/_common/embed-block-helper/embed-block-element.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
import type { BlockService } from '@blocksuite/block-std';
import { assertExists } from '@blocksuite/global/utils';
import { BlockElement } from '@blocksuite/lit';
import type { BaseBlockModel } from '@blocksuite/store';
import type { TemplateResult } from 'lit';
import { html } from 'lit';
import { html, render } from 'lit';
import { styleMap } from 'lit/directives/style-map.js';

import type { DragHandleOption } from '../../page-block/widgets/drag-handle/config.js';
import { AffineDragHandleWidget } from '../../page-block/widgets/drag-handle/drag-handle.js';
import { captureEventTarget } from '../../page-block/widgets/drag-handle/utils.js';
import { Bound } from '../../surface-block/index.js';
import type { EdgelessSelectableProps } from '../edgeless/mixin/index.js';
import {
convertBlockDocToEdgeless,
convertBlockEdgelessToDoc,
} from '../utils/edgeless.js';
import { type BlockModels, matchFlavours } from '../utils/index.js';

export class EmbedBlockElement<
Model extends
BaseBlockModel<EdgelessSelectableProps> = BaseBlockModel<EdgelessSelectableProps>,
Service extends BlockService = BlockService,
WidgetName extends string = string,
> extends BlockElement<Model, Service, WidgetName> {
protected get _isInSurface() {
const parent = this.host.querySelector('affine-edgeless-page');
return !!parent;
protected _isInSurface = false;

protected _width = 400;
protected _height = 200;

get isInSurface() {
return this._isInSurface;
}

get surface() {
if (!this._isInSurface) return null;

return this.host.querySelector('affine-surface');
}

Expand All @@ -48,4 +60,98 @@ export class EmbedBlockElement<
</div>
`;
};

private _dragHandleOption: DragHandleOption = {
flavour: /affine:embed-*/,
edgeless: true,
onDragStart: ({ state, startDragging, anchorBlockPath }) => {
if (!anchorBlockPath) return false;
const anchorComponent = this.std.view.viewFromPath(
'block',
anchorBlockPath
);
if (
!anchorComponent ||
!matchFlavours(anchorComponent.model, [
this.flavour as keyof BlockModels,
])
)
return false;

const blockComponent = anchorComponent as this;
const isInSurface = blockComponent.isInSurface;
if (!isInSurface) {
this.host.selection.setGroup('block', [
this.host.selection.getInstance('block', {
path: blockComponent.path,
}),
]);
startDragging([blockComponent], state);
return true;
}

const element = captureEventTarget(state.raw.target);
const insideDragHandle = !!element?.closest('affine-drag-handle-widget');
if (!insideDragHandle) return false;

const embedPortal = blockComponent.closest(
'.edgeless-block-portal-embed'
);
assertExists(embedPortal);
const dragPreviewEl = embedPortal.cloneNode() as HTMLElement;
dragPreviewEl.style.transform = '';
render(
blockComponent.host.renderModel(blockComponent.model),
dragPreviewEl
);

startDragging([blockComponent], state, dragPreviewEl);
return true;
},
onDragEnd: props => {
const { state, draggingElements } = props;
if (
draggingElements.length !== 1 ||
!matchFlavours(draggingElements[0].model, [
this.flavour as keyof BlockModels,
])
)
return false;

const blockComponent = draggingElements[0] as this;
const isInSurface = blockComponent.isInSurface;
const target = captureEventTarget(state.raw.target);
const isTargetEdgelessContainer =
target?.classList.contains('edgeless') &&
target?.classList.contains('affine-block-children-container');

if (isInSurface) {
return convertBlockEdgelessToDoc({
blockComponent,
...props,
});
} else if (isTargetEdgelessContainer) {
return convertBlockDocToEdgeless({
blockComponent,
cssSelector: '.embed-block-container',
width: this._width,
height: this._height,
...props,
});
}

return false;
},
};

override connectedCallback() {
super.connectedCallback();

const parent = this.host.page.getParent(this.model);
this._isInSurface = parent?.flavour === 'affine:surface';

this.disposables.add(
AffineDragHandleWidget.registerOption(this._dragHandleOption)
);
}
}
83 changes: 83 additions & 0 deletions packages/blocks/src/_common/utils/edgeless.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { assertExists } from '@blocksuite/global/utils';
import type { BlockElement } from '@blocksuite/lit';

import type { OnDragEndProps } from '../../page-block/widgets/drag-handle/config.js';
import type { EdgelessBlockType } from '../../surface-block/index.js';
import { Bound } from '../../surface-block/index.js';
import { getBlockProps, getEdgelessPage, isPageMode } from './query.js';

function buildViewportKey(pageId: string) {
return 'blocksuite:' + pageId + ':edgelessViewport';
}
Expand Down Expand Up @@ -28,3 +36,78 @@ export function getViewportFromSession(pageId: string): ViewportData | null {
return null;
}
}

export function convertBlockDocToEdgeless({
blockComponent,
dragPreview,
cssSelector,
width,
height,
}: OnDragEndProps & {
blockComponent: BlockElement;
cssSelector: string;
width?: number;
height?: number;
}): boolean {
const page = blockComponent.page;
if (isPageMode(page)) return false;
const edgelessPage = getEdgelessPage(page);
assertExists(edgelessPage);

const previewEl = dragPreview.querySelector(cssSelector);
assertExists(previewEl);
const rect = previewEl.getBoundingClientRect();
const point = edgelessPage.surface.toModelCoord(rect.x, rect.y);
const bound = new Bound(
point[0],
point[1],
width ?? previewEl.clientWidth,
height ?? previewEl.clientHeight
);

const blockModel = blockComponent.model;
const blockProps = getBlockProps(blockModel);

edgelessPage.surface.addElement(
blockComponent.flavour as EdgelessBlockType,
{
...blockProps,
xywh: bound.serialize(),
},
edgelessPage.surface.model
);
page.deleteBlock(blockModel);
return true;
}

export function convertBlockEdgelessToDoc({
blockComponent,
dropBlockId,
dropType,
}: OnDragEndProps & {
blockComponent: BlockElement;
}): boolean {
const page = blockComponent.page;
const targetBlock = page.getBlockById(dropBlockId);
if (!targetBlock) return false;

const shouldInsertIn = dropType === 'in';
const parentBlock = shouldInsertIn
? targetBlock
: page.getParent(targetBlock);
assertExists(parentBlock);
const parentIndex = shouldInsertIn
? 0
: parentBlock.children.indexOf(targetBlock);

const blockModel = blockComponent.model;
const blockProps = getBlockProps(blockModel);
delete blockProps.width;
delete blockProps.height;
delete blockProps.xywh;
delete blockProps.zIndex;

page.addBlock(blockModel.flavour, blockProps, parentBlock, parentIndex);
page.deleteBlock(blockModel);
return true;
}
21 changes: 21 additions & 0 deletions packages/blocks/src/_common/utils/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,10 @@ function isEdgelessChildNote({ classList }: Element) {
return classList.contains('edgeless-block-portal-note');
}

function isEdgelessChildImage({ classList }: Element) {
return classList.contains('edgeless-block-portal-image');
}

/**
* Returns the closest block element by a point in the rect.
*
Expand Down Expand Up @@ -686,6 +690,16 @@ export function getHoveringNote(point: Point) {
);
}

/**
* Get hovering top level image with given a point in edgeless mode.
*/
export function getHoveringImage(point: Point) {
return (
document.elementsFromPoint(point.x, point.y).find(isEdgelessChildImage) ||
null
);
}

/**
* Gets the table of the database.
*/
Expand Down Expand Up @@ -826,3 +840,10 @@ export function getEdgelessCanvasTextEditor(element: Element | Document) {
export function hasClassNameInList(element: Element, classList: string[]) {
return classList.some(className => element.classList.contains(className));
}

export function getBlockProps(model: BaseBlockModel) {
const keys = model.keys as (keyof typeof model)[];
const values = keys.map(key => model[key]);
const blockProps = Object.fromEntries(keys.map((key, i) => [key, values[i]]));
return blockProps;
}
2 changes: 1 addition & 1 deletion packages/blocks/src/_legacy/clipboard/utils/commons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async function generateClipboardInfo(
childrenJson.push(json);
}

const service = await getService(model.flavour);
const service = getService(model.flavour);

const html = await service.block2html(model, {
childText: childrenHtml.join(''),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class BookmarkBlockService extends BaseService<BookmarkBlockModel> {
</figure>
`;
}

override block2Text(block: BookmarkBlockModel): string {
return block.url;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/blocks/src/attachment-block/attachment-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ export class AttachmentBlockComponent extends BlockElement<AttachmentBlockModel>
this._disposables.add(
AffineDragHandleWidget.registerOption({
flavour: AttachmentBlockSchema.model.flavour,
onDragStart: (state, startDragging) => {
// Check if start dragging from the image block
onDragStart: ({ state, startDragging }) => {
// Check if start dragging from the attachment block
const target = captureEventTarget(state.raw.target);
const attachmentBlock = target?.closest('affine-attachment');
if (!attachmentBlock) return false;
Expand Down
26 changes: 7 additions & 19 deletions packages/blocks/src/attachment-block/attachment-service.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import { BlockService } from '@blocksuite/block-std';
import { assertExists, assertInstanceOf } from '@blocksuite/global/utils';

import {
FileDropManager,
type FileDropOptions,
} from '../_common/components/file-drop-manager.js';
import { toast } from '../_common/components/toast.js';
import { humanFileSize } from '../_common/utils/math.js';
import { matchFlavours } from '../_common/utils/model.js';
import { PageService } from '../page-block/page-service.js';
import type { AttachmentBlockModel } from './attachment-model.js';
import { addSiblingAttachmentBlock } from './utils.js';

export class AttachmentService extends BlockService<AttachmentBlockModel> {
maxFileSize = 10 * 1000 * 1000; // 10MB (default)

private _fileDropOptions: FileDropOptions = {
flavour: this.flavour,
maxFileSize: this.maxFileSize,
onDrop: async ({ files, targetModel, place }) => {
if (!files.length || !targetModel) return false;
if (matchFlavours(targetModel, ['affine:surface'])) return false;
Expand All @@ -25,22 +22,13 @@ export class AttachmentService extends BlockService<AttachmentBlockModel> {
file => !file.type.startsWith('image/')
);

const isSizeExceeded = attachmentFiles.some(
file => file.size > this.maxFileSize
);
if (isSizeExceeded) {
toast(
`You can only upload files less than ${humanFileSize(
this.maxFileSize,
true,
0
)}`
);
return true;
}
const pageService = this.std.spec.getService('affine:page');
assertExists(pageService);
assertInstanceOf(pageService, PageService);
const maxFileSize = pageService.maxFileSize;

attachmentFiles.forEach(file =>
addSiblingAttachmentBlock(file, targetModel, this.maxFileSize, place)
addSiblingAttachmentBlock(file, maxFileSize, targetModel, place)
);
return true;
},
Expand Down
2 changes: 1 addition & 1 deletion packages/blocks/src/attachment-block/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ export async function uploadBlobForAttachment(
*/
export async function addSiblingAttachmentBlock(
file: File,
model: BaseBlockModel,
maxFileSize: number,
model: BaseBlockModel,
place: 'before' | 'after' = 'after'
): Promise<string | null> {
if (file.size > maxFileSize) {
Expand Down
Loading

0 comments on commit 1bead15

Please sign in to comment.