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:
parent
299989b85e
commit
a8292d25f8
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue