@protobase/ui 0.26.8 → 0.27.0
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/dist/app/app.d.ts +1 -1
- package/dist/app/app.js +1 -0
- package/dist/app/displays/card-layout.js +11 -1
- package/dist/app/displays/virtual-cards.js +5 -4
- package/dist/app/list-places.js +48 -0
- package/dist/app/list-scroll.js +31 -12
- package/dist/app/main.js +2 -1
- package/dist/app/page-api-origin.js +18 -0
- package/dist/app/router.js +42 -13
- package/dist/auth/auth-provider.js +9 -3
- package/dist/data-grid/data-grid.js +4 -5
- package/dist/data-grid/use-virtual-rows.js +11 -8
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-BD3RltKN.js +69 -0
- package/dist/shell/assets/index-B7TwOZvP.js +9 -0
- package/dist/shell/assets/{records-map-vV1WvSKW.js → records-map-BMBT0MNo.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-DD72f8yk.js → shared-protobase-ui-DGh3BpD8.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/dist/shell/protobase.sw.js +2 -2
- package/package.json +4 -4
- package/dist/shell/assets/app-cyYn6g2t.js +0 -69
- package/dist/shell/assets/index-BHDPRnNa.js +0 -9
package/dist/app/app.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type AppProps = {
|
|
|
12
12
|
client?: Client;
|
|
13
13
|
/** Ready-made clients of the sources `/meta` names, by their URL, for stories and tests. */
|
|
14
14
|
sources?: Record<string, Client>;
|
|
15
|
-
/** Sign-in state for the app; made from
|
|
15
|
+
/** Sign-in state for the app; made from `baseUrl`'s origin when omitted. */
|
|
16
16
|
auth?: AuthSession;
|
|
17
17
|
/** Path prefix the app is mounted under, for example `/admin`. */
|
|
18
18
|
basePath?: string;
|
package/dist/app/app.js
CHANGED
|
@@ -309,6 +309,7 @@ const App = ({ auth, workspace, basePath, initialUrl, pluginLoader, locale, ...r
|
|
|
309
309
|
setFallbackLocale(locale ?? "en");
|
|
310
310
|
return /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(AuthProvider, {
|
|
311
311
|
session: auth,
|
|
312
|
+
...rest.baseUrl && { origin: new URL(rest.baseUrl, window.location.href).origin },
|
|
312
313
|
children: /* @__PURE__ */ jsx(Router, {
|
|
313
314
|
basePath,
|
|
314
315
|
initialUrl,
|
|
@@ -15,5 +15,15 @@ const anchoredTop = (top, before, after) => {
|
|
|
15
15
|
if (before.columns === after.columns) return row * after.rowHeight;
|
|
16
16
|
return Math.floor(Math.floor(row) * before.columns / after.columns) * after.rowHeight;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Where the cards are at the offset `top`, as a list remembers it: the first record of the row at the top, plus how far
|
|
20
|
+
* into that row as a fraction of its height, so it comes back to the same records whatever the columns then.
|
|
21
|
+
*/
|
|
22
|
+
const cardPlace = (top, layout) => {
|
|
23
|
+
const row = top / layout.rowHeight;
|
|
24
|
+
return Math.floor(row) * layout.columns + row % 1;
|
|
25
|
+
};
|
|
26
|
+
/** The row, with how far into it, that a place `cardPlace` gave falls in with `columns` cards a row. */
|
|
27
|
+
const placeRow = (place, columns) => Math.floor(Math.floor(place) / columns) + place % 1;
|
|
18
28
|
//#endregion
|
|
19
|
-
export { anchoredTop, cardColumns, cardWidth, recordRange };
|
|
29
|
+
export { anchoredTop, cardColumns, cardPlace, cardWidth, placeRow, recordRange };
|
|
@@ -2,7 +2,7 @@ import { Skeleton } from "../../primitives/skeleton.js";
|
|
|
2
2
|
import { usePagedRows } from "../../data-grid/use-paged-rows.js";
|
|
3
3
|
import { useVirtualRows } from "../../data-grid/use-virtual-rows.js";
|
|
4
4
|
import { createFlipGuard } from "../../lib/flip-guard.js";
|
|
5
|
-
import { anchoredTop, cardColumns, cardWidth, recordRange } from "./card-layout.js";
|
|
5
|
+
import { anchoredTop, cardColumns, cardPlace, cardWidth, placeRow, recordRange } from "./card-layout.js";
|
|
6
6
|
import { withMeasuredHeight } from "./measured-heights.js";
|
|
7
7
|
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
8
8
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -33,12 +33,14 @@ const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, c
|
|
|
33
33
|
const changed = anchor.current.top > 0 && (anchor.current.layout.columns !== columns || anchor.current.layout.rowHeight !== rowHeight);
|
|
34
34
|
const settle = (top) => changed ? anchoredTop(anchor.current.top, anchor.current.layout, layout) : top;
|
|
35
35
|
const ready = itemWidth > 0 && measured.has(itemWidth);
|
|
36
|
+
const onScroll = (top) => onScrollIndex(cardPlace(top, layout));
|
|
36
37
|
const scroll = useVirtualRows({
|
|
37
38
|
total: Math.ceil(total / columns),
|
|
38
39
|
rowHeight,
|
|
39
|
-
initialIndex:
|
|
40
|
+
initialIndex: placeRow(startIndex, columns),
|
|
40
41
|
settle,
|
|
41
|
-
ready
|
|
42
|
+
ready,
|
|
43
|
+
onScroll
|
|
42
44
|
});
|
|
43
45
|
useLayoutEffect(() => {
|
|
44
46
|
if (changed) scroll.scrollTo(scroll.top);
|
|
@@ -46,7 +48,6 @@ const VirtualCards = ({ source, label, initialIndex, onScrollIndex, minHeight, c
|
|
|
46
48
|
layout,
|
|
47
49
|
top: scroll.top
|
|
48
50
|
};
|
|
49
|
-
if (!changed) onScrollIndex(Math.floor(Math.max(0, scroll.top) / rowHeight) * columns);
|
|
50
51
|
});
|
|
51
52
|
const range = recordRange(scroll.first, scroll.last, columns, total);
|
|
52
53
|
const getRow = usePagedRows({
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { guarded } from "../lib/guarded.js";
|
|
2
|
+
//#region src/app/list-places.ts
|
|
3
|
+
const storageKey = "protobase:list-places";
|
|
4
|
+
/** The places kept, the most recent ones, so the storage stays small however long the tab is used. */
|
|
5
|
+
const kept = 50;
|
|
6
|
+
const keyOf = (entry, url) => `${entry} ${url}`;
|
|
7
|
+
const isSaved = (value) => {
|
|
8
|
+
if (typeof value !== "object" || value === null) return false;
|
|
9
|
+
const { entry, url, place } = value;
|
|
10
|
+
return typeof entry === "string" && typeof url === "string" && typeof place === "number" && Number.isFinite(place) && place >= 0;
|
|
11
|
+
};
|
|
12
|
+
const read = (storage) => guarded(() => {
|
|
13
|
+
const raw = storage.getItem(storageKey);
|
|
14
|
+
if (raw === null) return [];
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
return Array.isArray(parsed) ? parsed.filter(isSaved) : [];
|
|
17
|
+
}, []);
|
|
18
|
+
/**
|
|
19
|
+
* The places of the lists of a tab, by history entry and list URL, kept in `storage` (the tab's session storage) when
|
|
20
|
+
* `persist` is called, so they outlast a reload of the page. A list starts where it was in its history entry, as on
|
|
21
|
+
* Back; in an entry it has no place in yet, as after a link to the same list, where it last was in any entry.
|
|
22
|
+
*/
|
|
23
|
+
const createListPlaces = (storage) => {
|
|
24
|
+
const places = new Map(read(storage).map((saved) => [keyOf(saved.entry, saved.url), saved]));
|
|
25
|
+
const placeOf = (entry, url) => {
|
|
26
|
+
const own = places.get(keyOf(entry, url));
|
|
27
|
+
if (own) return own.place;
|
|
28
|
+
return [...places.values()].findLast((saved) => saved.url === url)?.place ?? 0;
|
|
29
|
+
};
|
|
30
|
+
const save = (entry, url, place) => {
|
|
31
|
+
const key = keyOf(entry, url);
|
|
32
|
+
places.delete(key);
|
|
33
|
+
places.set(key, {
|
|
34
|
+
entry,
|
|
35
|
+
url,
|
|
36
|
+
place
|
|
37
|
+
});
|
|
38
|
+
for (const oldest of [...places.keys()].slice(0, Math.max(0, places.size - kept))) places.delete(oldest);
|
|
39
|
+
};
|
|
40
|
+
const persist = () => guarded(() => storage.setItem(storageKey, JSON.stringify([...places.values()])), void 0);
|
|
41
|
+
return {
|
|
42
|
+
placeOf,
|
|
43
|
+
save,
|
|
44
|
+
persist
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
export { createListPlaces };
|
package/dist/app/list-scroll.js
CHANGED
|
@@ -1,23 +1,42 @@
|
|
|
1
|
+
import { guarded } from "../lib/guarded.js";
|
|
1
2
|
import { useRouter } from "./router.js";
|
|
2
|
-
import {
|
|
3
|
+
import { createListPlaces } from "./list-places.js";
|
|
4
|
+
import { useCallback, useEffect } from "react";
|
|
3
5
|
//#region src/app/list-scroll.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
const noStorage = {
|
|
7
|
+
getItem: () => null,
|
|
8
|
+
setItem: () => {}
|
|
9
|
+
};
|
|
10
|
+
/** The places of this tab's lists, read from its session storage on first use. */
|
|
11
|
+
let places;
|
|
12
|
+
const tabPlaces = () => places ??= createListPlaces(guarded(() => window.sessionStorage, void 0) ?? noStorage);
|
|
6
13
|
/**
|
|
7
|
-
* Where a list starts and how it remembers where it is, so going back to it
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
14
|
+
* Where a list starts and how it remembers where it is, so going back to it, or following a link to it, shows the same
|
|
15
|
+
* records at the same height. After a change of sort or filter, a list of another URL, it starts at the top. The places
|
|
16
|
+
* are stored when the list goes and when the page is hidden or left, so a reload or a discarded tab keeps them.
|
|
10
17
|
*/
|
|
11
18
|
const useListScroll = () => {
|
|
12
19
|
const { entry, path, params } = useRouter();
|
|
13
20
|
const url = `${path}?${params.toString()}`;
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const tab = tabPlaces();
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const persist = () => tab.persist();
|
|
24
|
+
const hidden = () => document.visibilityState === "hidden" && tab.persist();
|
|
25
|
+
window.addEventListener("pagehide", persist);
|
|
26
|
+
document.addEventListener("visibilitychange", hidden);
|
|
27
|
+
return () => {
|
|
28
|
+
window.removeEventListener("pagehide", persist);
|
|
29
|
+
document.removeEventListener("visibilitychange", hidden);
|
|
30
|
+
tab.persist();
|
|
31
|
+
};
|
|
32
|
+
}, [tab]);
|
|
33
|
+
const remember = useCallback((place) => tab.save(entry, url, place), [
|
|
34
|
+
tab,
|
|
35
|
+
entry,
|
|
36
|
+
url
|
|
37
|
+
]);
|
|
19
38
|
return {
|
|
20
|
-
initialIndex:
|
|
39
|
+
initialIndex: tab.placeOf(entry, url),
|
|
21
40
|
remember
|
|
22
41
|
};
|
|
23
42
|
};
|
package/dist/app/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { browserLocale } from "../locale/active-locale.js";
|
|
2
2
|
import { uiFromModule } from "./pages/ui-from-module.js";
|
|
3
3
|
import { App } from "./app.js";
|
|
4
|
+
import { pageApiOrigin } from "./page-api-origin.js";
|
|
4
5
|
import { StrictMode } from "react";
|
|
5
6
|
import { jsx } from "react/jsx-runtime";
|
|
6
7
|
import { createRoot } from "react-dom/client";
|
|
@@ -10,7 +11,7 @@ import "../styles.css";
|
|
|
10
11
|
const root = document.getElementById("root");
|
|
11
12
|
if (!root) throw new Error("index.html needs an element with id \"root\"");
|
|
12
13
|
createRoot(root).render(/* @__PURE__ */ jsx(StrictMode, { children: /* @__PURE__ */ jsx(App, {
|
|
13
|
-
baseUrl: "/api/v1
|
|
14
|
+
baseUrl: `${pageApiOrigin() ?? ""}/api/v1`,
|
|
14
15
|
ui: uiFromModule(projectUi),
|
|
15
16
|
locale: browserLocale()
|
|
16
17
|
}) }));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { apiOriginMeta } from "@protobase/schema/internal";
|
|
2
|
+
//#region src/app/page-api-origin.ts
|
|
3
|
+
/**
|
|
4
|
+
* The API origin the page's `<meta name="protobase-api-origin">` tags name: `undefined` without one. Anything but one
|
|
5
|
+
* tag holding exactly an http or https origin, such as `https://shop.example.com`, is an error.
|
|
6
|
+
*/
|
|
7
|
+
const apiOriginOf = (contents) => {
|
|
8
|
+
if (contents.length === 0) return void 0;
|
|
9
|
+
if (contents.length > 1) throw new Error(`The page has ${contents.length} ${apiOriginMeta} meta tags; it may have one`);
|
|
10
|
+
const value = contents[0];
|
|
11
|
+
const url = URL.canParse(value) ? new URL(value) : void 0;
|
|
12
|
+
if (!url || url.protocol !== "http:" && url.protocol !== "https:" || url.origin !== value) throw new Error(`The page's ${apiOriginMeta} meta tag holds "${value}", which is not an http or https origin such as https://shop.example.com`);
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
15
|
+
/** The API origin this page names in its `<meta name="protobase-api-origin">` (see `apiOriginOf`). */
|
|
16
|
+
const pageApiOrigin = () => apiOriginOf([...document.querySelectorAll(`meta[name="${apiOriginMeta}"]`)].map((meta) => meta.content));
|
|
17
|
+
//#endregion
|
|
18
|
+
export { apiOriginOf, pageApiOrigin };
|
package/dist/app/router.js
CHANGED
|
@@ -13,14 +13,30 @@ const useRouter = () => {
|
|
|
13
13
|
};
|
|
14
14
|
const browserUrl = () => `${window.location.pathname}${window.location.search}`;
|
|
15
15
|
const pathOf = (url) => new URL(url, "http://local").pathname;
|
|
16
|
+
const historyState = () => window.history.state ?? {};
|
|
16
17
|
/** Our place in the browser history, so a back or forward press can be told apart and, if needed, undone. */
|
|
17
|
-
const historyIndex = () =>
|
|
18
|
+
const historyIndex = () => historyState().protobaseIndex ?? 0;
|
|
19
|
+
const newEntry = () => Math.random().toString(36).slice(2);
|
|
20
|
+
/** The id of the current history entry, given one if it has none yet, as an entry pushed before the app started. */
|
|
21
|
+
const historyEntry = () => {
|
|
22
|
+
const state = historyState();
|
|
23
|
+
if (state.protobaseEntry) return state.protobaseEntry;
|
|
24
|
+
const entry = newEntry();
|
|
25
|
+
window.history.replaceState({
|
|
26
|
+
...state,
|
|
27
|
+
protobaseEntry: entry
|
|
28
|
+
}, "");
|
|
29
|
+
return entry;
|
|
30
|
+
};
|
|
18
31
|
const Router = ({ basePath = "", initialUrl, children }) => {
|
|
19
32
|
const memory = initialUrl !== void 0;
|
|
20
|
-
const [
|
|
33
|
+
const [page, setPage] = useState(() => ({
|
|
34
|
+
url: initialUrl ?? browserUrl(),
|
|
35
|
+
entry: memory ? newEntry() : historyEntry()
|
|
36
|
+
}));
|
|
21
37
|
const guards = useRef(/* @__PURE__ */ new Set());
|
|
22
|
-
const
|
|
23
|
-
|
|
38
|
+
const current = useRef(page);
|
|
39
|
+
current.current = page;
|
|
24
40
|
const position = useRef(memory ? 0 : historyIndex());
|
|
25
41
|
const undoing = useRef(false);
|
|
26
42
|
const proceeding = useRef(false);
|
|
@@ -28,7 +44,10 @@ const Router = ({ basePath = "", initialUrl, children }) => {
|
|
|
28
44
|
const [pending, setPending] = useState();
|
|
29
45
|
useEffect(() => {
|
|
30
46
|
if (memory) return;
|
|
31
|
-
window.history.replaceState({
|
|
47
|
+
window.history.replaceState({
|
|
48
|
+
protobaseIndex: position.current,
|
|
49
|
+
protobaseEntry: current.current.entry
|
|
50
|
+
}, "");
|
|
32
51
|
const onPop = () => {
|
|
33
52
|
const next = historyIndex();
|
|
34
53
|
if (undoing.current) {
|
|
@@ -39,7 +58,7 @@ const Router = ({ basePath = "", initialUrl, children }) => {
|
|
|
39
58
|
return;
|
|
40
59
|
}
|
|
41
60
|
const delta = next - position.current;
|
|
42
|
-
const leaving = pathOf(browserUrl()) !== pathOf(
|
|
61
|
+
const leaving = pathOf(browserUrl()) !== pathOf(current.current.url);
|
|
43
62
|
if (!proceeding.current && guards.current.size > 0 && leaving) {
|
|
44
63
|
undoing.current = true;
|
|
45
64
|
window.history.go(-delta);
|
|
@@ -51,21 +70,31 @@ const Router = ({ basePath = "", initialUrl, children }) => {
|
|
|
51
70
|
}
|
|
52
71
|
proceeding.current = false;
|
|
53
72
|
position.current = next;
|
|
54
|
-
|
|
73
|
+
setPage({
|
|
74
|
+
url: browserUrl(),
|
|
75
|
+
entry: historyEntry()
|
|
76
|
+
});
|
|
55
77
|
};
|
|
56
78
|
window.addEventListener("popstate", onPop);
|
|
57
79
|
return () => window.removeEventListener("popstate", onPop);
|
|
58
80
|
}, [memory]);
|
|
59
81
|
const go = useCallback((full, replace) => {
|
|
82
|
+
const entry = replace ? current.current.entry : newEntry();
|
|
60
83
|
if (!memory) {
|
|
61
84
|
position.current += replace ? 0 : 1;
|
|
62
|
-
window.history[replace ? "replaceState" : "pushState"]({
|
|
85
|
+
window.history[replace ? "replaceState" : "pushState"]({
|
|
86
|
+
protobaseIndex: position.current,
|
|
87
|
+
protobaseEntry: entry
|
|
88
|
+
}, "", full);
|
|
63
89
|
}
|
|
64
|
-
|
|
90
|
+
setPage({
|
|
91
|
+
url: full,
|
|
92
|
+
entry
|
|
93
|
+
});
|
|
65
94
|
}, [memory]);
|
|
66
95
|
const navigate = useCallback((to, { replace = false } = {}) => {
|
|
67
96
|
const full = `${basePath}${to}`;
|
|
68
|
-
if (pathOf(full) !== pathOf(
|
|
97
|
+
if (pathOf(full) !== pathOf(current.current.url) && guards.current.size > 0) return setPending({
|
|
69
98
|
kind: "link",
|
|
70
99
|
full,
|
|
71
100
|
replace
|
|
@@ -86,17 +115,17 @@ const Router = ({ basePath = "", initialUrl, children }) => {
|
|
|
86
115
|
else window.history.go(pending.delta);
|
|
87
116
|
};
|
|
88
117
|
const value = useMemo(() => {
|
|
89
|
-
const parsed = new URL(url, "http://local");
|
|
118
|
+
const parsed = new URL(page.url, "http://local");
|
|
90
119
|
return {
|
|
91
120
|
path: parsed.pathname.slice(basePath.length) || "/",
|
|
92
121
|
params: parsed.searchParams,
|
|
93
122
|
basePath,
|
|
94
|
-
entry:
|
|
123
|
+
entry: page.entry,
|
|
95
124
|
navigate,
|
|
96
125
|
registerGuard
|
|
97
126
|
};
|
|
98
127
|
}, [
|
|
99
|
-
|
|
128
|
+
page,
|
|
100
129
|
basePath,
|
|
101
130
|
navigate,
|
|
102
131
|
registerGuard
|
|
@@ -10,8 +10,11 @@ const useAuth = () => {
|
|
|
10
10
|
if (!api) throw new Error("useAuth must be used inside AuthProvider");
|
|
11
11
|
return api;
|
|
12
12
|
};
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Finds out whether the server has a login and who is signed in, and keeps that state for the app. Without a `session`,
|
|
15
|
+
* it makes one on `origin`, the page's own by default.
|
|
16
|
+
*/
|
|
17
|
+
const AuthProvider = ({ session: given, origin, children }) => {
|
|
15
18
|
const [state, setState] = useState({ kind: "loading" });
|
|
16
19
|
const [signInMethods, setSignInMethods] = useState(["password"]);
|
|
17
20
|
const [passwordReset, setPasswordReset] = useState(false);
|
|
@@ -29,7 +32,10 @@ const AuthProvider = ({ session: given, children }) => {
|
|
|
29
32
|
const markSignedOut = useCallback(() => setState((current) => current.kind === "signed-in" ? { kind: "signed-out" } : current), []);
|
|
30
33
|
const signedOut = useRef(markSignedOut);
|
|
31
34
|
signedOut.current = markSignedOut;
|
|
32
|
-
const session = useMemo(() => given ?? createAuthSession({
|
|
35
|
+
const session = useMemo(() => given ?? createAuthSession({
|
|
36
|
+
...origin && { origin },
|
|
37
|
+
onSignedOut: () => signedOut.current()
|
|
38
|
+
}), [given, origin]);
|
|
33
39
|
const enter = useCallback(async (user) => {
|
|
34
40
|
const [{ missing }, staff] = await Promise.all([session.account.signInMethods(), session.staff.current()]);
|
|
35
41
|
setState(missing.length > 0 ? {
|
|
@@ -16,7 +16,7 @@ import { usePagedRows } from "./use-paged-rows.js";
|
|
|
16
16
|
import { useVirtualRows } from "./use-virtual-rows.js";
|
|
17
17
|
import { useCellEditing } from "./use-cell-editing.js";
|
|
18
18
|
import { ReorderRows, SortableRow } from "./row-reorder.js";
|
|
19
|
-
import { useCallback,
|
|
19
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
20
20
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
21
21
|
import { useTable } from "@tanstack/react-table";
|
|
22
22
|
//#region src/data-grid/data-grid.tsx
|
|
@@ -61,15 +61,14 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
61
61
|
total
|
|
62
62
|
]);
|
|
63
63
|
const [startIndex] = useState(initialScrollIndex);
|
|
64
|
-
const { ref,
|
|
64
|
+
const { ref, geometry, rows: virtualRows, first: firstIndex, last: lastIndex } = useVirtualRows({
|
|
65
65
|
total,
|
|
66
66
|
rowHeight,
|
|
67
67
|
initialIndex: startIndex,
|
|
68
|
-
inset: headerHeight
|
|
68
|
+
inset: headerHeight,
|
|
69
|
+
onScroll: onScrollIndex && ((offset) => onScrollIndex(offset / rowHeight))
|
|
69
70
|
});
|
|
70
71
|
const gutter = narrow || !rowNumbers ? 40 : gutterWidthFor(total);
|
|
71
|
-
const topIndex = Math.floor(Math.max(0, top) / rowHeight);
|
|
72
|
-
useEffect(() => onScrollIndex?.(topIndex), [topIndex, onScrollIndex]);
|
|
73
72
|
const getRow = usePagedRows({
|
|
74
73
|
total,
|
|
75
74
|
rowAt: sourceRowAt ? rowAt : void 0,
|
|
@@ -20,7 +20,7 @@ const viewRows = (view, total, rowHeight) => {
|
|
|
20
20
|
* wheel or touch: the list follows the scroll position, and draws again only when other rows come into view.
|
|
21
21
|
* `first` and `last` are the indexes of the drawn rows, overscan included.
|
|
22
22
|
*/
|
|
23
|
-
const useVirtualRows = ({ total, rowHeight, initialIndex, inset = 0, settle, ready = true }) => {
|
|
23
|
+
const useVirtualRows = ({ total, rowHeight, initialIndex, inset = 0, settle, ready = true, onScroll }) => {
|
|
24
24
|
const ref = useRef(null);
|
|
25
25
|
const scroller = useRef(null);
|
|
26
26
|
const [view, setView] = useState({
|
|
@@ -32,14 +32,16 @@ const useVirtualRows = ({ total, rowHeight, initialIndex, inset = 0, settle, rea
|
|
|
32
32
|
rowHeight,
|
|
33
33
|
initialIndex,
|
|
34
34
|
inset,
|
|
35
|
-
ready
|
|
35
|
+
ready,
|
|
36
|
+
onScroll
|
|
36
37
|
});
|
|
37
38
|
options.current = {
|
|
38
39
|
total,
|
|
39
40
|
rowHeight,
|
|
40
41
|
initialIndex,
|
|
41
42
|
inset,
|
|
42
|
-
ready
|
|
43
|
+
ready,
|
|
44
|
+
onScroll
|
|
43
45
|
};
|
|
44
46
|
const read = useCallback(() => {
|
|
45
47
|
const content = ref.current;
|
|
@@ -51,24 +53,25 @@ const useVirtualRows = ({ total, rowHeight, initialIndex, inset = 0, settle, rea
|
|
|
51
53
|
height: Math.max(0, area.height - hidden)
|
|
52
54
|
};
|
|
53
55
|
}, []);
|
|
56
|
+
const start = useRef({
|
|
57
|
+
pending: true,
|
|
58
|
+
laidOut: false
|
|
59
|
+
});
|
|
54
60
|
const shown = useRef(view);
|
|
55
61
|
shown.current = view;
|
|
56
62
|
const measure = useCallback(() => {
|
|
57
63
|
const next = read();
|
|
58
64
|
if (!next) return;
|
|
59
65
|
const current = shown.current;
|
|
60
|
-
const { total, rowHeight } = options.current;
|
|
66
|
+
const { total, rowHeight, onScroll } = options.current;
|
|
61
67
|
const before = viewRows(current, total, rowHeight);
|
|
62
68
|
const after = viewRows(next, total, rowHeight);
|
|
69
|
+
if (!start.current.pending) onScroll?.(Math.max(0, after.top));
|
|
63
70
|
const sameRows = !after.geometry.scaled && before.first === after.first && before.last === after.last && before.topIndex === after.topIndex;
|
|
64
71
|
if (current.height === next.height && (current.position === next.position || sameRows)) return;
|
|
65
72
|
shown.current = next;
|
|
66
73
|
setView(next);
|
|
67
74
|
}, [read]);
|
|
68
|
-
const start = useRef({
|
|
69
|
-
pending: true,
|
|
70
|
-
laidOut: false
|
|
71
|
-
});
|
|
72
75
|
const begin = useCallback(() => {
|
|
73
76
|
if (!start.current.pending || !start.current.laidOut || !options.current.ready) return;
|
|
74
77
|
const view = read();
|
package/dist/package.js
CHANGED