silverbullet/web/navigator.ts

156 lines
4.2 KiB
TypeScript
Raw Normal View History

2024-07-30 23:33:33 +08:00
import { type PageRef, parsePageRef } from "../plug-api/lib/page_ref.ts";
import type { Client } from "./client.ts";
import { cleanPageRef } from "@silverbulletmd/silverbullet/lib/resolve";
import { renderTheTemplate } from "$common/syscalls/template.ts";
import { safeRun } from "../lib/async.ts";
export type PageState = PageRef & {
scrollTop?: number;
selection?: {
anchor: number;
head?: number;
};
};
2022-03-28 21:25:05 +08:00
export class PathPageNavigator {
navigationResolve?: () => void;
indexPage!: string;
2022-03-28 21:25:05 +08:00
openPages = new Map<string, PageState>();
constructor(
private client: Client,
) {
}
async init() {
this.indexPage = cleanPageRef(
await renderTheTemplate(
this.client.config.indexPage,
{},
{},
2024-03-05 02:50:37 +08:00
this.client.stateDataStore.functionMap,
),
);
}
2022-07-22 19:44:28 +08:00
/**
* Navigates the client to the given page, this involves:
* - Patching the current popstate with current state
* - Pushing the new state
* - Dispatching a popstate event
* @param pageRef to navigate to
* @param replaceState whether to update the state in place (rather than to push a new state)
*/
2024-01-02 18:01:34 +08:00
async navigate(
pageRef: PageRef,
2024-01-02 18:01:34 +08:00
replaceState = false,
) {
if (pageRef.page === this.indexPage) {
pageRef.page = "";
2022-08-02 18:43:39 +08:00
}
const currentState = this.buildCurrentPageState();
// No need to keep pos and anchor if we already have scrollTop and selection
const cleanState = { ...currentState, pos: undefined, anchor: undefined };
this.openPages.set(currentState.page || this.indexPage, cleanState);
if (!replaceState) {
window.history.replaceState(
cleanState,
"",
2024-08-18 16:39:04 +08:00
`/${encodeURIComponent(currentState.page)}`,
);
window.history.pushState(
pageRef,
"",
2024-08-18 16:39:04 +08:00
`/${encodeURIComponent(pageRef.page)}`,
);
} else {
window.history.replaceState(
pageRef,
"",
2024-08-18 16:39:04 +08:00
`/${encodeURIComponent(pageRef.page)}`,
);
}
globalThis.dispatchEvent(
2022-04-01 21:02:35 +08:00
new PopStateEvent("popstate", {
state: pageRef,
}),
2022-03-28 21:25:05 +08:00
);
await new Promise<void>((resolve) => {
this.navigationResolve = resolve;
});
this.navigationResolve = undefined;
}
2022-03-28 21:25:05 +08:00
buildCurrentPageState(): PageState {
2024-01-24 21:44:39 +08:00
const pageState: PageState = parsePageRefFromURI();
const mainSelection = this.client.editorView.state.selection.main;
pageState.scrollTop = this.client.editorView.scrollDOM.scrollTop;
pageState.selection = {
head: mainSelection.head,
anchor: mainSelection.anchor,
};
return pageState;
}
2022-03-28 21:25:05 +08:00
subscribe(
2024-01-02 18:01:34 +08:00
pageLoadCallback: (
pageState: PageState,
2024-01-02 18:01:34 +08:00
) => Promise<void>,
2022-03-28 21:25:05 +08:00
): void {
const cb = (event: PopStateEvent) => {
2022-04-01 21:03:12 +08:00
safeRun(async () => {
const popState = event.state;
if (popState) {
// This is the usual case
if (!popState.page) {
popState.page = this.indexPage;
}
if (
popState.anchor === undefined && popState.pos === undefined &&
popState.selection === undefined &&
popState.scrollTop === undefined
) {
// Pretty low-context popstate, so let's leverage openPages
const openPage = this.openPages.get(popState.page);
if (openPage) {
popState.selection = openPage.selection;
popState.scrollTop = openPage.scrollTop;
}
}
await pageLoadCallback(popState);
} else {
// This occurs when the page is loaded completely fresh with no browser history around it
2024-01-24 21:44:39 +08:00
const pageRef = parsePageRefFromURI();
if (!pageRef.page) {
pageRef.page = this.indexPage;
}
await pageLoadCallback(pageRef);
}
2022-04-01 21:03:12 +08:00
if (this.navigationResolve) {
this.navigationResolve();
}
2022-04-01 21:03:12 +08:00
});
};
globalThis.addEventListener("popstate", cb);
cb(
new PopStateEvent("popstate", {
state: this.buildCurrentPageState(),
}),
);
}
2024-01-24 21:44:39 +08:00
}
2024-01-24 21:44:39 +08:00
export function parsePageRefFromURI(): PageRef {
2024-08-18 16:39:04 +08:00
const pageRef = parsePageRef(decodeURIComponent(
2024-01-24 21:44:39 +08:00
location.pathname.substring(1),
));
2024-01-25 21:51:40 +08:00
if (location.hash) {
2024-08-18 16:39:04 +08:00
pageRef.header = decodeURIComponent(location.hash.substring(1));
2024-01-25 21:51:40 +08:00
}
2024-01-24 21:44:39 +08:00
return pageRef;
}