mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
### Overview:
We've been working with some legacy code in the default-tool and edgeless-selected-rect modules, which are responsible for fundamental operations like moving, resizing, and rotating elements. Currently, these operations are hardcoded, making it challenging to extend functionalities without diving deep into the code.
### What's Changing:
Introducing `ElementTransformManager` to streamline the handling of basic transformations (move, resize, rotate) while allowing the business logic to dictate when these actions occur.
Providing two ways to extend the transformations behaviour:
- Extends inside element view definition: Elements can decide how to handle move/resize events, such as enforcing size constraints.
- Extension mechanism provided by this manager: Adjust or completely override default drag behaviors, like snapping elements into alignment.
### Code Examples:
Delegate element movement to TransformManager:
```typescript
class DefaultTool {
override dragStart(event) {
if(this.dragType === DragType.ContentMoving) {
const transformManager = this.std.get(TransformManagerIdentifier);
transformManager.startDrag({ selectedElements, event });
}
}
}
```
Enforce minimum width inside view definition:
```typescript
class EdgelessNoteBlock extends GfxBlockComponent {
onResizeDelta({ dw, dh }) {
const bound = this.model.elementBound;
bound.w = Math.min(MAX_WIDTH, bound.w + dw);
bound.h = Math.min(MAX_HEIGHT, bound.h + dh);
this.model.xywh = bound.serialize();
}
}
```
Use extension to implement element snapping:
```typescript
import { TransformerExtension } from '@blocksuite/std/gfx';
// Just extends the TransformerExtension
class SnapManager extends TransformerExtension {
static override key = 'snap-manager';
onDragInitialize() {
return {
onDragMove(context) {
const { dx, dy } = this.getAlignmentMoveDistance(context.elements);
context.dx = dx;
context.dy = dy;
}
}
}
}
```
### Others
The migration will be divided into several PRs. This PR mostly focus on refactoring elements movement part of `default-tool`.
- Delegate elements movement to `TransformManager`
- Rewrite the default tool extension into `TransformManager` extension
- Add drag handler interface to gfx view (both `GfxBlockComponent` and `GfxElementModelView`) to allow element to define how it gonna react on drag
354 lines
10 KiB
TypeScript
354 lines
10 KiB
TypeScript
import { expect, type Locator, type Page } from '@playwright/test';
|
|
|
|
import { dragBetweenCoords } from '../utils/actions/drag.js';
|
|
import {
|
|
addBasicShapeElement,
|
|
addNote,
|
|
createNote,
|
|
createShapeElement,
|
|
dragBetweenViewCoords,
|
|
edgelessCommonSetup,
|
|
enterPresentationMode,
|
|
getZoomLevel,
|
|
setEdgelessTool,
|
|
Shape,
|
|
switchEditorMode,
|
|
toggleFramePanel,
|
|
} from '../utils/actions/edgeless.js';
|
|
import { waitNextFrame } from '../utils/actions/index.js';
|
|
import {
|
|
assertEdgelessNonSelectedRect,
|
|
assertEdgelessSelectedRect,
|
|
assertZoomLevel,
|
|
} from '../utils/asserts.js';
|
|
import { test } from '../utils/playwright.js';
|
|
|
|
async function dragFrameCard(
|
|
page: Page,
|
|
fromCard: Locator,
|
|
toCard: Locator,
|
|
direction: 'up' | 'down' = 'down'
|
|
) {
|
|
const fromRect = await fromCard.boundingBox();
|
|
const toRect = await toCard.boundingBox();
|
|
// drag to the center of the toCard
|
|
const center = { x: toRect!.width / 2, y: toRect!.height / 2 };
|
|
const offset = direction === 'up' ? { x: 0, y: -20 } : { x: 0, y: 20 };
|
|
await page.mouse.move(fromRect!.x + center.x, fromRect!.y + center.y);
|
|
await page.mouse.down();
|
|
await page.mouse.move(
|
|
toRect!.x + center.x + offset.x,
|
|
toRect!.y + center.y + offset.y,
|
|
{ steps: 10 }
|
|
);
|
|
await page.mouse.up();
|
|
}
|
|
|
|
test.describe('frame panel', () => {
|
|
test('should display empty placeholder when no frames', async ({ page }) => {
|
|
await edgelessCommonSetup(page);
|
|
await toggleFramePanel(page);
|
|
const frameCards = page.locator('affine-frame-card');
|
|
expect(await frameCards.count()).toBe(0);
|
|
|
|
const placeholder = page.locator('.no-frame-placeholder');
|
|
expect(await placeholder.isVisible()).toBeTruthy();
|
|
});
|
|
|
|
test('should display frame cards when there are frames', async ({ page }) => {
|
|
await edgelessCommonSetup(page);
|
|
await toggleFramePanel(page);
|
|
|
|
await addBasicShapeElement(
|
|
page,
|
|
{ x: 300, y: 300 },
|
|
{ x: 350, y: 350 },
|
|
Shape.Square
|
|
);
|
|
|
|
await addNote(page, 'hello', 150, 500);
|
|
|
|
await page.mouse.click(0, 0);
|
|
|
|
await setEdgelessTool(page, 'frame');
|
|
await dragBetweenCoords(page, { x: 250, y: 250 }, { x: 360, y: 360 });
|
|
|
|
await setEdgelessTool(page, 'frame');
|
|
await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 });
|
|
|
|
const frames = page.locator('affine-frame');
|
|
expect(await frames.count()).toBe(2);
|
|
const frameCards = page.locator('affine-frame-card');
|
|
expect(await frameCards.count()).toBe(2);
|
|
});
|
|
|
|
test('should render edgeless note correctly in frame preview', async ({
|
|
page,
|
|
}) => {
|
|
await edgelessCommonSetup(page);
|
|
await toggleFramePanel(page);
|
|
|
|
await addNote(page, 'hello', 150, 500);
|
|
|
|
await page.mouse.click(0, 0);
|
|
|
|
await setEdgelessTool(page, 'frame');
|
|
await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 });
|
|
await waitNextFrame(page, 100);
|
|
|
|
const frames = page.locator('affine-frame');
|
|
expect(await frames.count()).toBe(1);
|
|
const frameCards = page.locator('affine-frame-card');
|
|
expect(await frameCards.count()).toBe(1);
|
|
const edgelessNote = page.locator('affine-frame-card affine-edgeless-note');
|
|
expect(await edgelessNote.count()).toBe(1);
|
|
});
|
|
|
|
test('should update panel when frames change', async ({ page }) => {
|
|
await edgelessCommonSetup(page);
|
|
await toggleFramePanel(page);
|
|
const frameCards = page.locator('affine-frame-card');
|
|
expect(await frameCards.count()).toBe(0);
|
|
|
|
await addNote(page, 'hello', 150, 500);
|
|
|
|
await page.mouse.click(0, 0);
|
|
|
|
await setEdgelessTool(page, 'frame');
|
|
await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 });
|
|
|
|
await setEdgelessTool(page, 'frame');
|
|
await dragBetweenCoords(page, { x: 50, y: 300 }, { x: 120, y: 400 });
|
|
await waitNextFrame(page);
|
|
|
|
const frames = page.locator('affine-frame');
|
|
expect(await frames.count()).toBe(2);
|
|
expect(await frameCards.count()).toBe(2);
|
|
|
|
await page.mouse.click(50, 300);
|
|
await page.keyboard.press('Delete');
|
|
await waitNextFrame(page);
|
|
|
|
expect(await frames.count()).toBe(1);
|
|
expect(await frameCards.count()).toBe(1);
|
|
});
|
|
|
|
test.describe('frame panel behavior after mode switch', () => {
|
|
async function setupFrameTest(page: Page) {
|
|
await edgelessCommonSetup(page);
|
|
await toggleFramePanel(page);
|
|
|
|
await addNote(page, 'hello', 150, 500);
|
|
await page.mouse.click(0, 0);
|
|
await waitNextFrame(page, 100);
|
|
|
|
await setEdgelessTool(page, 'frame');
|
|
await dragBetweenCoords(
|
|
page,
|
|
{ x: 100, y: 440 },
|
|
{ x: 640, y: 600 },
|
|
{ steps: 10 }
|
|
);
|
|
await waitNextFrame(page, 100);
|
|
|
|
const edgelessNote = page.locator(
|
|
'affine-frame-card affine-edgeless-note'
|
|
);
|
|
expect(await edgelessNote.count()).toBe(1);
|
|
|
|
return edgelessNote;
|
|
}
|
|
|
|
test('should render edgeless note correctly after mode switch', async ({
|
|
page,
|
|
}) => {
|
|
const edgelessNote = await setupFrameTest(page);
|
|
|
|
const initialNoteRect = await edgelessNote.boundingBox();
|
|
expect(initialNoteRect).not.toBeNull();
|
|
|
|
const {
|
|
width: noteWidth,
|
|
height: noteHeight,
|
|
x: noteX,
|
|
y: noteY,
|
|
} = initialNoteRect!;
|
|
|
|
const checkNoteRect = async () => {
|
|
expect(await edgelessNote.count()).toBe(1);
|
|
|
|
const newNoteRect = await edgelessNote.boundingBox();
|
|
expect(newNoteRect).not.toBeNull();
|
|
|
|
expect(newNoteRect!.width).toBe(noteWidth);
|
|
expect(newNoteRect!.height).toBe(noteHeight);
|
|
expect(newNoteRect!.x).toBe(noteX);
|
|
expect(newNoteRect!.y).toBe(noteY);
|
|
};
|
|
|
|
await switchEditorMode(page);
|
|
await checkNoteRect();
|
|
|
|
await switchEditorMode(page);
|
|
await checkNoteRect();
|
|
});
|
|
|
|
test('should update frame preview when note is moved', async ({ page }) => {
|
|
const edgelessNote = await setupFrameTest(page);
|
|
|
|
const initialNoteRect = await edgelessNote.boundingBox();
|
|
expect(initialNoteRect).not.toBeNull();
|
|
|
|
await switchEditorMode(page);
|
|
await switchEditorMode(page);
|
|
|
|
async function moveNoteAndCheck(
|
|
start: { x: number; y: number },
|
|
end: { x: number; y: number },
|
|
comparison: 'greaterThan' | 'lessThan'
|
|
) {
|
|
await dragBetweenCoords(page, start, end);
|
|
await waitNextFrame(page);
|
|
|
|
const newNoteRect = await edgelessNote.boundingBox();
|
|
expect(newNoteRect).not.toBeNull();
|
|
|
|
if (comparison === 'greaterThan') {
|
|
expect(newNoteRect!.x).toBeGreaterThan(initialNoteRect!.x);
|
|
expect(newNoteRect!.y).toBeGreaterThan(initialNoteRect!.y);
|
|
} else {
|
|
expect(newNoteRect!.x).toBeLessThan(initialNoteRect!.x);
|
|
expect(newNoteRect!.y).toBeLessThan(initialNoteRect!.y);
|
|
}
|
|
}
|
|
|
|
// Move the note to the right
|
|
await moveNoteAndCheck(
|
|
{ x: 150, y: 500 },
|
|
{ x: 200, y: 550 },
|
|
'greaterThan'
|
|
);
|
|
|
|
// Move the note back to the left
|
|
await moveNoteAndCheck(
|
|
{ x: 200, y: 550 },
|
|
{ x: 100, y: 450 },
|
|
'lessThan'
|
|
);
|
|
|
|
// Move the note diagonally
|
|
await moveNoteAndCheck(
|
|
{ x: 100, y: 450 },
|
|
{ x: 250, y: 600 },
|
|
'greaterThan'
|
|
);
|
|
});
|
|
});
|
|
|
|
test.describe('select and de-select frame', () => {
|
|
async function setupFrameTest(page: Page) {
|
|
await edgelessCommonSetup(page);
|
|
await toggleFramePanel(page);
|
|
|
|
await addNote(page, 'hello', 150, 500);
|
|
|
|
await page.mouse.click(0, 0);
|
|
|
|
await setEdgelessTool(page, 'frame');
|
|
await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 640, y: 600 });
|
|
await waitNextFrame(page);
|
|
|
|
const frames = page.locator('affine-frame');
|
|
const frameCards = page.locator('affine-frame-card');
|
|
expect(await frames.count()).toBe(1);
|
|
expect(await frameCards.count()).toBe(1);
|
|
|
|
return { frames, frameCards };
|
|
}
|
|
|
|
test('by click on frame card', async ({ page }) => {
|
|
const { frameCards } = await setupFrameTest(page);
|
|
|
|
// click on the first frame card
|
|
await frameCards.nth(0).click();
|
|
await assertEdgelessSelectedRect(page, [100, 440, 540, 160]);
|
|
|
|
await frameCards.nth(0).click();
|
|
await assertEdgelessNonSelectedRect(page);
|
|
});
|
|
|
|
test('by click on blank area', async ({ page }) => {
|
|
const { frameCards } = await setupFrameTest(page);
|
|
|
|
// click on the first frame card
|
|
await frameCards.nth(0).click();
|
|
await assertEdgelessSelectedRect(page, [100, 440, 540, 160]);
|
|
|
|
const framePanel = page.locator('.frame-panel-container');
|
|
const panelRect = await framePanel.boundingBox();
|
|
expect(panelRect).not.toBeNull();
|
|
const { x, y, width, height } = panelRect!;
|
|
await page.mouse.click(x + width / 2, y + height / 2);
|
|
await assertEdgelessNonSelectedRect(page);
|
|
});
|
|
});
|
|
|
|
test('should fit the viewport to the frame when double click frame card', async ({
|
|
page,
|
|
}) => {
|
|
await edgelessCommonSetup(page);
|
|
await toggleFramePanel(page);
|
|
|
|
await assertZoomLevel(page, 100);
|
|
|
|
await addNote(page, 'hello', 150, 500);
|
|
await page.mouse.click(0, 0);
|
|
|
|
await setEdgelessTool(page, 'frame');
|
|
await dragBetweenCoords(page, { x: 100, y: 440 }, { x: 600, y: 600 });
|
|
await waitNextFrame(page);
|
|
|
|
const frameCards = page.locator('affine-frame-card');
|
|
await frameCards.nth(0).dblclick();
|
|
|
|
const zoomLevel = await getZoomLevel(page);
|
|
expect(zoomLevel).toBeGreaterThan(100);
|
|
});
|
|
|
|
test('should reorder frames when drag and drop frame card', async ({
|
|
page,
|
|
}) => {
|
|
await edgelessCommonSetup(page);
|
|
await createShapeElement(page, [100, 100], [200, 200], Shape.Square);
|
|
await createNote(page, [300, 100], 'hello');
|
|
|
|
// Frame shape
|
|
await setEdgelessTool(page, 'frame');
|
|
await dragBetweenViewCoords(page, [80, 80], [220, 220]);
|
|
await waitNextFrame(page, 100);
|
|
|
|
// Frame note
|
|
await setEdgelessTool(page, 'frame');
|
|
await dragBetweenViewCoords(page, [240, 0], [800, 200]);
|
|
|
|
expect(await page.locator('affine-frame').count()).toBe(2);
|
|
|
|
await toggleFramePanel(page);
|
|
|
|
const frameCards = page.locator('affine-frame-card');
|
|
expect(await frameCards.count()).toBe(2);
|
|
|
|
// Drag the first frame card to the second
|
|
await dragFrameCard(page, frameCards.nth(0), frameCards.nth(1));
|
|
|
|
await enterPresentationMode(page);
|
|
await waitNextFrame(page, 100);
|
|
|
|
// Check if frame contains note now is the first
|
|
const edgelessNote = page.locator(
|
|
'affine-edgeless-root affine-edgeless-note'
|
|
);
|
|
await expect(edgelessNote).toBeVisible();
|
|
});
|
|
});
|