mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +08:00
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 -->
This commit is contained in:
@@ -185,9 +185,33 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
|
|||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
document.exitFullscreen().catch(console.error);
|
document.exitFullscreen().catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the flag when fully exiting presentation mode
|
||||||
|
this.edgeless.std
|
||||||
|
.get(EditPropsStore)
|
||||||
|
.setStorage('presentNoFrameToastShown', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _moveToCurrentFrame() {
|
private _moveToCurrentFrame(forceMove = false) {
|
||||||
|
const currentToolOption = this.gfx.tool.currentToolOption$.value;
|
||||||
|
const toolOptions = currentToolOption?.options;
|
||||||
|
|
||||||
|
// If PresentTool is being activated after a temporary pan (indicated by restoredAfterPan)
|
||||||
|
// and a forced move isn't explicitly requested, skip moving to the current frame.
|
||||||
|
// This preserves the user's panned position instead of resetting to the frame's default view.
|
||||||
|
if (
|
||||||
|
currentToolOption?.toolType === PresentTool &&
|
||||||
|
toolOptions?.restoredAfterPan &&
|
||||||
|
!forceMove
|
||||||
|
) {
|
||||||
|
// Clear the flag so future navigations behave normally
|
||||||
|
this.gfx.tool.setTool(PresentTool, {
|
||||||
|
...toolOptions,
|
||||||
|
restoredAfterPan: false,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const current = this._currentFrameIndex;
|
const current = this._currentFrameIndex;
|
||||||
const viewport = this.gfx.viewport;
|
const viewport = this.gfx.viewport;
|
||||||
const frame = this._frames[current];
|
const frame = this._frames[current];
|
||||||
@@ -263,28 +287,56 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
|
|||||||
|
|
||||||
_disposables.add(
|
_disposables.add(
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const currentTool = this.gfx.tool.currentToolOption$.value;
|
const currentToolOption = this.gfx.tool.currentToolOption$.value;
|
||||||
const selection = this.gfx.selection;
|
|
||||||
|
|
||||||
if (currentTool?.toolType === PresentTool) {
|
if (currentToolOption?.toolType === PresentTool) {
|
||||||
this._cachedIndex = this._currentFrameIndex;
|
const opts = currentToolOption.options as
|
||||||
this._navigatorMode =
|
| ToolOptions<PresentTool>
|
||||||
(currentTool.options as ToolOptions<PresentTool>)?.mode ??
|
| undefined;
|
||||||
this._navigatorMode;
|
|
||||||
if (isFrameBlock(selection.selectedElements[0])) {
|
const isAlreadyFullscreen = !!document.fullscreenElement;
|
||||||
this._cachedIndex = this._frames.findIndex(
|
|
||||||
frame => frame.id === selection.selectedElements[0].id
|
if (!isAlreadyFullscreen) {
|
||||||
);
|
this._toggleFullScreen();
|
||||||
|
} else {
|
||||||
|
this._fullScreenMode = true;
|
||||||
}
|
}
|
||||||
if (this._frames.length === 0)
|
|
||||||
toast(
|
|
||||||
this.host,
|
|
||||||
'The presentation requires at least 1 frame. You can firstly create a frame.',
|
|
||||||
5000
|
|
||||||
);
|
|
||||||
this._toggleFullScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
this._cachedIndex = this._currentFrameIndex;
|
||||||
|
this._navigatorMode = opts?.mode ?? this._navigatorMode;
|
||||||
|
|
||||||
|
const selection = this.gfx.selection;
|
||||||
|
if (
|
||||||
|
selection.selectedElements.length > 0 &&
|
||||||
|
isFrameBlock(selection.selectedElements[0])
|
||||||
|
) {
|
||||||
|
const selectedFrameId = selection.selectedElements[0].id;
|
||||||
|
const indexOfSelectedFrame = this._frames.findIndex(
|
||||||
|
frame => frame.id === selectedFrameId
|
||||||
|
);
|
||||||
|
if (indexOfSelectedFrame !== -1) {
|
||||||
|
this._cachedIndex = indexOfSelectedFrame;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = this.edgeless.std.get(EditPropsStore);
|
||||||
|
if (this._frames.length === 0) {
|
||||||
|
if (!store.getStorage('presentNoFrameToastShown')) {
|
||||||
|
toast(
|
||||||
|
this.host,
|
||||||
|
'The presentation requires at least 1 frame. You can firstly create a frame.',
|
||||||
|
5000
|
||||||
|
);
|
||||||
|
store.setStorage('presentNoFrameToastShown', true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If frames exist, and the flag was set, reset it.
|
||||||
|
// This allows the toast to show again if all frames are subsequently deleted.
|
||||||
|
if (store.getStorage('presentNoFrameToastShown')) {
|
||||||
|
store.setStorage('presentNoFrameToastShown', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -305,12 +357,10 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
|
|||||||
|
|
||||||
_disposables.addFromEvent(document, 'fullscreenchange', () => {
|
_disposables.addFromEvent(document, 'fullscreenchange', () => {
|
||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
// When enter fullscreen, we need to set current frame to the cached index
|
|
||||||
this._timer = setTimeout(() => {
|
this._timer = setTimeout(() => {
|
||||||
this._currentFrameIndex = this._cachedIndex;
|
this._currentFrameIndex = this._cachedIndex;
|
||||||
}, 400);
|
}, 400);
|
||||||
} else {
|
} else {
|
||||||
// When exit fullscreen, we need to clear the timer
|
|
||||||
clearTimeout(this._timer);
|
clearTimeout(this._timer);
|
||||||
if (
|
if (
|
||||||
this.edgelessTool.toolType === PresentTool &&
|
this.edgelessTool.toolType === PresentTool &&
|
||||||
@@ -324,7 +374,7 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => this._moveToCurrentFrame(), 400);
|
setTimeout(() => this._moveToCurrentFrame(true), 400);
|
||||||
this.slots.fullScreenToggled.next();
|
this.slots.fullScreenToggled.next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -430,11 +480,29 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override updated(changedProperties: PropertyValues) {
|
protected override updated(changedProperties: PropertyValues) {
|
||||||
if (
|
const currentToolOption = this.gfx.tool.currentToolOption$.value;
|
||||||
changedProperties.has('_currentFrameIndex') &&
|
const isPresentToolActive = currentToolOption?.toolType === PresentTool;
|
||||||
this.edgelessTool.toolType === PresentTool
|
const toolOptions = currentToolOption?.options;
|
||||||
) {
|
const isRestoredAfterPan = !!(
|
||||||
this._moveToCurrentFrame();
|
isPresentToolActive && toolOptions?.restoredAfterPan
|
||||||
|
);
|
||||||
|
|
||||||
|
if (changedProperties.has('_currentFrameIndex') && isPresentToolActive) {
|
||||||
|
// When the current frame index changes (e.g., user navigates), a viewport update is needed.
|
||||||
|
// However, if PresentTool is merely being restored after a pan (isRestoredAfterPan = true)
|
||||||
|
// without an explicit index change in this update cycle, we avoid forcing a move to preserve the panned position.
|
||||||
|
// Thus, `forceMove` is true unless it's a pan restoration.
|
||||||
|
const shouldForceMove = !isRestoredAfterPan;
|
||||||
|
this._moveToCurrentFrame(shouldForceMove);
|
||||||
|
} else if (isPresentToolActive && changedProperties.has('edgelessTool')) {
|
||||||
|
// Handles cases where the tool is set/switched to PresentTool (e.g., initial activation or returning from another tool).
|
||||||
|
// Similar to frame index changes, avoid forcing a viewport move if restoring after a pan.
|
||||||
|
const currentToolIsPresentTool =
|
||||||
|
this.edgelessTool.toolType === PresentTool;
|
||||||
|
if (currentToolIsPresentTool) {
|
||||||
|
const shouldForceMoveOnToolChange = !isRestoredAfterPan;
|
||||||
|
this._moveToCurrentFrame(shouldForceMoveOnToolChange);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { NavigatorMode } from './frame-manager';
|
|||||||
|
|
||||||
export type PresentToolOption = {
|
export type PresentToolOption = {
|
||||||
mode?: NavigatorMode;
|
mode?: NavigatorMode;
|
||||||
|
restoredAfterPan?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PresentTool extends BaseTool<PresentToolOption> {
|
export class PresentTool extends BaseTool<PresentToolOption> {
|
||||||
|
|||||||
@@ -467,6 +467,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
|||||||
const selection = gfx.selection;
|
const selection = gfx.selection;
|
||||||
|
|
||||||
if (event.code === 'Space' && !event.repeat) {
|
if (event.code === 'Space' && !event.repeat) {
|
||||||
|
const currentToolName =
|
||||||
|
this.rootComponent.gfx.tool.currentToolName$.peek();
|
||||||
|
if (currentToolName === 'frameNavigator') return false;
|
||||||
this._space(event);
|
this._space(event);
|
||||||
} else if (
|
} else if (
|
||||||
!selection.editing &&
|
!selection.editing &&
|
||||||
@@ -504,8 +507,12 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
|||||||
ctx => {
|
ctx => {
|
||||||
const event = ctx.get('keyboardState').raw;
|
const event = ctx.get('keyboardState').raw;
|
||||||
if (event.code === 'Space' && !event.repeat) {
|
if (event.code === 'Space' && !event.repeat) {
|
||||||
|
const currentToolName =
|
||||||
|
this.rootComponent.gfx.tool.currentToolName$.peek();
|
||||||
|
if (currentToolName === 'frameNavigator') return false;
|
||||||
this._space(event);
|
this._space(event);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
{ global: true }
|
{ global: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { on } from '@blocksuite/affine-shared/utils';
|
import { on } from '@blocksuite/affine-shared/utils';
|
||||||
import type { PointerEventState } from '@blocksuite/std';
|
import type { PointerEventState } from '@blocksuite/std';
|
||||||
import { BaseTool, MouseButton } from '@blocksuite/std/gfx';
|
import { BaseTool, MouseButton, type ToolOptions } from '@blocksuite/std/gfx';
|
||||||
import { Signal } from '@preact/signals-core';
|
import { Signal } from '@preact/signals-core';
|
||||||
|
|
||||||
|
interface RestorablePresentToolOptions {
|
||||||
|
mode?: string; // 'fit' | 'fill', simplified to string for local use
|
||||||
|
restoredAfterPan?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type PanToolOption = {
|
export type PanToolOption = {
|
||||||
panning: boolean;
|
panning: boolean;
|
||||||
};
|
};
|
||||||
@@ -53,14 +58,30 @@ export class PanTool extends BaseTool<PanToolOption> {
|
|||||||
|
|
||||||
evt.raw.preventDefault();
|
evt.raw.preventDefault();
|
||||||
|
|
||||||
const selection = this.gfx.selection.surfaceSelections;
|
|
||||||
const currentTool = this.controller.currentToolOption$.peek();
|
const currentTool = this.controller.currentToolOption$.peek();
|
||||||
const restoreToPrevious = () => {
|
const restoreToPrevious = () => {
|
||||||
const { toolType, options } = currentTool;
|
const { toolType, options: originalToolOptions } = currentTool;
|
||||||
if (toolType && options) {
|
const selectionToRestore = this.gfx.selection.surfaceSelections;
|
||||||
this.controller.setTool(toolType, options);
|
if (!toolType) return;
|
||||||
this.gfx.selection.set(selection);
|
|
||||||
|
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, {
|
this.controller.setTool(PanTool, {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const LocalPropsSchema = z.object({
|
|||||||
presentBlackBackground: z.boolean(),
|
presentBlackBackground: z.boolean(),
|
||||||
presentFillScreen: z.boolean(),
|
presentFillScreen: z.boolean(),
|
||||||
presentHideToolbar: z.boolean(),
|
presentHideToolbar: z.boolean(),
|
||||||
|
presentNoFrameToastShown: z.boolean(),
|
||||||
|
|
||||||
autoHideEmbedHTMLFullScreenToolbar: z.boolean(),
|
autoHideEmbedHTMLFullScreenToolbar: z.boolean(),
|
||||||
});
|
});
|
||||||
@@ -126,6 +127,8 @@ export class EditPropsStore extends LifeCycleWatcher {
|
|||||||
return 'blocksuite:presentation:fillScreen';
|
return 'blocksuite:presentation:fillScreen';
|
||||||
case 'presentHideToolbar':
|
case 'presentHideToolbar':
|
||||||
return 'blocksuite:presentation:hideToolbar';
|
return 'blocksuite:presentation:hideToolbar';
|
||||||
|
case 'presentNoFrameToastShown':
|
||||||
|
return 'blocksuite:presentation:noFrameToastShown';
|
||||||
case 'templateCache':
|
case 'templateCache':
|
||||||
return 'blocksuite:' + id + ':templateTool';
|
return 'blocksuite:' + id + ':templateTool';
|
||||||
case 'remoteColor':
|
case 'remoteColor':
|
||||||
|
|||||||
Reference in New Issue
Block a user