mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 10:06:17 +08: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,221 @@
|
||||
import type { Blocker, Listener, Location, To } from 'history';
|
||||
import {
|
||||
Action,
|
||||
createPath,
|
||||
type MemoryHistory,
|
||||
type MemoryHistoryOptions,
|
||||
parsePath,
|
||||
} from 'history';
|
||||
|
||||
export interface NavigableHistory extends MemoryHistory {
|
||||
entries: Location[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `createMemoryHistory` from `history` package, but with additional `entries` property.
|
||||
*
|
||||
* Original `MemoryHistory` does not have `entries` property, so we can't get `backable` and `forwardable` state which
|
||||
* is needed for implementing back and forward buttons.
|
||||
*
|
||||
* @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#creatememoryhistory
|
||||
*/
|
||||
export function createNavigableHistory(
|
||||
options: MemoryHistoryOptions = {}
|
||||
): NavigableHistory {
|
||||
const { initialEntries = ['/'], initialIndex } = options;
|
||||
const entries: Location[] = initialEntries.map(entry => {
|
||||
const location = Object.freeze<Location>({
|
||||
pathname: '/',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
key: createKey(),
|
||||
...(typeof entry === 'string' ? parsePath(entry) : entry),
|
||||
});
|
||||
|
||||
warning(
|
||||
location.pathname.charAt(0) === '/',
|
||||
`Relative pathnames are not supported in createMemoryHistory({ initialEntries }) (invalid entry: ${JSON.stringify(
|
||||
entry
|
||||
)})`
|
||||
);
|
||||
|
||||
return location;
|
||||
});
|
||||
let index = clamp(
|
||||
initialIndex == null ? entries.length - 1 : initialIndex,
|
||||
0,
|
||||
entries.length - 1
|
||||
);
|
||||
|
||||
let action = Action.Pop;
|
||||
let location = entries[index];
|
||||
const listeners = createEvents<Listener>();
|
||||
const blockers = createEvents<Blocker>();
|
||||
|
||||
function createHref(to: To) {
|
||||
return typeof to === 'string' ? to : createPath(to);
|
||||
}
|
||||
|
||||
function getNextLocation(to: To, state: any = null): Location {
|
||||
return Object.freeze<Location>({
|
||||
pathname: location.pathname,
|
||||
search: '',
|
||||
hash: '',
|
||||
...(typeof to === 'string' ? parsePath(to) : to),
|
||||
state,
|
||||
key: createKey(),
|
||||
});
|
||||
}
|
||||
|
||||
function allowTx(action: Action, location: Location, retry: () => void) {
|
||||
return (
|
||||
!blockers.length || (blockers.call({ action, location, retry }), false)
|
||||
);
|
||||
}
|
||||
|
||||
function applyTx(nextAction: Action, nextLocation: Location) {
|
||||
action = nextAction;
|
||||
location = nextLocation;
|
||||
listeners.call({ action, location });
|
||||
}
|
||||
|
||||
function push(to: To, state?: any) {
|
||||
const nextAction = Action.Push;
|
||||
const nextLocation = getNextLocation(to, state);
|
||||
function retry() {
|
||||
push(to, state);
|
||||
}
|
||||
|
||||
warning(
|
||||
location.pathname.charAt(0) === '/',
|
||||
`Relative pathnames are not supported in memory history.push(${JSON.stringify(
|
||||
to
|
||||
)})`
|
||||
);
|
||||
|
||||
if (allowTx(nextAction, nextLocation, retry)) {
|
||||
index += 1;
|
||||
entries.splice(index, entries.length, nextLocation);
|
||||
applyTx(nextAction, nextLocation);
|
||||
}
|
||||
}
|
||||
|
||||
function replace(to: To, state?: any) {
|
||||
const nextAction = Action.Replace;
|
||||
const nextLocation = getNextLocation(to, state);
|
||||
function retry() {
|
||||
replace(to, state);
|
||||
}
|
||||
|
||||
warning(
|
||||
location.pathname.charAt(0) === '/',
|
||||
`Relative pathnames are not supported in memory history.replace(${JSON.stringify(
|
||||
to
|
||||
)})`
|
||||
);
|
||||
|
||||
if (allowTx(nextAction, nextLocation, retry)) {
|
||||
entries[index] = nextLocation;
|
||||
applyTx(nextAction, nextLocation);
|
||||
}
|
||||
}
|
||||
|
||||
function go(delta: number) {
|
||||
const nextIndex = clamp(index + delta, 0, entries.length - 1);
|
||||
const nextAction = Action.Pop;
|
||||
const nextLocation = entries[nextIndex];
|
||||
function retry() {
|
||||
go(delta);
|
||||
}
|
||||
|
||||
if (allowTx(nextAction, nextLocation, retry)) {
|
||||
index = nextIndex;
|
||||
applyTx(nextAction, nextLocation);
|
||||
}
|
||||
}
|
||||
|
||||
const history: NavigableHistory = {
|
||||
get index() {
|
||||
return index;
|
||||
},
|
||||
get action() {
|
||||
return action;
|
||||
},
|
||||
get location() {
|
||||
return location;
|
||||
},
|
||||
get entries() {
|
||||
return entries;
|
||||
},
|
||||
createHref,
|
||||
push,
|
||||
replace,
|
||||
go,
|
||||
back() {
|
||||
go(-1);
|
||||
},
|
||||
forward() {
|
||||
go(1);
|
||||
},
|
||||
listen(listener) {
|
||||
return listeners.push(listener);
|
||||
},
|
||||
block(blocker) {
|
||||
return blockers.push(blocker);
|
||||
},
|
||||
};
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
function createKey() {
|
||||
return Math.random().toString(36).substr(2, 8);
|
||||
}
|
||||
|
||||
function warning(cond: any, message: string) {
|
||||
if (!cond) {
|
||||
// eslint-disable-next-line no-console
|
||||
if (typeof console !== 'undefined') console.warn(message);
|
||||
|
||||
try {
|
||||
// Welcome to debugging history!
|
||||
//
|
||||
// This error is thrown as a convenience so you can more easily
|
||||
// find the source for a warning that appears in the console by
|
||||
// enabling "pause on exceptions" in your JavaScript debugger.
|
||||
throw new Error(message);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(n: number, lowerBound: number, upperBound: number) {
|
||||
return Math.min(Math.max(n, lowerBound), upperBound);
|
||||
}
|
||||
|
||||
type Events<F> = {
|
||||
length: number;
|
||||
push: (fn: F) => () => void;
|
||||
call: (arg: any) => void;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
function createEvents<F extends Function>(): Events<F> {
|
||||
let handlers: F[] = [];
|
||||
|
||||
return {
|
||||
get length() {
|
||||
return handlers.length;
|
||||
},
|
||||
push(fn: F) {
|
||||
handlers.push(fn);
|
||||
return function () {
|
||||
handlers = handlers.filter(handler => handler !== fn);
|
||||
};
|
||||
},
|
||||
call(arg) {
|
||||
handlers.forEach(fn => fn && fn(arg));
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user