mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat: add shortcut of zooming to selection (#9447)
### Changed - change edgeless shortcut `cmd + 0`, `cmd + 1` to `alt + 0`, `alt + 1` - add new shortcut `alt + 2` to zoom to currently selected elements
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-text';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import {
|
||||
ConnectorElementModel,
|
||||
ConnectorMode,
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
isGfxGroupCompatibleModel,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import { IS_MAC } from '@blocksuite/global/env';
|
||||
import { Bound } from '@blocksuite/global/utils';
|
||||
import { Bound, getCommonBound } from '@blocksuite/global/utils';
|
||||
|
||||
import {
|
||||
getNearestTranslation,
|
||||
@@ -48,6 +49,10 @@ import {
|
||||
} from './utils/text.js';
|
||||
|
||||
export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
get gfx() {
|
||||
return this.rootComponent.gfx;
|
||||
}
|
||||
|
||||
constructor(override rootComponent: EdgelessRootBlockComponent) {
|
||||
super(rootComponent);
|
||||
this.rootComponent.bindHotKey(
|
||||
@@ -254,18 +259,40 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
editing: false,
|
||||
});
|
||||
},
|
||||
'Mod-1': ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
this.rootComponent.service.setZoomByAction('fit');
|
||||
},
|
||||
'Mod--': ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
this.rootComponent.service.setZoomByAction('out');
|
||||
},
|
||||
'Mod-0': ctx => {
|
||||
'Alt-0': ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
this.rootComponent.service.setZoomByAction('reset');
|
||||
},
|
||||
'Alt-1': ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
this.rootComponent.service.setZoomByAction('fit');
|
||||
},
|
||||
'Alt-2': ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
|
||||
const selectedElements = this.gfx.selection.selectedElements;
|
||||
|
||||
if (selectedElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bound = getCommonBound(selectedElements);
|
||||
if (bound === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast(this.rootComponent.host, 'Zoom to selection');
|
||||
|
||||
this.gfx.viewport.setViewportByBound(
|
||||
bound,
|
||||
[0.12, 0.12, 0.12, 0.12],
|
||||
true
|
||||
);
|
||||
},
|
||||
'Mod-=': ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
this.rootComponent.service.setZoomByAction('in');
|
||||
|
||||
@@ -278,17 +278,47 @@ export class Viewport {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the viewport to fit the bound with padding.
|
||||
* @param bound The bound will be zoomed to fit the viewport.
|
||||
* @param padding The padding will be applied to the bound after zooming, default is [0, 0, 0, 0],
|
||||
* the value may be reduced if there is not enough space for the padding.
|
||||
* Use decimal less than 1 to represent percentage padding. e.g. [0.1, 0.1, 0.1, 0.1] means 10% padding.
|
||||
* @param smooth whether to animate the zooming
|
||||
*/
|
||||
setViewportByBound(
|
||||
bound: Bound,
|
||||
padding: [number, number, number, number] = [0, 0, 0, 0],
|
||||
smooth = false
|
||||
) {
|
||||
const [pt, pr, pb, pl] = padding;
|
||||
const zoom = clamp(
|
||||
let [pt, pr, pb, pl] = padding;
|
||||
|
||||
// Convert percentage padding to absolute values if they are between 0 and 1
|
||||
if (pt > 0 && pt < 1) pt *= this.height;
|
||||
if (pr > 0 && pr < 1) pr *= this.width;
|
||||
if (pb > 0 && pb < 1) pb *= this.height;
|
||||
if (pl > 0 && pl < 1) pl *= this.width;
|
||||
|
||||
// Calculate zoom
|
||||
let zoom = Math.min(
|
||||
(this.width - (pr + pl)) / bound.w,
|
||||
this.ZOOM_MIN,
|
||||
(this.height - (pt + pb)) / bound.h
|
||||
);
|
||||
|
||||
// Adjust padding if space is not enough
|
||||
if (zoom < this.ZOOM_MIN) {
|
||||
zoom = this.ZOOM_MIN;
|
||||
const totalPaddingWidth = this.width - bound.w * zoom;
|
||||
const totalPaddingHeight = this.height - bound.h * zoom;
|
||||
pr = pl = Math.max(totalPaddingWidth / 2, 1);
|
||||
pt = pb = Math.max(totalPaddingHeight / 2, 1);
|
||||
}
|
||||
|
||||
// Ensure zoom does not exceed ZOOM_MAX
|
||||
if (zoom > this.ZOOM_MAX) {
|
||||
zoom = this.ZOOM_MAX;
|
||||
}
|
||||
|
||||
const center = [
|
||||
bound.x + (bound.w + pr / zoom) / 2 - pl / zoom / 2,
|
||||
bound.y + (bound.h + pb / zoom) / 2 - pt / zoom / 2,
|
||||
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
zoomInByKeyboard,
|
||||
zoomOutByKeyboard,
|
||||
zoomResetByKeyboard,
|
||||
zoomToSelection,
|
||||
} from '../utils/actions/edgeless.js';
|
||||
import {
|
||||
clickView,
|
||||
dragBetweenCoords,
|
||||
enterPlaygroundRoom,
|
||||
focusRichText,
|
||||
initEmptyEdgelessState,
|
||||
@@ -236,6 +238,45 @@ test.describe('zooming', () => {
|
||||
zoom = await getZoomLevel(page);
|
||||
expect(zoom).toBe(150);
|
||||
});
|
||||
|
||||
test('zoom to selection', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyEdgelessState(page);
|
||||
await switchEditorMode(page);
|
||||
await zoomToSelection(page);
|
||||
|
||||
const start = { x: 0, y: 0 };
|
||||
const end = { x: 900, y: 200 };
|
||||
await addBasicRectShapeElement(page, start, end);
|
||||
await page.keyboard.down('Space');
|
||||
await dragBetweenCoords(
|
||||
page,
|
||||
{
|
||||
x: 200,
|
||||
y: 200,
|
||||
},
|
||||
{
|
||||
x: 200 - 50,
|
||||
y: 200 - 50,
|
||||
}
|
||||
);
|
||||
await page.keyboard.up('Space');
|
||||
|
||||
await zoomFitByKeyboard(page);
|
||||
const shapeContained = await page.evaluate(() => {
|
||||
const edgelessBlock = document.querySelector('affine-edgeless-root');
|
||||
if (!edgelessBlock) {
|
||||
throw new Error('edgeless block not found');
|
||||
}
|
||||
|
||||
const gfx = edgelessBlock.gfx;
|
||||
const element = gfx.selection.selectedElements[0];
|
||||
|
||||
return gfx.viewport.viewportBounds.contains(element.elementBound);
|
||||
});
|
||||
|
||||
expect(shapeContained).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('cmd + A should select all elements by default', async ({ page }) => {
|
||||
|
||||
@@ -928,7 +928,7 @@ export async function multiTouchUp(page: Page, points: Point[]) {
|
||||
}
|
||||
|
||||
export async function zoomFitByKeyboard(page: Page) {
|
||||
await page.keyboard.press(`${SHORT_KEY}+1`, { delay: 100 });
|
||||
await page.keyboard.press(`Alt+1`, { delay: 100 });
|
||||
await waitNextFrame(page, 300);
|
||||
}
|
||||
|
||||
@@ -938,7 +938,13 @@ export async function zoomOutByKeyboard(page: Page) {
|
||||
}
|
||||
|
||||
export async function zoomResetByKeyboard(page: Page) {
|
||||
await page.keyboard.press(`${SHORT_KEY}+0`, { delay: 50 });
|
||||
await page.keyboard.press(`Alt+0`, { delay: 50 });
|
||||
// Wait for animation
|
||||
await waitNextFrame(page, 300);
|
||||
}
|
||||
|
||||
export async function zoomToSelection(page: Page) {
|
||||
await page.keyboard.press(`Alt+2`, { delay: 50 });
|
||||
// Wait for animation
|
||||
await waitNextFrame(page, 300);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user