DEV: Introduce history-store service

This commit extracts the storage part of the route-scroll-manager into a dedicated service. This provides a key/value store which will reset for each navigation, and restore previous values when the user uses the back/forward buttons in their browser.
This commit is contained in:
David Taylor 2023-11-20 18:59:49 +00:00
parent 299989b85e
commit a8292d25f8
2 changed files with 96 additions and 28 deletions

View File

@ -0,0 +1,92 @@
import Service, { inject as service } from "@ember/service";
import { TrackedMap } from "@ember-compat/tracked-built-ins";
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
import { bind } from "discourse-common/utils/decorators";
const HISTORY_SIZE = 100;
const HISTORIC_KEY = Symbol("historic");
/**
* This service provides a key-value store which can store per-route information.
* When navigating 'back' via browser controls, the service will restore the data
* for the appropriate route.
*/
@disableImplicitInjections
export default class HistoryStore extends Service {
@service router;
#routeData = new Map();
#uuid;
#route;
constructor() {
super(...arguments);
this.router.on("routeDidChange", this.maybeRouteDidChange);
}
get #data() {
// Check if route changed since we last checked the uuid.
// This can happen if some other logic has a routeDidChange
// handler that runs before ours.
this.maybeRouteDidChange();
const uuid = this.#uuid;
let data = this.#routeData.get(uuid);
if (data) {
return data;
}
data = new TrackedMap();
this.#routeData.set(uuid, data);
this.#pruneOldData();
return data;
}
get isPoppedState() {
return !!this.get(HISTORIC_KEY);
}
get(key) {
return this.#data.get(key);
}
set(key, value) {
return this.#data.set(key, value);
}
delete(key) {
return this.#data.delete(key);
}
#pruneOldData() {
while (this.#routeData.size > HISTORY_SIZE) {
// JS Map guarantees keys will be returned in insertion order
const oldestUUID = this.#routeData.keys().next().value;
this.#routeData.delete(oldestUUID);
}
}
@bind
maybeRouteDidChange() {
if (this.#route === this.router.currentRoute) {
return;
}
this.#route = this.router.currentRoute;
this.#routeData.get(this.#uuid)?.set(HISTORIC_KEY, true);
const newUuid = window.history.state?.uuid;
if (this.#uuid === newUuid) {
// A refresh. Clear the state
this.#routeData.delete(newUuid);
}
this.#uuid = newUuid;
}
willDestroy() {
this.router.off("routeDidChange", this.maybeRouteDidChange);
}
}

View File

@ -4,7 +4,7 @@ import { disableImplicitInjections } from "discourse/lib/implicit-injections";
import { isTesting } from "discourse-common/config/environment";
import { bind } from "discourse-common/utils/decorators";
const MAX_SCROLL_LOCATIONS = 100;
const STORE_KEY = Symbol("scroll-location");
/**
* This service is responsible for managing scroll position when transitioning.
@ -18,9 +18,7 @@ const MAX_SCROLL_LOCATIONS = 100;
@disableImplicitInjections
export default class RouteScrollManager extends Service {
@service router;
scrollLocationHistory = new Map();
uuid;
@service historyStore;
scrollElement = isTesting()
? document.getElementById("ember-testing-container")
@ -28,14 +26,10 @@ export default class RouteScrollManager extends Service {
@bind
routeWillChange() {
if (!this.uuid) {
return;
}
this.scrollLocationHistory.set(this.uuid, [
this.historyStore.set(STORE_KEY, [
this.scrollElement.scrollLeft,
this.scrollElement.scrollTop,
]);
this.#pruneOldScrollLocations();
}
@bind
@ -44,34 +38,16 @@ export default class RouteScrollManager extends Service {
return;
}
const newUuid = this.router.location.getState?.().uuid;
if (newUuid === this.uuid) {
// routeDidChange fired without the history state actually changing. Most likely a refresh.
// Forget the previously-stored scroll location so that we scroll to the top
this.scrollLocationHistory.delete(this.uuid);
}
this.uuid = newUuid;
if (!this.#shouldScroll(transition.to)) {
return;
}
const scrollLocation = this.scrollLocationHistory.get(this.uuid) || [0, 0];
const scrollLocation = this.historyStore.get(STORE_KEY) || [0, 0];
schedule("afterRender", () => {
this.scrollElement.scrollTo(...scrollLocation);
});
}
#pruneOldScrollLocations() {
while (this.scrollLocationHistory.size > MAX_SCROLL_LOCATIONS) {
// JS Map guarantees keys will be returned in insertion order
const oldestUUID = this.scrollLocationHistory.keys().next().value;
this.scrollLocationHistory.delete(oldestUUID);
}
}
#shouldScroll(routeInfo) {
// Leafmost route has priority
for (let route = routeInfo; route; route = route.parent) {