Skip to content

Commit a478f90

Browse files
committed
feat(file-browser): Implement NavStack class for navigation history management
Abstract navigation tracking, history state management, and navbar UI syncing into a dedicated `EventTarget` class. Create `NavStack` class (`src/pages/fileBrowser/NavStack.js`): - Implement `NavStack` extending `EventTarget` with a custom `Symbol.toStringTag` property - Add `push`, `pop`, `popUntil`, `get` (supporting negative indexing), `has`, `on`, `off`, and `toJSON` methods with parameter validation - Maintain an internal `#urlSet` to prevent duplicate stack entries - Queue microtasks for `update` event dispatching, providing read-only `added` and `removed` location diffs in event details Integrate `NavStack` into file browser (`src/pages/fileBrowser/fileBrowser.js`): - Replace manual `state` array and direct `localStorage` persistence with a `NavStack` instance - Listen to `update` events on `NavStack` to persist state to `localStorage`, clean up removed navbar elements and `actionStack` entries, and register new back-navigation actions - Cache navbar DOM elements using a `navBarEls` Map with `getOrInsertComputed` - Refactor `navigate` to accept location objects or strings and manage stack state using `navStack.has`, `navStack.popUntil`, and `navStack.push` - Refactor `loadStates` to push history entries into `navStack` and navigate directly to the top item (`navStack.get(-1)`) - Update folder selection button state (`$openFolder.disabled`) in `render()` and remove obsolete `pushState()` helper function (AI generated commit message)
1 parent f90fc1d commit a478f90

2 files changed

Lines changed: 201 additions & 112 deletions

File tree

src/pages/fileBrowser/NavStack.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import Url from "utils/Url";
2+
3+
/**
4+
* @typedef {{url: string, name: string}} Location
5+
*/
6+
7+
export default class NavStack extends EventTarget {
8+
static {
9+
Object.defineProperty(this.prototype, Symbol.toStringTag, {
10+
value: "NavStack",
11+
configurable: true,
12+
});
13+
}
14+
15+
get length() {
16+
return this.#arr.length;
17+
}
18+
toJSON() {
19+
return this.#arr.map((obj) => ({ ...obj }));
20+
}
21+
on() {
22+
return this.addEventListener(...arguments);
23+
}
24+
off() {
25+
return this.removeEventListener(...arguments);
26+
}
27+
28+
/** @type {null | { added: Map<string, string>, removed: Set<string> }} */
29+
#updatedURLs;
30+
#queueUpdateEvent() {
31+
if (this.#updatedURLs) return;
32+
const added = new Map();
33+
const removed = new Set();
34+
this.#updatedURLs = Object.freeze({ added, removed });
35+
queueMicrotask(() => {
36+
this.#updatedURLs = null;
37+
this.dispatchEvent(
38+
new CustomEvent("update", {
39+
detail: Object.freeze({
40+
get added() {
41+
return added.entries();
42+
},
43+
get removed() {
44+
return removed.values();
45+
},
46+
}),
47+
}),
48+
);
49+
});
50+
}
51+
52+
/** @type {Set<string>} */
53+
#urlSet = new Set();
54+
/** @type {Array<Location>} */
55+
#arr = [];
56+
/**
57+
* @param {{ url: string, name?: string } | string} url
58+
* @param {string} [name]
59+
*/
60+
push(url, name) {
61+
if (typeof url === "object") ({ url, name } = url);
62+
if (!(url = `${url ?? ""}`)) {
63+
throw new TypeError(
64+
"NavStack.prototype.push(" +
65+
"url: { url: string, name?: string } | string, name?: string): \n" +
66+
'"url" is either missing, null or undefined, or resolves to an empty string.',
67+
);
68+
}
69+
const urlSet = this.#urlSet;
70+
if (urlSet.has(url)) return;
71+
urlSet.add(url);
72+
name = `${name ?? ""}` || Url.basename(url) || url;
73+
const arr = this.#arr;
74+
const i = arr.length;
75+
arr[i] = { url, name };
76+
77+
this.#queueUpdateEvent();
78+
const { added, removed } = this.#updatedURLs;
79+
if (removed.has(url)) removed.delete(url);
80+
else added.set(url, { name, index: i });
81+
}
82+
/**
83+
* @param {string} [url]
84+
*/
85+
#popUntil(url) {
86+
const urlSet = this.#urlSet;
87+
const arr = this.#arr;
88+
for (let i = arr.length - 1; i >= 0; i--) {
89+
const item = arr[i];
90+
const url2 = item.url;
91+
if (url && url === url2) return;
92+
this.#urlSet.delete(url2);
93+
arr.length = i;
94+
95+
this.#queueUpdateEvent();
96+
const { added, removed } = this.#updatedURLs;
97+
if (!added.has(url2)) removed.add(url2);
98+
else added.delete(url2);
99+
100+
if (!url) return;
101+
}
102+
}
103+
/**
104+
* @param {string} url
105+
*/
106+
popUntil(url) {
107+
if ((url = `${url ?? ""}`)) return this.#popUntil(url);
108+
throw new TypeError(
109+
"NavStack.prototype.popUntil(url: string): \n" +
110+
'"url" is either missing, null or undefined, or resolves to an empty string.',
111+
);
112+
}
113+
pop() {
114+
return this.#popUntil();
115+
}
116+
/**
117+
* @param {number} i
118+
* @returns {Location}
119+
*/
120+
get(i) {
121+
if ((i = +i) !== i) {
122+
throw new TypeError(
123+
'NavStack.prototype.get(i: number): "i" is either missing or resolves to NaN.',
124+
);
125+
}
126+
const arr = this.#arr;
127+
const l = arr.length;
128+
if (i < 0) i += l;
129+
if (i < 0 || i > l - 1) return;
130+
return { ...arr[i] };
131+
}
132+
has(url) {
133+
return this.#urlSet.has(`${url ?? ""}`);
134+
}
135+
}

0 commit comments

Comments
 (0)