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:
EYHN
2024-03-05 07:01:24 +00:00
parent b06aeb22dd
commit 7c76c25a9c
77 changed files with 625 additions and 349 deletions
@@ -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));
},
};
}