@protobase/ui 0.26.7 → 0.26.9
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/displays/card-layout.js +11 -1
- package/dist/app/displays/virtual-cards.js +5 -4
- package/dist/app/list-actions/row-record.js +15 -0
- package/dist/app/list-actions/use-list-actions.js +95 -0
- package/dist/app/list-messages.js +2 -0
- package/dist/app/list-page.js +29 -17
- package/dist/app/list-places.js +48 -0
- package/dist/app/list-scroll.js +31 -12
- package/dist/app/named-icon.js +20 -2
- package/dist/app/pages/action-href.js +5 -2
- package/dist/app/pages/action-prompt.js +3 -1
- package/dist/app/pages/action-when.js +14 -0
- package/dist/app/pages/blocks/action.js +5 -3
- package/dist/app/pages/messages.js +12 -0
- package/dist/app/pages/record-actions.js +4 -4
- package/dist/app/pages/run-each.js +24 -0
- package/dist/app/pages/use-run-action.js +52 -21
- package/dist/app/router.js +42 -13
- package/dist/data-grid/bulk-actions.js +12 -0
- package/dist/data-grid/data-grid.js +8 -8
- package/dist/data-grid/row-menu.js +27 -19
- package/dist/data-grid/use-virtual-rows.js +11 -8
- package/dist/package.js +1 -1
- package/dist/shell/assets/app-CKIJTg2U.js +69 -0
- package/dist/shell/assets/{index-D1-1uonk.js → index-nDDDN3FR.js} +1 -1
- package/dist/shell/assets/{records-map-CYm8mlKL.js → records-map-DG0dbm9k.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-DDDmYXGd.js → shared-protobase-ui-CvcWMNa3.js} +1 -1
- package/dist/shell/index.html +3 -3
- 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-CNDa8IHl.js +0 -69
|
@@ -5,7 +5,9 @@ import { useAdminMeta, usePermissions } from "../meta-gate.js";
|
|
|
5
5
|
import { useAppAddress } from "../use-app-address.js";
|
|
6
6
|
import { useProjectUi } from "./project-ui.js";
|
|
7
7
|
import { can } from "../permissions.js";
|
|
8
|
+
import { actionApplies } from "./action-when.js";
|
|
8
9
|
import { actionFor, linkTarget, openLink } from "./action-href.js";
|
|
10
|
+
import { runEach } from "./run-each.js";
|
|
9
11
|
import { useRefreshResource } from "./use-layout-data.js";
|
|
10
12
|
import { useState } from "react";
|
|
11
13
|
import { fillAddress, updateValues } from "@protobase/schema/internal";
|
|
@@ -14,9 +16,11 @@ const message = (error) => error instanceof Error ? error.message : text("someth
|
|
|
14
16
|
/**
|
|
15
17
|
* The named actions of `resource`'s view, ready to run on `scope` (the record they act on): built-in behaviour first
|
|
16
18
|
* (`update`, `delete`, `link`, and `ai`, which the server runs), else the handler the project registered under the
|
|
17
|
-
* action's name.
|
|
18
|
-
*
|
|
19
|
+
* action's name. One whose `when` does not hold for the record is left out, or blocked with its `reason`; an update
|
|
20
|
+
* whose values the record holds already is left out, and so is a link to a URL the record holds while it holds none.
|
|
21
|
+
* `blocked` says why one cannot run; `afterRun` hears about every action that succeeded.
|
|
19
22
|
* `submit` runs one with the answers to its inputs (a handler gets them, an update writes them) and throws its failure, for the dialog to show.
|
|
23
|
+
* `runEach` runs one on each of several records it applies to, as a list's selection does, and says how that went.
|
|
20
24
|
*/
|
|
21
25
|
const useActionRunner = (resource, scope, afterRun) => {
|
|
22
26
|
const { views } = useAdminMeta();
|
|
@@ -29,35 +33,41 @@ const useActionRunner = (resource, scope, afterRun) => {
|
|
|
29
33
|
const { go } = useAppAddress();
|
|
30
34
|
const [running, setRunning] = useState();
|
|
31
35
|
const actions = (views[resource]?.actions ?? []).flatMap((action) => actionFor(action, scope?.record) ?? []);
|
|
32
|
-
const
|
|
36
|
+
const blockedOn = (action, on) => {
|
|
33
37
|
const kind = action.run?.kind;
|
|
34
|
-
if ((kind === "update" || kind === "delete" || kind === "ai") && !
|
|
35
|
-
if (
|
|
36
|
-
if (
|
|
37
|
-
if (kind === "
|
|
38
|
+
if ((kind === "update" || kind === "delete" || kind === "ai") && !on) return text("needsRecord");
|
|
39
|
+
if (!actionApplies(action, on?.record)) return action.reason ?? text("doesNotApply");
|
|
40
|
+
if ((kind === "update" || kind === "ai") && (!can(permissions, "update") || on?.permissions?.update === false)) return text("cannotChange");
|
|
41
|
+
if (action.run?.kind === "ai" && on?.permissions?.fields?.[action.run.writes] === "read") return text("cannotChange");
|
|
42
|
+
if (kind === "delete" && (!can(permissions, "delete") || on?.permissions?.delete === false)) return text("cannotDelete");
|
|
38
43
|
if (!action.run && !handlers?.[action.name]) return text("noHandler", action.name);
|
|
39
44
|
const custom = action.inputs?.find((input) => input.kind === "custom" && !components?.[input.component ?? ""]);
|
|
40
45
|
if (custom) return text("noInputComponent", String(custom.component), custom.name);
|
|
41
46
|
};
|
|
42
|
-
const
|
|
47
|
+
const blocked = (action) => blockedOn(action, scope);
|
|
48
|
+
const execute = async (action, on, values) => {
|
|
43
49
|
const { run } = action;
|
|
44
50
|
if (run?.kind === "link") {
|
|
45
|
-
if ("to" in run) go(fillAddress(run.to,
|
|
46
|
-
else openLink(linkTarget(run.href,
|
|
51
|
+
if ("to" in run) go(fillAddress(run.to, on?.record));
|
|
52
|
+
else openLink(linkTarget(run.href, on?.record));
|
|
47
53
|
return;
|
|
48
54
|
}
|
|
49
|
-
if (run?.kind === "update") await client.update(resource,
|
|
50
|
-
else if (run?.kind === "delete") await client.remove(resource,
|
|
51
|
-
else if (run?.kind === "ai") await client.generate(resource,
|
|
55
|
+
if (run?.kind === "update") await client.update(resource, on.key, updateValues(run.values, values ?? {}), on.etag ?? "*");
|
|
56
|
+
else if (run?.kind === "delete") await client.remove(resource, on.key, { etag: on.etag });
|
|
57
|
+
else if (run?.kind === "ai") await client.generate(resource, on.key, action.name);
|
|
52
58
|
else await handlers[action.name]({
|
|
53
59
|
resource,
|
|
54
|
-
record:
|
|
55
|
-
recordKey:
|
|
56
|
-
etag:
|
|
60
|
+
record: on?.record,
|
|
61
|
+
recordKey: on?.key,
|
|
62
|
+
etag: on?.etag,
|
|
57
63
|
client: handlerClient,
|
|
58
64
|
navigate: go,
|
|
59
65
|
...values && { values }
|
|
60
66
|
});
|
|
67
|
+
};
|
|
68
|
+
const perform = async (action, on, values) => {
|
|
69
|
+
await execute(action, on, values);
|
|
70
|
+
if (action.run?.kind === "link") return;
|
|
61
71
|
await refresh(resource);
|
|
62
72
|
toast.show({
|
|
63
73
|
state: "success",
|
|
@@ -65,10 +75,10 @@ const useActionRunner = (resource, scope, afterRun) => {
|
|
|
65
75
|
});
|
|
66
76
|
afterRun?.(action);
|
|
67
77
|
};
|
|
68
|
-
const run = async (action) => {
|
|
69
|
-
if (
|
|
78
|
+
const run = async (action, on = scope) => {
|
|
79
|
+
if (blockedOn(action, on)) return;
|
|
70
80
|
setRunning(action.name);
|
|
71
|
-
await perform(action).catch((error) => toast.show({
|
|
81
|
+
await perform(action, on).catch((error) => toast.show({
|
|
72
82
|
state: "error",
|
|
73
83
|
title: text("failed", action.label),
|
|
74
84
|
description: message(error)
|
|
@@ -78,14 +88,35 @@ const useActionRunner = (resource, scope, afterRun) => {
|
|
|
78
88
|
const submit = async (action, values) => {
|
|
79
89
|
if (blocked(action)) return;
|
|
80
90
|
setRunning(action.name);
|
|
81
|
-
await perform(action, values).finally(() => setRunning(void 0));
|
|
91
|
+
await perform(action, scope, values).finally(() => setRunning(void 0));
|
|
92
|
+
};
|
|
93
|
+
/** Whether `action` shows and can run on `on`. */
|
|
94
|
+
const appliesOn = (action, on) => actionFor(action, on.record) !== void 0 && !blockedOn(action, on);
|
|
95
|
+
const runOnEach = async (action, records, skipped = 0) => {
|
|
96
|
+
setRunning(action.name);
|
|
97
|
+
const outcome = await runEach(records, (on) => appliesOn(action, on), (on) => execute(action, on));
|
|
98
|
+
setRunning(void 0);
|
|
99
|
+
await refresh(resource);
|
|
100
|
+
const title = text("doneOn", action.label, outcome.done, outcome.skipped + skipped, outcome.failed);
|
|
101
|
+
toast.show(outcome.failed > 0 ? {
|
|
102
|
+
state: "error",
|
|
103
|
+
title,
|
|
104
|
+
description: message(outcome.error)
|
|
105
|
+
} : {
|
|
106
|
+
state: "success",
|
|
107
|
+
title
|
|
108
|
+
});
|
|
109
|
+
if (outcome.done > 0) afterRun?.(action);
|
|
82
110
|
};
|
|
83
111
|
return {
|
|
84
112
|
actions,
|
|
85
113
|
blocked,
|
|
114
|
+
blockedOn,
|
|
115
|
+
appliesOn,
|
|
86
116
|
running,
|
|
87
117
|
run,
|
|
88
|
-
submit
|
|
118
|
+
submit,
|
|
119
|
+
runEach: runOnEach
|
|
89
120
|
};
|
|
90
121
|
};
|
|
91
122
|
/** One named action of `resource`'s view, for a button. */
|
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
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/data-grid/bulk-actions.ts
|
|
2
|
+
/** The actions offered for `selected`, each with the rows it runs on and how many it skips. */
|
|
3
|
+
const offeredBulkActions = (actions, selected) => actions.flatMap((action) => {
|
|
4
|
+
const rows = action.available ? selected.filter(action.available) : selected;
|
|
5
|
+
return rows.length === 0 ? [] : [{
|
|
6
|
+
action,
|
|
7
|
+
rows,
|
|
8
|
+
skipped: selected.length - rows.length
|
|
9
|
+
}];
|
|
10
|
+
});
|
|
11
|
+
//#endregion
|
|
12
|
+
export { offeredBulkActions };
|
|
@@ -2,6 +2,7 @@ import { cn } from "../lib/cn.js";
|
|
|
2
2
|
import { useIsDesktop } from "../lib/use-media-query.js";
|
|
3
3
|
import { RouteLink } from "../primitives/route-link.js";
|
|
4
4
|
import { buildColumns, features } from "./columns.js";
|
|
5
|
+
import { offeredBulkActions } from "./bulk-actions.js";
|
|
5
6
|
import { GridFooter } from "./grid-footer.js";
|
|
6
7
|
import { RowMenu, availableActions } from "./row-menu.js";
|
|
7
8
|
import { desktopMinScale, gridMinWidth, gutterWidthFor } from "./grid-template.js";
|
|
@@ -15,7 +16,7 @@ import { usePagedRows } from "./use-paged-rows.js";
|
|
|
15
16
|
import { useVirtualRows } from "./use-virtual-rows.js";
|
|
16
17
|
import { useCellEditing } from "./use-cell-editing.js";
|
|
17
18
|
import { ReorderRows, SortableRow } from "./row-reorder.js";
|
|
18
|
-
import { useCallback,
|
|
19
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
19
20
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
20
21
|
import { useTable } from "@tanstack/react-table";
|
|
21
22
|
//#region src/data-grid/data-grid.tsx
|
|
@@ -60,15 +61,14 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
60
61
|
total
|
|
61
62
|
]);
|
|
62
63
|
const [startIndex] = useState(initialScrollIndex);
|
|
63
|
-
const { ref,
|
|
64
|
+
const { ref, geometry, rows: virtualRows, first: firstIndex, last: lastIndex } = useVirtualRows({
|
|
64
65
|
total,
|
|
65
66
|
rowHeight,
|
|
66
67
|
initialIndex: startIndex,
|
|
67
|
-
inset: headerHeight
|
|
68
|
+
inset: headerHeight,
|
|
69
|
+
onScroll: onScrollIndex && ((offset) => onScrollIndex(offset / rowHeight))
|
|
68
70
|
});
|
|
69
71
|
const gutter = narrow || !rowNumbers ? 40 : gutterWidthFor(total);
|
|
70
|
-
const topIndex = Math.floor(Math.max(0, top) / rowHeight);
|
|
71
|
-
useEffect(() => onScrollIndex?.(topIndex), [topIndex, onScrollIndex]);
|
|
72
72
|
const getRow = usePagedRows({
|
|
73
73
|
total,
|
|
74
74
|
rowAt: sourceRowAt ? rowAt : void 0,
|
|
@@ -260,11 +260,11 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
260
260
|
total,
|
|
261
261
|
className: own ? void 0 : "sticky bottom-[var(--pb-sticky-bottom,0px)] z-20",
|
|
262
262
|
selectedCount,
|
|
263
|
-
bulkActions: bulkActions
|
|
264
|
-
label: action.label(
|
|
263
|
+
bulkActions: offeredBulkActions(bulkActions ?? [], Object.keys(rowSelection).flatMap((id) => selectedRows.current.get(id) ?? [])).map(({ action, rows, skipped }) => ({
|
|
264
|
+
label: action.label(rows.length),
|
|
265
265
|
destructive: action.destructive,
|
|
266
266
|
onRun: () => {
|
|
267
|
-
action.onRun(
|
|
267
|
+
action.onRun(rows, skipped);
|
|
268
268
|
setRowSelection({});
|
|
269
269
|
selectedRows.current.clear();
|
|
270
270
|
}
|
|
@@ -6,6 +6,7 @@ import { MoreHorizontal } from "lucide-react";
|
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { createPortal } from "react-dom";
|
|
8
8
|
//#region src/data-grid/row-menu.tsx
|
|
9
|
+
const labelOf = (action, row) => typeof action.label === "string" ? action.label : action.label(row);
|
|
9
10
|
/** The actions that apply to a row; a row with none gets no menu at all. */
|
|
10
11
|
const availableActions = (actions, row) => actions.filter((action) => action.available?.(row) ?? true);
|
|
11
12
|
/**
|
|
@@ -33,25 +34,32 @@ const RowMenu = ({ row, actions }) => {
|
|
|
33
34
|
onClick: (event) => event.stopPropagation(),
|
|
34
35
|
style: place ?? { visibility: "hidden" },
|
|
35
36
|
className: "fixed z-50 min-w-36 rounded-md border bg-background py-1 shadow-pop",
|
|
36
|
-
children: actions.map((action) =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
37
|
+
children: actions.map((action, index) => {
|
|
38
|
+
const label = labelOf(action, row);
|
|
39
|
+
const blocked = action.blocked?.(row);
|
|
40
|
+
const itemClass = cn("block w-full px-3 py-1.5 text-left hover:bg-muted disabled:cursor-default disabled:opacity-60 disabled:hover:bg-transparent", action.destructive && "text-danger-text");
|
|
41
|
+
return /* @__PURE__ */ jsx("li", {
|
|
42
|
+
role: "none",
|
|
43
|
+
children: action.link ? /* @__PURE__ */ jsx(RouteLink, {
|
|
44
|
+
role: "menuitem",
|
|
45
|
+
to: action.link(row),
|
|
46
|
+
onClick: () => setOpen(false),
|
|
47
|
+
className: itemClass,
|
|
48
|
+
children: label
|
|
49
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
50
|
+
type: "button",
|
|
51
|
+
role: "menuitem",
|
|
52
|
+
disabled: blocked !== void 0,
|
|
53
|
+
title: blocked,
|
|
54
|
+
className: itemClass,
|
|
55
|
+
onClick: () => {
|
|
56
|
+
setOpen(false);
|
|
57
|
+
action.onSelect(row);
|
|
58
|
+
},
|
|
59
|
+
children: label
|
|
60
|
+
})
|
|
61
|
+
}, index);
|
|
62
|
+
})
|
|
55
63
|
}), document.body)]
|
|
56
64
|
});
|
|
57
65
|
};
|
|
@@ -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