@vp-tw/dirwell 0.1.0-alpha.5 → 0.1.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/dist/bin.mjs +1 -1
- package/dist/crosswave-runtime.js +523 -25
- package/dist/index.d.mts +22 -1
- package/dist/index.mjs +207 -73
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @vp-tw/dirwell
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.7
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Make Crosswave categories configurable with ordered labels, decorative icons, overlapping extension/MIME filters, and exported default definitions. Infer filename types with the pinned Node-only MIME database; generated browser assets contain membership IDs without the database. Add a runnable custom-category example and configuration documentation.
|
|
8
|
+
|
|
9
|
+
## 0.1.0-alpha.6
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Keep Crosswave's background and controls alive while directory contents slide in opposite directions for entry and return. Generated navigation assets support SSG/MPA, custom URLs, static hosting, build adapters, and relative local HTML. Restore browsing state through history and handle cancelled, superseded, unavailable, incompatible, restricted, and timed-out loads with usable recovery controls.
|
|
14
|
+
|
|
3
15
|
## 0.1.0-alpha.5
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -12,3 +12,11 @@ in the published package, and in `src/vscode-icons/NOTICE.txt` in the source tre
|
|
|
12
12
|
Generated sites include this notice as `vscode-icons-NOTICE.txt`. The default
|
|
13
13
|
theme footer links to the [Ledger theme README](https://github.com/vp-tw/dirwell/blob/main/src/theme-default/README.md),
|
|
14
14
|
which links to the source notice.
|
|
15
|
+
|
|
16
|
+
## MIME classification
|
|
17
|
+
|
|
18
|
+
Crosswave uses [`mime` 4.1.0](https://github.com/broofa/mime), MIT-licensed,
|
|
19
|
+
Copyright (c) 2023 Robert Kieffer, for filename-based classification during Node
|
|
20
|
+
generation. It uses optimized MIME database mappings; no MIME database is emitted
|
|
21
|
+
to generated browser assets. The installed dependency includes its full MIT
|
|
22
|
+
license. MIME estimates do not validate file contents or change serving headers.
|
package/dist/bin.mjs
CHANGED
|
@@ -12,6 +12,442 @@
|
|
|
12
12
|
if (reduced.matches || root.dataset.cwTransitions === "false") transition.skipTransition();
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
+
// Included in the theme's private runtime closure, never installed as a global API.
|
|
16
|
+
function createCrosswaveNavigator({ main, reduced, getState, commit }) {
|
|
17
|
+
const rootUrl = new URL(main.dataset.cwRoot, document.baseURI);
|
|
18
|
+
const initialUrl = new URL(location.href);
|
|
19
|
+
initialUrl.hash = "";
|
|
20
|
+
const local = initialUrl.protocol === "file:";
|
|
21
|
+
const owner = rootUrl.href;
|
|
22
|
+
const scopePath = rootUrl.pathname.endsWith("/")
|
|
23
|
+
? rootUrl.pathname
|
|
24
|
+
: new URL(".", rootUrl).pathname;
|
|
25
|
+
let current = {
|
|
26
|
+
url: initialUrl.href,
|
|
27
|
+
asset: new URL(main.dataset.cwPage, document.baseURI).href,
|
|
28
|
+
path: main.dataset.cwPath,
|
|
29
|
+
};
|
|
30
|
+
const recent = new Map();
|
|
31
|
+
const historyViews = new Map();
|
|
32
|
+
const savedEntry = history.state?.dirwellCrosswave;
|
|
33
|
+
let index =
|
|
34
|
+
savedEntry?.owner === owner && Number.isInteger(savedEntry.index) ? savedEntry.index : 0,
|
|
35
|
+
request = 0,
|
|
36
|
+
pending,
|
|
37
|
+
animations = [];
|
|
38
|
+
const notice = document.createElement("div");
|
|
39
|
+
notice.className = "cw-navigation-notice";
|
|
40
|
+
notice.setAttribute("role", "status");
|
|
41
|
+
notice.hidden = true;
|
|
42
|
+
document.body.append(notice);
|
|
43
|
+
const allowed = (url) =>
|
|
44
|
+
url.protocol === rootUrl.protocol &&
|
|
45
|
+
url.host === rootUrl.host &&
|
|
46
|
+
url.pathname.startsWith(scopePath);
|
|
47
|
+
const absoluteLinks = (scope, base) => {
|
|
48
|
+
for (const link of scope.querySelectorAll("a[href]")) {
|
|
49
|
+
const url = new URL(link.getAttribute("href"), base);
|
|
50
|
+
if (local && url.pathname.endsWith("/")) url.pathname += "index.html";
|
|
51
|
+
link.href = url.href;
|
|
52
|
+
}
|
|
53
|
+
for (const link of scope.querySelectorAll("[data-cw-page]"))
|
|
54
|
+
link.dataset.cwPage = new URL(link.dataset.cwPage, base).href;
|
|
55
|
+
};
|
|
56
|
+
absoluteLinks(document, document.baseURI);
|
|
57
|
+
const ownsCurrentEntry = () => {
|
|
58
|
+
const entry = history.state?.dirwellCrosswave;
|
|
59
|
+
return entry?.owner === owner && entry.index === index && entry.route.path === current.path;
|
|
60
|
+
};
|
|
61
|
+
const state = (route = current, position = index, view = getState()) => ({
|
|
62
|
+
...history.state,
|
|
63
|
+
dirwellCrosswave: { owner, route, index: position, view },
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
history.replaceState(state(), "");
|
|
67
|
+
} catch {
|
|
68
|
+
return;
|
|
69
|
+
} // Restricted hosts retain ordinary links and native transitions.
|
|
70
|
+
|
|
71
|
+
const stopAnimations = () => {
|
|
72
|
+
for (const { animation, layer } of animations) {
|
|
73
|
+
animation.cancel();
|
|
74
|
+
layer?.remove();
|
|
75
|
+
}
|
|
76
|
+
animations = [];
|
|
77
|
+
};
|
|
78
|
+
const show = (text, actions = []) => {
|
|
79
|
+
notice.replaceChildren(document.createTextNode(text));
|
|
80
|
+
for (const { label, run, href } of actions) {
|
|
81
|
+
const action = document.createElement(href ? "a" : "button");
|
|
82
|
+
action.textContent = label;
|
|
83
|
+
if (href) action.href = href;
|
|
84
|
+
else {
|
|
85
|
+
action.type = "button";
|
|
86
|
+
action.addEventListener("click", run);
|
|
87
|
+
}
|
|
88
|
+
notice.append(action);
|
|
89
|
+
}
|
|
90
|
+
notice.hidden = false;
|
|
91
|
+
};
|
|
92
|
+
const load = (route, signal) =>
|
|
93
|
+
new Promise((resolve, reject) => {
|
|
94
|
+
const asset = new URL(route.asset);
|
|
95
|
+
if (
|
|
96
|
+
!allowed(asset) ||
|
|
97
|
+
!/^crosswave-page-[a-f0-9]{24}\.js$/.test(asset.pathname.split("/").at(-1))
|
|
98
|
+
) {
|
|
99
|
+
reject(new Error("Unsupported folder data"));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const script = document.createElement("script");
|
|
103
|
+
script.async = true;
|
|
104
|
+
// Revalidate on every visit: a watcher may have rebuilt this directory.
|
|
105
|
+
asset.searchParams.set("cw-visit", String(Date.now()));
|
|
106
|
+
script.src = asset.href;
|
|
107
|
+
let payload;
|
|
108
|
+
const received = (event) => {
|
|
109
|
+
if (document.currentScript !== script) return;
|
|
110
|
+
const data = event.detail;
|
|
111
|
+
if (
|
|
112
|
+
data?.version === 1 &&
|
|
113
|
+
data.id === asset.pathname.split("/").at(-1) &&
|
|
114
|
+
data.path === route.path &&
|
|
115
|
+
typeof data.html === "string" &&
|
|
116
|
+
typeof data.outputName === "string" &&
|
|
117
|
+
(data.baseHref === null || typeof data.baseHref === "string")
|
|
118
|
+
)
|
|
119
|
+
payload = data;
|
|
120
|
+
};
|
|
121
|
+
const cleanup = () => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
removeEventListener("dirwell:crosswave-page", received);
|
|
124
|
+
signal.removeEventListener("abort", aborted);
|
|
125
|
+
script.remove();
|
|
126
|
+
script.onload = script.onerror = null;
|
|
127
|
+
};
|
|
128
|
+
const fail = (error) => {
|
|
129
|
+
cleanup();
|
|
130
|
+
reject(error);
|
|
131
|
+
};
|
|
132
|
+
const aborted = () => fail(new DOMException("Navigation superseded", "AbortError"));
|
|
133
|
+
const timer = setTimeout(() => fail(new Error("Folder loading timed out")), 8000);
|
|
134
|
+
script.onerror = () => fail(new Error("Folder data is unavailable"));
|
|
135
|
+
script.onload = () => {
|
|
136
|
+
cleanup();
|
|
137
|
+
try {
|
|
138
|
+
if (!payload) {
|
|
139
|
+
reject(new Error("Folder data is incompatible"));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const doc = new DOMParser().parseFromString(payload.html, "text/html");
|
|
143
|
+
const required = [
|
|
144
|
+
"[data-cw-root]",
|
|
145
|
+
".cw-files",
|
|
146
|
+
".cw-directory",
|
|
147
|
+
".cw-location",
|
|
148
|
+
".cw-stage > h1",
|
|
149
|
+
".cw-directory > :first-child",
|
|
150
|
+
".cw-empty",
|
|
151
|
+
];
|
|
152
|
+
if (required.some((selector) => !doc.querySelector(selector))) {
|
|
153
|
+
reject(new Error("Folder data is incomplete"));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (
|
|
157
|
+
doc.querySelector("[data-cw-root]").dataset.cwCategoryConfig !==
|
|
158
|
+
main.dataset.cwCategoryConfig
|
|
159
|
+
)
|
|
160
|
+
throw new Error("Folder category configuration changed; open normally to reload");
|
|
161
|
+
const url = new URL(route.url);
|
|
162
|
+
if (local && url.pathname.endsWith("/")) url.pathname += payload.outputName;
|
|
163
|
+
const base = new URL(payload.baseHref ?? ".", url);
|
|
164
|
+
absoluteLinks(doc, base);
|
|
165
|
+
resolve({ doc, route: { ...route, url: url.href } });
|
|
166
|
+
} catch (error) {
|
|
167
|
+
reject(error);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
addEventListener("dirwell:crosswave-page", received);
|
|
171
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
172
|
+
try {
|
|
173
|
+
document.head.append(script);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
fail(error);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const address = (route) => {
|
|
179
|
+
if (!local) return route.url;
|
|
180
|
+
const url = new URL(initialUrl);
|
|
181
|
+
const relative = (value) => new URL(value).pathname.slice(scopePath.length);
|
|
182
|
+
url.hash = `cw=${encodeURIComponent(relative(route.url))}&cw-data=${encodeURIComponent(relative(route.asset))}&cw-path=${encodeURIComponent(route.path)}`;
|
|
183
|
+
return url.href;
|
|
184
|
+
};
|
|
185
|
+
const snapshot = (element) => {
|
|
186
|
+
const rect = element.getBoundingClientRect();
|
|
187
|
+
const viewportBottom = Math.min(
|
|
188
|
+
innerHeight,
|
|
189
|
+
document.querySelector(".cw-bottom").getBoundingClientRect().top,
|
|
190
|
+
);
|
|
191
|
+
const clone = element.cloneNode(false);
|
|
192
|
+
if (element.matches(".cw-browser")) {
|
|
193
|
+
clone.append(element.querySelector(".cw-list-head").cloneNode(true));
|
|
194
|
+
const source = element.querySelector(".cw-list-scroll");
|
|
195
|
+
const scroll = source.cloneNode(false),
|
|
196
|
+
list = element.querySelector(".cw-files").cloneNode(false);
|
|
197
|
+
const filesRect = element.querySelector(".cw-files").getBoundingClientRect();
|
|
198
|
+
const sourceRect = source.getBoundingClientRect();
|
|
199
|
+
const visibleTop = Math.max(0, sourceRect.top);
|
|
200
|
+
const visibleBottom = Math.min(viewportBottom, sourceRect.bottom);
|
|
201
|
+
list.style.cssText = `position:relative;height:${source.scrollHeight}px`;
|
|
202
|
+
// Clone visible rows only; large directories must not duplicate their entire DOM for motion.
|
|
203
|
+
for (const row of element.querySelectorAll(".cw-row:not([hidden])")) {
|
|
204
|
+
const rowRect = row.getBoundingClientRect();
|
|
205
|
+
if (rowRect.bottom < visibleTop) continue;
|
|
206
|
+
if (rowRect.top > visibleBottom) break;
|
|
207
|
+
const copy = row.cloneNode(true);
|
|
208
|
+
copy.style.cssText = `position:absolute;left:0;right:0;top:${rowRect.top - filesRect.top}px;width:100%`;
|
|
209
|
+
list.append(copy);
|
|
210
|
+
}
|
|
211
|
+
scroll.append(list, element.querySelector(".cw-empty").cloneNode(true));
|
|
212
|
+
clone.append(scroll);
|
|
213
|
+
clone._scroll = { element: scroll, top: source.scrollTop };
|
|
214
|
+
} else clone.replaceChildren(...[...element.childNodes].map((node) => node.cloneNode(true)));
|
|
215
|
+
for (const node of [clone, ...clone.querySelectorAll("*")]) {
|
|
216
|
+
for (const name of node.getAttributeNames())
|
|
217
|
+
if (name.startsWith("data-cw-")) node.removeAttribute(name);
|
|
218
|
+
}
|
|
219
|
+
clone.removeAttribute("id");
|
|
220
|
+
clone.querySelectorAll("[id]").forEach((node) => node.removeAttribute("id"));
|
|
221
|
+
clone.setAttribute("aria-hidden", "true");
|
|
222
|
+
clone.inert = true;
|
|
223
|
+
clone.classList.add("cw-transition-layer");
|
|
224
|
+
clone.style.cssText = `position:fixed;left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;height:${rect.height}px;margin:0;z-index:3;pointer-events:none;view-transition-name:none`;
|
|
225
|
+
clone.style.overflow = "hidden";
|
|
226
|
+
clone.style.clipPath = `inset(${Math.max(0, -rect.top)}px 0 ${Math.max(0, rect.bottom - viewportBottom)}px 0)`;
|
|
227
|
+
document.body.append(clone);
|
|
228
|
+
if (clone._scroll) clone._scroll.element.scrollTop = clone._scroll.top;
|
|
229
|
+
return clone;
|
|
230
|
+
};
|
|
231
|
+
const exchange = (loaded, view, direction, writeHistory, position) => {
|
|
232
|
+
const moving =
|
|
233
|
+
!reduced.matches &&
|
|
234
|
+
document.documentElement.dataset.cwTransitions !== "false" &&
|
|
235
|
+
typeof main.animate === "function";
|
|
236
|
+
stopAnimations();
|
|
237
|
+
const elements = [
|
|
238
|
+
document.querySelector(".cw-browser"),
|
|
239
|
+
document.querySelector(".cw-detail"),
|
|
240
|
+
];
|
|
241
|
+
const layers = moving
|
|
242
|
+
? elements.map((element) =>
|
|
243
|
+
element.hidden || !element.getBoundingClientRect().height ? null : snapshot(element),
|
|
244
|
+
)
|
|
245
|
+
: [];
|
|
246
|
+
try {
|
|
247
|
+
if (writeHistory) {
|
|
248
|
+
// Save the view at departure, including edits made while a request was pending.
|
|
249
|
+
if (ownsCurrentEntry()) history.replaceState(state(), "");
|
|
250
|
+
history.pushState(state(loaded.route, position, view ?? {}), "", address(loaded.route));
|
|
251
|
+
for (const key of historyViews.keys())
|
|
252
|
+
if (key > position || key < position - history.length + 1) historyViews.delete(key);
|
|
253
|
+
}
|
|
254
|
+
commit(loaded.doc, view);
|
|
255
|
+
current = loaded.route;
|
|
256
|
+
index = position;
|
|
257
|
+
historyViews.set(index, { path: current.path, view: getState() });
|
|
258
|
+
try {
|
|
259
|
+
history.replaceState(state(), "");
|
|
260
|
+
} catch {}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
layers.forEach((layer) => layer?.remove());
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
if (!moving) return;
|
|
266
|
+
try {
|
|
267
|
+
elements.forEach((element, i) => {
|
|
268
|
+
const layer = layers[i];
|
|
269
|
+
if (layer) {
|
|
270
|
+
const animation = layer.animate(
|
|
271
|
+
[
|
|
272
|
+
{ opacity: 1, transform: "translateX(0)" },
|
|
273
|
+
{ opacity: 0, transform: `translateX(${-direction * 24}px)` },
|
|
274
|
+
],
|
|
275
|
+
{ duration: 140, easing: "ease-out" },
|
|
276
|
+
);
|
|
277
|
+
animations.push({ animation, layer });
|
|
278
|
+
animation.finished.then(
|
|
279
|
+
() => layer.remove(),
|
|
280
|
+
() => layer.remove(),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (!element.hidden) {
|
|
284
|
+
const animation = element.animate(
|
|
285
|
+
[
|
|
286
|
+
{ opacity: 0.25, transform: `translateX(${direction * 28}px)` },
|
|
287
|
+
{ opacity: 1, transform: "translateX(0)" },
|
|
288
|
+
],
|
|
289
|
+
{ duration: 240, easing: "cubic-bezier(.16,1,.3,1)" },
|
|
290
|
+
);
|
|
291
|
+
animations.push({ animation });
|
|
292
|
+
animation.finished.catch(() => {});
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
} catch {
|
|
296
|
+
stopAnimations();
|
|
297
|
+
layers.forEach((layer) => layer?.remove());
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const navigate = async (route, { view, pop = false, position = index + 1 } = {}) => {
|
|
301
|
+
historyViews.set(index, { path: current.path, view: getState() });
|
|
302
|
+
if (pop && historyViews.get(position)?.path === route.path)
|
|
303
|
+
view = historyViews.get(position).view;
|
|
304
|
+
const ticket = ++request;
|
|
305
|
+
pending?.abort();
|
|
306
|
+
const controller = new AbortController();
|
|
307
|
+
pending = controller;
|
|
308
|
+
if (!pop) {
|
|
309
|
+
const entry = history.state?.dirwellCrosswave;
|
|
310
|
+
position =
|
|
311
|
+
(entry?.owner === owner && Number.isInteger(entry.index) ? entry.index : index) + 1;
|
|
312
|
+
recent.delete(current.path);
|
|
313
|
+
recent.set(current.path, getState());
|
|
314
|
+
if (recent.size > 32) recent.delete(recent.keys().next().value);
|
|
315
|
+
if (view === undefined) view = recent.get(route.path);
|
|
316
|
+
try {
|
|
317
|
+
if (ownsCurrentEntry()) history.replaceState(state(), "");
|
|
318
|
+
} catch {
|
|
319
|
+
location.assign(route.url);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const direction = pop
|
|
324
|
+
? Math.sign(position - index) || 1
|
|
325
|
+
: current.path && (current.path.startsWith(`${route.path}/`) || route.path === "")
|
|
326
|
+
? -1
|
|
327
|
+
: 1;
|
|
328
|
+
main.setAttribute("aria-busy", "true");
|
|
329
|
+
const delay = setTimeout(() => {
|
|
330
|
+
if (ticket === request)
|
|
331
|
+
show("Loading folder…", [{ label: "Cancel", run: () => controller.abort() }]);
|
|
332
|
+
}, 120);
|
|
333
|
+
try {
|
|
334
|
+
const loaded = await load(route, controller.signal);
|
|
335
|
+
if (ticket !== request || controller.signal.aborted) return;
|
|
336
|
+
exchange(loaded, view, direction, !pop, position);
|
|
337
|
+
notice.hidden = true;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (ticket !== request) return;
|
|
340
|
+
if (pop || !ownsCurrentEntry()) {
|
|
341
|
+
// The browser has already changed its address: reload that exact destination on failure.
|
|
342
|
+
if (local) location.replace(route.url);
|
|
343
|
+
else location.reload();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (error.name === "AbortError") {
|
|
347
|
+
notice.hidden = true;
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
show("Could not open this folder. Your current files are still available.", [
|
|
351
|
+
{ label: "Retry", run: () => navigate(route, { view }) },
|
|
352
|
+
{ label: "Open normally", href: route.url },
|
|
353
|
+
]);
|
|
354
|
+
} finally {
|
|
355
|
+
clearTimeout(delay);
|
|
356
|
+
if (ticket === request) {
|
|
357
|
+
main.removeAttribute("aria-busy");
|
|
358
|
+
pending = null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
document.addEventListener("click", (event) => {
|
|
363
|
+
if (event.target.closest?.(".cw-skip")) {
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
const panel = document.querySelector("#cw-panel");
|
|
366
|
+
panel.tabIndex = -1;
|
|
367
|
+
panel.focus({ preventScroll: true });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const link = event.target.closest?.("a[data-cw-navigation][data-cw-page]");
|
|
371
|
+
if (
|
|
372
|
+
!link ||
|
|
373
|
+
event.defaultPrevented ||
|
|
374
|
+
event.button !== 0 ||
|
|
375
|
+
event.metaKey ||
|
|
376
|
+
event.ctrlKey ||
|
|
377
|
+
event.shiftKey ||
|
|
378
|
+
event.altKey ||
|
|
379
|
+
link.target ||
|
|
380
|
+
link.hasAttribute("download")
|
|
381
|
+
)
|
|
382
|
+
return;
|
|
383
|
+
const url = new URL(link.href),
|
|
384
|
+
asset = new URL(link.dataset.cwPage);
|
|
385
|
+
if (!allowed(url) || !allowed(asset) || url.hash) return;
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
if (link.dataset.cwPath === current.path) {
|
|
388
|
+
pending?.abort();
|
|
389
|
+
notice.hidden = true;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
navigate({ url: url.href, asset: asset.href, path: link.dataset.cwPath });
|
|
393
|
+
});
|
|
394
|
+
document.addEventListener("keydown", (event) => {
|
|
395
|
+
if (
|
|
396
|
+
event.key === "Escape" &&
|
|
397
|
+
!event.isComposing &&
|
|
398
|
+
!event.altKey &&
|
|
399
|
+
!event.ctrlKey &&
|
|
400
|
+
!event.metaKey &&
|
|
401
|
+
!event.target.matches?.("input,textarea,select,[contenteditable]")
|
|
402
|
+
) {
|
|
403
|
+
pending?.abort();
|
|
404
|
+
notice.hidden = true;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
addEventListener("popstate", (event) => {
|
|
408
|
+
const entry = event.state?.dirwellCrosswave;
|
|
409
|
+
if (entry?.owner !== owner || !allowed(new URL(entry.route.url))) {
|
|
410
|
+
location.reload();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
navigate(entry.route, { view: entry.view, pop: true, position: entry.index });
|
|
414
|
+
});
|
|
415
|
+
addEventListener("pagehide", () => {
|
|
416
|
+
++request;
|
|
417
|
+
pending?.abort();
|
|
418
|
+
main.removeAttribute("aria-busy");
|
|
419
|
+
notice.hidden = true;
|
|
420
|
+
stopAnimations();
|
|
421
|
+
});
|
|
422
|
+
reduced.addEventListener("change", () => {
|
|
423
|
+
if (reduced.matches) stopAnimations();
|
|
424
|
+
});
|
|
425
|
+
if (local && initialUrl.href !== location.href) {
|
|
426
|
+
const hash = new URLSearchParams(location.hash.slice(1));
|
|
427
|
+
if (hash.has("cw") && hash.has("cw-data") && hash.has("cw-path")) {
|
|
428
|
+
const url = new URL(hash.get("cw"), rootUrl),
|
|
429
|
+
asset = new URL(hash.get("cw-data"), rootUrl);
|
|
430
|
+
if (allowed(url) && allowed(asset))
|
|
431
|
+
navigate(
|
|
432
|
+
{ url: url.href, asset: asset.href, path: hash.get("cw-path") },
|
|
433
|
+
{
|
|
434
|
+
pop: true,
|
|
435
|
+
position: index,
|
|
436
|
+
view: savedEntry?.owner === owner ? savedEntry.view : undefined,
|
|
437
|
+
},
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
cancel() {
|
|
443
|
+
if (!pending) return false;
|
|
444
|
+
pending.abort();
|
|
445
|
+
notice.hidden = true;
|
|
446
|
+
return true;
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
15
451
|
document.addEventListener("DOMContentLoaded", () => {
|
|
16
452
|
const main = document.querySelector("[data-cw-root]");
|
|
17
453
|
if (!main) return;
|
|
@@ -40,22 +476,20 @@
|
|
|
40
476
|
const savedColor = read("color");
|
|
41
477
|
if (Object.hasOwn(colors, savedColor)) root.dataset.cwColor = savedColor;
|
|
42
478
|
let paused = root.dataset.cwMotion === "false" || read("paused") === "true";
|
|
43
|
-
|
|
479
|
+
let rows = [...document.querySelectorAll("[data-cw-entry]")];
|
|
44
480
|
const tabs = [...document.querySelectorAll("[data-cw-category]")];
|
|
45
481
|
const search = document.querySelector("#cw-search");
|
|
46
482
|
const clear = document.querySelector("[data-cw-clear]");
|
|
47
483
|
const empty = document.querySelector("[data-cw-empty]");
|
|
48
484
|
const status = document.querySelector("[data-cw-status]");
|
|
49
485
|
const detail = document.querySelector(".cw-detail");
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
if (linkIcon) icons.set("link", linkIcon);
|
|
53
|
-
let category = "all",
|
|
486
|
+
const belongs = (row, id) => row.dataset.cwCategories.split(" ").includes(id);
|
|
487
|
+
let category = tabs[0].dataset.cwCategory,
|
|
54
488
|
selected = null,
|
|
55
489
|
composing = false,
|
|
56
490
|
searchTimer;
|
|
57
491
|
const visible = () => rows.filter((row) => !row.hidden);
|
|
58
|
-
const select = (row, focus = false) => {
|
|
492
|
+
const select = (row, focus = false, reveal = true) => {
|
|
59
493
|
if (selected) selected.classList.remove("cw-selected");
|
|
60
494
|
selected = row;
|
|
61
495
|
if (!row) {
|
|
@@ -74,7 +508,7 @@
|
|
|
74
508
|
document.querySelector(`[data-cw-detail-${field}]`).textContent = row.dataset[data];
|
|
75
509
|
const host = document.querySelector("[data-cw-detail-icon]");
|
|
76
510
|
host.replaceChildren();
|
|
77
|
-
const icon =
|
|
511
|
+
const icon = row.querySelector(".cw-entry-icon svg");
|
|
78
512
|
if (icon) host.append(icon.cloneNode(true));
|
|
79
513
|
const link = row.querySelector("a");
|
|
80
514
|
const open = document.querySelector("[data-cw-open]");
|
|
@@ -90,16 +524,24 @@
|
|
|
90
524
|
open.target = "_blank";
|
|
91
525
|
open.rel = "noopener";
|
|
92
526
|
open.removeAttribute("data-cw-navigation");
|
|
527
|
+
delete open.dataset.cwPage;
|
|
528
|
+
delete open.dataset.cwPath;
|
|
93
529
|
} else {
|
|
94
530
|
open.removeAttribute("target");
|
|
95
531
|
open.removeAttribute("rel");
|
|
96
532
|
open.setAttribute("data-cw-navigation", "");
|
|
533
|
+
open.dataset.cwPage = link.dataset.cwPage;
|
|
534
|
+
open.dataset.cwPath = link.dataset.cwPath;
|
|
97
535
|
}
|
|
98
536
|
}
|
|
99
537
|
detail.hidden = false;
|
|
100
538
|
if (focus) {
|
|
101
539
|
link?.focus({ preventScroll: true });
|
|
102
|
-
|
|
540
|
+
if (reveal)
|
|
541
|
+
row.scrollIntoView({
|
|
542
|
+
block: "nearest",
|
|
543
|
+
behavior: reduced.matches ? "instant" : "smooth",
|
|
544
|
+
});
|
|
103
545
|
}
|
|
104
546
|
};
|
|
105
547
|
const applyFilter = () => {
|
|
@@ -107,7 +549,7 @@
|
|
|
107
549
|
let count = 0;
|
|
108
550
|
for (const row of rows) {
|
|
109
551
|
row.hidden =
|
|
110
|
-
(
|
|
552
|
+
!belongs(row, category) ||
|
|
111
553
|
!`${row.dataset.name} ${row.dataset.path}`.toLocaleLowerCase().includes(query);
|
|
112
554
|
if (!row.hidden) count++;
|
|
113
555
|
}
|
|
@@ -123,7 +565,7 @@
|
|
|
123
565
|
clear.hidden = !search.value;
|
|
124
566
|
if (!selected || selected.hidden) select(visible()[0] ?? null);
|
|
125
567
|
};
|
|
126
|
-
const setCategory = (next, focus = false) => {
|
|
568
|
+
const setCategory = (next, focus = false, animate = true) => {
|
|
127
569
|
category = next;
|
|
128
570
|
for (const tab of tabs) {
|
|
129
571
|
const active = tab.dataset.cwCategory === next;
|
|
@@ -146,9 +588,11 @@
|
|
|
146
588
|
selected = null;
|
|
147
589
|
rows.forEach((row) => row.classList.remove("cw-selected"));
|
|
148
590
|
applyFilter();
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
591
|
+
if (animate) {
|
|
592
|
+
main.classList.remove("cw-category-changing");
|
|
593
|
+
void main.offsetWidth;
|
|
594
|
+
main.classList.add("cw-category-changing");
|
|
595
|
+
}
|
|
152
596
|
};
|
|
153
597
|
const moveCategory = (direction) => {
|
|
154
598
|
const index = tabs.findIndex((tab) => tab.dataset.cwCategory === category);
|
|
@@ -163,18 +607,17 @@
|
|
|
163
607
|
};
|
|
164
608
|
for (const tab of tabs) {
|
|
165
609
|
tab.addEventListener("click", () => setCategory(tab.dataset.cwCategory));
|
|
166
|
-
const count =
|
|
167
|
-
tab.dataset.cwCategory === "all"
|
|
168
|
-
? rows.length
|
|
169
|
-
: rows.filter((row) => row.dataset.category === tab.dataset.cwCategory).length;
|
|
610
|
+
const count = rows.filter((row) => belongs(row, tab.dataset.cwCategory)).length;
|
|
170
611
|
tab.querySelector("small").textContent = String(count);
|
|
171
612
|
}
|
|
172
|
-
|
|
173
|
-
row.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
613
|
+
main.addEventListener("pointerover", (event) => {
|
|
614
|
+
const row = event.target.closest?.("[data-cw-entry]");
|
|
615
|
+
if (row && event.pointerType !== "touch") select(row);
|
|
616
|
+
});
|
|
617
|
+
main.addEventListener("focusin", (event) => {
|
|
618
|
+
const row = event.target.closest?.("[data-cw-entry]");
|
|
619
|
+
if (row) select(row);
|
|
620
|
+
});
|
|
178
621
|
search.addEventListener("compositionstart", () => {
|
|
179
622
|
composing = true;
|
|
180
623
|
clearTimeout(searchTimer);
|
|
@@ -215,7 +658,10 @@
|
|
|
215
658
|
"Your browser blocked the new tab. Press Enter or click the file to open it.";
|
|
216
659
|
}
|
|
217
660
|
};
|
|
218
|
-
|
|
661
|
+
let navigation;
|
|
662
|
+
const back = () => {
|
|
663
|
+
if (!navigation?.cancel()) document.querySelector("[data-cw-parent]")?.click();
|
|
664
|
+
};
|
|
219
665
|
const editing = () =>
|
|
220
666
|
document.activeElement?.matches("input,textarea,select,[contenteditable=true]");
|
|
221
667
|
document.addEventListener("keydown", (event) => {
|
|
@@ -317,7 +763,7 @@
|
|
|
317
763
|
document.querySelector("#cw-wave").addEventListener("cw-wave-fallback", updateMotion);
|
|
318
764
|
reduced.addEventListener("change", updateMotion);
|
|
319
765
|
updateMotion();
|
|
320
|
-
setCategory(
|
|
766
|
+
setCategory(tabs[0].dataset.cwCategory);
|
|
321
767
|
const scroll = document.querySelector(".cw-list-scroll");
|
|
322
768
|
const fitList = () => {
|
|
323
769
|
const footer = document.querySelector(".cw-bottom");
|
|
@@ -342,6 +788,58 @@
|
|
|
342
788
|
addEventListener("resize", fitList);
|
|
343
789
|
fitList();
|
|
344
790
|
|
|
791
|
+
navigation = createCrosswaveNavigator({
|
|
792
|
+
main,
|
|
793
|
+
reduced,
|
|
794
|
+
getState: () => ({
|
|
795
|
+
category,
|
|
796
|
+
query: search.value,
|
|
797
|
+
selected: selected?.dataset.path,
|
|
798
|
+
scroll: scroll.scrollTop,
|
|
799
|
+
}),
|
|
800
|
+
commit: (doc, view) => {
|
|
801
|
+
clearTimeout(searchTimer);
|
|
802
|
+
composing = false;
|
|
803
|
+
document.title = doc.title;
|
|
804
|
+
document
|
|
805
|
+
.querySelector(".cw-location")
|
|
806
|
+
.replaceChildren(...doc.querySelector(".cw-location").childNodes);
|
|
807
|
+
document.querySelector(".cw-stage > h1").textContent =
|
|
808
|
+
doc.querySelector(".cw-stage > h1").textContent;
|
|
809
|
+
document
|
|
810
|
+
.querySelector(".cw-directory")
|
|
811
|
+
.firstElementChild.replaceWith(doc.querySelector(".cw-directory").firstElementChild);
|
|
812
|
+
document
|
|
813
|
+
.querySelector(".cw-files")
|
|
814
|
+
.replaceChildren(...doc.querySelector(".cw-files").childNodes);
|
|
815
|
+
main.dataset.cwPath = doc.querySelector("[data-cw-root]").dataset.cwPath;
|
|
816
|
+
rows = [...main.querySelectorAll("[data-cw-entry]")];
|
|
817
|
+
for (const tab of tabs)
|
|
818
|
+
tab.querySelector("small").textContent = String(
|
|
819
|
+
rows.filter((row) => belongs(row, tab.dataset.cwCategory)).length,
|
|
820
|
+
);
|
|
821
|
+
search.value = view?.query ?? "";
|
|
822
|
+
setCategory(
|
|
823
|
+
tabs.some((tab) => tab.dataset.cwCategory === view?.category)
|
|
824
|
+
? view.category
|
|
825
|
+
: tabs[0].dataset.cwCategory,
|
|
826
|
+
false,
|
|
827
|
+
false,
|
|
828
|
+
);
|
|
829
|
+
main.classList.remove("cw-category-changing");
|
|
830
|
+
const row = rows.find((row) => !row.hidden && row.dataset.path === view?.selected);
|
|
831
|
+
select(row ?? visible()[0] ?? null, true, false);
|
|
832
|
+
if (!selected?.querySelector("a")) {
|
|
833
|
+
document.querySelector("#cw-panel").tabIndex = -1;
|
|
834
|
+
document.querySelector("#cw-panel").focus({ preventScroll: true });
|
|
835
|
+
}
|
|
836
|
+
scroll.scrollTop = view?.scroll ?? 0;
|
|
837
|
+
status.className = "cw-sr";
|
|
838
|
+
document.querySelector(".cw-skip").href = `${location.href.split("#")[0]}#cw-panel`;
|
|
839
|
+
// Persistent ResizeObservers update geometry after the browser lays out the new content.
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
|
|
345
843
|
// VueUse's useGamepad demonstrates event-driven connection and rAF polling.
|
|
346
844
|
// Read fresh standard-mapping snapshots; controls here are original and framework-free.
|
|
347
845
|
let padFrame = 0,
|