Files
racket-wiki/static/js/wiki/breadcrumb-trail.js
T
2026-09-02 08:38:03 +02:00

117 lines
3.5 KiB
JavaScript

/*
* Per-tab wiki page history.
*
* BreadcrumbTrail owns the ordered page slugs and their session-storage
* representation. It deliberately knows nothing about page titles, routes or
* DOM elements; the application supplies the current set of valid page slugs.
*/
const storageKey = "racket-wiki-breadcrumb-trail";
const trailLimit = 8;
class BreadcrumbTrail {
/**
* goal : Create an empty breadcrumb history backed by session storage.
* pre : storage implements the Web Storage getItem/setItem interface.
* post : No stored value is read until load is called with valid pages.
* result : A directly usable breadcrumb history.
*/
constructor(storage) {
this.storage = storage;
this.pageSlugs = [];
}
/** Return a copy of the ordered non-home page slugs. */
get slugs() {
return this.pageSlugs.slice();
}
/**
* goal : Restore the trail and discard references to missing pages.
* pre : validSlugs contains the current readable page slugs.
* post : The normalized trail is stored again for this browser tab.
* result : A copy of the restored trail.
* internals: JSON is decoded first, then filtered through validSlugs so stale
* browser state cannot become application navigation.
*/
load(validSlugs) {
let stored = [];
try {
stored = JSON.parse(this.storage.getItem(storageKey) || "[]");
} catch (_error) {
stored = [];
}
const valid = new Set(validSlugs);
if (Array.isArray(stored)) {
this.pageSlugs = stored.filter((slug) =>
typeof slug === "string" && valid.has(slug));
} else {
this.pageSlugs = [];
}
this.#save();
return this.slugs;
}
/** Empty and persist the breadcrumb history. */
clear() {
this.pageSlugs = [];
this.#save();
}
/**
* goal : Record navigation to one wiki page.
* pre : pageSlug identifies the visited page; homeSlug may be null.
* post : Home clears the trail, revisits truncate it, and new visits append.
* result : A copy of the updated trail.
* internals: The trail contains only non-home pages and retains at most the
* eight most recent entries, matching the visible breadcrumb.
*/
record(pageSlug, homeSlug = null) {
if (!pageSlug) return this.slugs;
if (homeSlug && pageSlug === homeSlug) {
this.clear();
return this.slugs;
}
const existingIndex = this.pageSlugs.indexOf(pageSlug);
if (existingIndex >= 0) {
this.pageSlugs = this.pageSlugs.slice(0, existingIndex + 1);
} else {
this.pageSlugs.push(pageSlug);
if (this.pageSlugs.length > trailLimit) {
this.pageSlugs = this.pageSlugs.slice(-trailLimit);
}
}
this.#save();
return this.slugs;
}
/**
* goal : Discard breadcrumb entries that follow a selected trail page.
* pre : pageSlug is a breadcrumb destination; homeSlug may be null.
* post : Home clears the trail; an existing page becomes its final entry.
* result : A copy of the updated trail.
*/
truncate(pageSlug, homeSlug = null) {
if (homeSlug && pageSlug === homeSlug) {
this.clear();
return this.slugs;
}
const index = this.pageSlugs.indexOf(pageSlug);
if (index >= 0) {
this.pageSlugs = this.pageSlugs.slice(0, index + 1);
this.#save();
}
return this.slugs;
}
/** Persist the current representation for this browser tab. */
#save() {
this.storage.setItem(storageKey, JSON.stringify(this.pageSlugs));
}
}
export { BreadcrumbTrail };