Files
AFFiNE-Mirror/blocksuite/affine/gfx/pointer/src/tools/pan-tool.ts
T
doodlewind b6e9c41ee3 fix(editor): mid button drag in presentation mode (#12309)
Fixes https://linear.app/affine-design/issue/BS-3448

Before this PR, presentation mode would force quit if user either:

1. Press space
2. Drag with mouse middle button

Unfixed behavior:

https://github.com/user-attachments/assets/8ff4e13a-69a8-4de6-8994-bf36e6e3eb49

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
	- Improved presentation mode to preserve your current panned view when exiting pan mode or toggling fullscreen, preventing unwanted viewport resets.
	- Spacebar actions are now correctly disabled when using the frame navigator tool, avoiding accidental tool switches.
- **New Features**
	- Enhanced presentation controls for smoother transitions and better handling of user navigation states.
	- Added a one-time toast notification for presentations without frames, shown only once per session for better user guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-15 11:12:41 +00:00

102 lines
2.9 KiB
TypeScript

import { on } from '@blocksuite/affine-shared/utils';
import type { PointerEventState } from '@blocksuite/std';
import { BaseTool, MouseButton, type ToolOptions } from '@blocksuite/std/gfx';
import { Signal } from '@preact/signals-core';
interface RestorablePresentToolOptions {
mode?: string; // 'fit' | 'fill', simplified to string for local use
restoredAfterPan?: boolean;
}
export type PanToolOption = {
panning: boolean;
};
export class PanTool extends BaseTool<PanToolOption> {
static override toolName = 'pan';
private _lastPoint: [number, number] | null = null;
readonly panning$ = new Signal<boolean>(false);
override get allowDragWithRightButton(): boolean {
return true;
}
override dragEnd(_: PointerEventState): void {
this._lastPoint = null;
this.panning$.value = false;
}
override dragMove(e: PointerEventState): void {
if (!this._lastPoint) return;
const { viewport } = this.gfx;
const { zoom } = viewport;
const [lastX, lastY] = this._lastPoint;
const deltaX = lastX - e.x;
const deltaY = lastY - e.y;
this._lastPoint = [e.x, e.y];
viewport.applyDeltaCenter(deltaX / zoom, deltaY / zoom);
}
override dragStart(e: PointerEventState): void {
this._lastPoint = [e.x, e.y];
this.panning$.value = true;
}
override mounted(): void {
this.addHook('pointerDown', evt => {
const shouldPanWithMiddle = evt.raw.button === MouseButton.MIDDLE;
if (!shouldPanWithMiddle) {
return;
}
evt.raw.preventDefault();
const currentTool = this.controller.currentToolOption$.peek();
const restoreToPrevious = () => {
const { toolType, options: originalToolOptions } = currentTool;
const selectionToRestore = this.gfx.selection.surfaceSelections;
if (!toolType) return;
let finalOptions: ToolOptions<BaseTool<any>> | undefined =
originalToolOptions;
const PRESENT_TOOL_NAME = 'frameNavigator';
if (toolType.toolName === PRESENT_TOOL_NAME) {
// When restoring PresentTool (frameNavigator) after a temporary pan (e.g., via middle mouse button),
// set 'restoredAfterPan' to true. This allows PresentTool to avoid an unwanted viewport reset
// and maintain the panned position.
const currentPresentOptions = originalToolOptions as
| RestorablePresentToolOptions
| undefined;
finalOptions = {
...currentPresentOptions,
restoredAfterPan: true,
} as RestorablePresentToolOptions;
}
this.controller.setTool(toolType, finalOptions);
this.gfx.selection.set(selectionToRestore);
};
this.controller.setTool(PanTool, {
panning: true,
});
const dispose = on(document, 'pointerup', evt => {
if (evt.button === MouseButton.MIDDLE) {
restoreToPrevious();
dispose();
}
});
return false;
});
}
}