mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
refactor(core): new back&forward button base on workbench (#6012)
# feature: ## In Browser: - hidden back&forward button in sidebar. - back and forward is equal with `window.history.back()` `window.history.forward()` ## In Desktop: - Back and forward can be controlled through the sidebar, cmdk, and shortcut keys. - back and forward act on the currently **active** view. - buttons change disable&enable style based on current active view history # Refactor: Move app-sidebar and app-container from @affine/component to @affine/core
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { LiveData } from '@toeverything/infra';
|
||||
import type { Location } from 'history';
|
||||
import { Observable, switchMap } from 'rxjs';
|
||||
|
||||
import type { Workbench } from '../../workbench';
|
||||
|
||||
export class Navigator {
|
||||
constructor(private readonly workbench: Workbench) {}
|
||||
|
||||
private readonly history = this.workbench.activeView.map(
|
||||
view => view.history
|
||||
);
|
||||
|
||||
private readonly location = LiveData.from(
|
||||
this.history.pipe(
|
||||
switchMap(
|
||||
history =>
|
||||
new Observable<{ index: number; entries: Location[] }>(subscriber => {
|
||||
subscriber.next({ index: history.index, entries: history.entries });
|
||||
return history.listen(() => {
|
||||
subscriber.next({
|
||||
index: history.index,
|
||||
entries: history.entries,
|
||||
});
|
||||
});
|
||||
})
|
||||
)
|
||||
),
|
||||
{ index: 0, entries: [] }
|
||||
);
|
||||
|
||||
readonly backable = this.location.map(
|
||||
({ index, entries }) => index > 0 && entries.length > 1
|
||||
);
|
||||
|
||||
readonly forwardable = this.location.map(
|
||||
({ index, entries }) => index < entries.length - 1
|
||||
);
|
||||
|
||||
back() {
|
||||
if (!environment.isDesktop) {
|
||||
window.history.back();
|
||||
} else {
|
||||
this.history.value.back();
|
||||
}
|
||||
}
|
||||
|
||||
forward() {
|
||||
if (!environment.isDesktop) {
|
||||
window.history.forward();
|
||||
} else {
|
||||
this.history.value.forward();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
packages/frontend/core/src/modules/navigation/index.ts
Normal file
2
packages/frontend/core/src/modules/navigation/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Navigator } from './entities/navigator';
|
||||
export { NavigationButtons } from './view/navigation-buttons';
|
||||
@@ -0,0 +1,13 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
columnGap: '32px',
|
||||
});
|
||||
|
||||
export const button = style({
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
flexShrink: 0,
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { IconButton, Tooltip } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { useService } from '@toeverything/infra/di';
|
||||
import { useLiveData } from '@toeverything/infra/livedata';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { useGeneralShortcuts } from '../../../hooks/affine/use-shortcuts';
|
||||
import { Navigator } from '../entities/navigator';
|
||||
import * as styles from './navigation-buttons.css';
|
||||
import { useRegisterNavigationCommands } from './use-register-navigation-commands';
|
||||
|
||||
export const NavigationButtons = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const shortcuts = useGeneralShortcuts().shortcuts;
|
||||
|
||||
useRegisterNavigationCommands();
|
||||
|
||||
const shortcutsObject = useMemo(() => {
|
||||
const goBack = t['com.affine.keyboardShortcuts.goBack']();
|
||||
const goBackShortcut = shortcuts?.[goBack];
|
||||
|
||||
const goForward = t['com.affine.keyboardShortcuts.goForward']();
|
||||
const goForwardShortcut = shortcuts?.[goForward];
|
||||
return {
|
||||
goBack,
|
||||
goBackShortcut,
|
||||
goForward,
|
||||
goForwardShortcut,
|
||||
};
|
||||
}, [shortcuts, t]);
|
||||
|
||||
const navigator = useService(Navigator);
|
||||
|
||||
const backable = useLiveData(navigator.backable);
|
||||
const forwardable = useLiveData(navigator.forwardable);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
navigator.back();
|
||||
}, [navigator]);
|
||||
|
||||
const handleForward = useCallback(() => {
|
||||
navigator.forward();
|
||||
}, [navigator]);
|
||||
|
||||
useEffect(() => {
|
||||
const cb = (event: MouseEvent) => {
|
||||
if (event.button === 3 || event.button === 4) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.button === 3) {
|
||||
navigator.back();
|
||||
} else {
|
||||
navigator.forward();
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('mouseup', cb);
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', cb);
|
||||
};
|
||||
}, [navigator]);
|
||||
|
||||
if (!environment.isDesktop) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Tooltip
|
||||
content={`${shortcutsObject.goBack} ${shortcutsObject.goBackShortcut}`}
|
||||
side="bottom"
|
||||
>
|
||||
<IconButton
|
||||
className={styles.button}
|
||||
data-testid="app-navigation-button-back"
|
||||
disabled={!backable}
|
||||
onClick={handleBack}
|
||||
>
|
||||
<ArrowLeftSmallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={`${shortcutsObject.goForward} ${shortcutsObject.goForwardShortcut}`}
|
||||
side="bottom"
|
||||
>
|
||||
<IconButton
|
||||
className={styles.button}
|
||||
data-testid="app-navigation-button-forward"
|
||||
disabled={!forwardable}
|
||||
onClick={handleForward}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
PreconditionStrategy,
|
||||
registerAffineCommand,
|
||||
} from '@toeverything/infra/command';
|
||||
import { useService } from '@toeverything/infra/di';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Navigator } from '../entities/navigator';
|
||||
|
||||
export function useRegisterNavigationCommands() {
|
||||
const navigator = useService(Navigator);
|
||||
useEffect(() => {
|
||||
const unsubs: Array<() => void> = [];
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:shortcut-history-go-back',
|
||||
category: 'affine:general',
|
||||
preconditionStrategy: PreconditionStrategy.Never,
|
||||
icon: 'none',
|
||||
label: 'go back',
|
||||
keyBinding: {
|
||||
binding: '$mod+[',
|
||||
},
|
||||
run() {
|
||||
navigator.back();
|
||||
},
|
||||
})
|
||||
);
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:shortcut-history-go-forward',
|
||||
category: 'affine:general',
|
||||
preconditionStrategy: PreconditionStrategy.Never,
|
||||
icon: 'none',
|
||||
label: 'go forward',
|
||||
keyBinding: {
|
||||
binding: '$mod+]',
|
||||
},
|
||||
run() {
|
||||
navigator.forward();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}, [navigator]);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
LocalStorageGlobalCache,
|
||||
LocalStorageGlobalState,
|
||||
} from './infra-web/storage';
|
||||
import { Navigator } from './navigation';
|
||||
import { RightSidebar } from './right-sidebar/entities/right-sidebar';
|
||||
import { Workbench } from './workbench';
|
||||
import {
|
||||
@@ -24,6 +25,7 @@ export function configureBusinessServices(services: ServiceCollection) {
|
||||
services
|
||||
.scope(WorkspaceScope)
|
||||
.add(Workbench)
|
||||
.add(Navigator, [Workbench])
|
||||
.add(RightSidebar)
|
||||
.add(WorkspacePropertiesAdapter, [Workspace])
|
||||
.add(CollectionService, [Workspace])
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { LiveData } from '@toeverything/infra';
|
||||
import type { Location, To } from 'history';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { createIsland } from '../../../utils/island';
|
||||
import { createNavigableHistory } from '../../../utils/navigable-history';
|
||||
|
||||
export class View {
|
||||
constructor(defaultPath: To = { pathname: '/all' }) {
|
||||
this.history = createNavigableHistory({
|
||||
initialEntries: [defaultPath],
|
||||
initialIndex: 0,
|
||||
});
|
||||
}
|
||||
|
||||
id = nanoid();
|
||||
|
||||
history = createMemoryHistory();
|
||||
history = createNavigableHistory({
|
||||
initialEntries: ['/all'],
|
||||
initialIndex: 0,
|
||||
});
|
||||
|
||||
location = LiveData.from<Location>(
|
||||
new Observable(subscriber => {
|
||||
@@ -20,6 +30,17 @@ export class View {
|
||||
}),
|
||||
this.history.location
|
||||
);
|
||||
|
||||
entries = LiveData.from<Location[]>(
|
||||
new Observable(subscriber => {
|
||||
subscriber.next(this.history.entries);
|
||||
return this.history.listen(() => {
|
||||
subscriber.next(this.history.entries);
|
||||
});
|
||||
}),
|
||||
this.history.entries
|
||||
);
|
||||
|
||||
size = new LiveData(100);
|
||||
|
||||
header = createIsland();
|
||||
|
||||
@@ -27,8 +27,8 @@ export class Workbench {
|
||||
this.activeViewIndex.next(index);
|
||||
}
|
||||
|
||||
createView(at: WorkbenchPosition = 'beside') {
|
||||
const view = new View();
|
||||
createView(at: WorkbenchPosition = 'beside', defaultLocation: To) {
|
||||
const view = new View(defaultLocation);
|
||||
const newViews = [...this.views.value];
|
||||
newViews.splice(this.indexAt(at), 0, view);
|
||||
this.views.next(newViews);
|
||||
@@ -44,16 +44,17 @@ export class Workbench {
|
||||
) {
|
||||
let view = this.viewAt(at);
|
||||
if (!view) {
|
||||
const newIndex = this.createView(at);
|
||||
const newIndex = this.createView(at, to);
|
||||
view = this.viewAt(newIndex);
|
||||
if (!view) {
|
||||
throw new Unreachable();
|
||||
}
|
||||
}
|
||||
if (replaceHistory) {
|
||||
view.history.replace(to);
|
||||
} else {
|
||||
view.history.push(to);
|
||||
if (replaceHistory) {
|
||||
view.history.replace(to);
|
||||
} else {
|
||||
view.history.push(to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,13 @@ export function useBindWorkbenchToDesktopRouter(
|
||||
if (newLocation === null) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
workbench.location.value.pathname === newLocation.pathname &&
|
||||
workbench.location.value.search === newLocation.search &&
|
||||
workbench.location.value.hash === newLocation.hash
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
workbench.open(newLocation);
|
||||
}, [basename, browserLocation, workbench]);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import {
|
||||
appSidebarOpenAtom,
|
||||
SidebarSwitch,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
||||
import { RightSidebarIcon } from '@blocksuite/icons';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
@@ -10,6 +6,10 @@ import { useService } from '@toeverything/infra/di';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
|
||||
import {
|
||||
appSidebarOpenAtom,
|
||||
SidebarSwitch,
|
||||
} from '../../../components/app-sidebar';
|
||||
import { RightSidebar } from '../../right-sidebar';
|
||||
import * as styles from './route-container.css';
|
||||
import { useView } from './use-view';
|
||||
|
||||
Reference in New Issue
Block a user