@protobase/ui 0.13.4 → 0.14.1
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/current-address.js +1 -1
- package/dist/app/custom-section.js +1 -1
- package/dist/app/displays/custom-display.js +1 -1
- package/dist/app/displays/list-cards.js +2 -2
- package/dist/app/displays/list-displays.js +1 -1
- package/dist/app/displays/virtual-cards.js +43 -65
- package/dist/app/error-reporting.js +1 -1
- package/dist/app/list-page.js +11 -3
- package/dist/app/nav-recent.js +2 -1
- package/dist/app/pages/blocks/link.js +2 -1
- package/dist/app/pages/render-node.js +1 -1
- package/dist/app/pages/use-layout-data.js +1 -1
- package/dist/app/pages/use-run-action.js +1 -1
- package/dist/app/search/search-model.js +2 -1
- package/dist/app/use-lines-draft.js +1 -1
- package/dist/app-shell/app-shell.js +54 -38
- package/dist/app-shell/bottom-bar.d.ts +1 -0
- package/dist/app-shell/bottom-bar.js +3 -2
- package/dist/app-shell/index.d.ts +1 -0
- package/dist/app-shell/page-scroll.d.ts +1 -0
- package/dist/app-shell/page-scroll.js +56 -0
- package/dist/app-shell/sidebar.js +2 -2
- package/dist/app-shell/top-bar.js +3 -2
- package/dist/assistant-dock/use-assistant.js +1 -1
- package/dist/data/use-record.js +1 -1
- package/dist/data/use-update-record.js +1 -1
- package/dist/data-grid/data-grid.js +92 -85
- package/dist/data-grid/geometry.js +26 -6
- package/dist/data-grid/grid-footer.js +5 -12
- package/dist/data-grid/scroll-parent.js +35 -0
- package/dist/data-grid/use-virtual-rows.js +123 -18
- package/dist/package.js +1 -1
- package/dist/plugins/load-plugin.d.ts +1 -1
- package/dist/plugins/load-plugin.js +1 -1
- package/dist/shell/assets/app-Dkz6z_t_.js +66 -0
- package/dist/shell/assets/{index-rR2iasG9.js → index-BCLK6g9a.js} +1 -1
- package/dist/shell/assets/index-BQ4FIU4i.css +2 -0
- package/dist/shell/assets/{maplibre-gl-estTAzEH.js → maplibre-gl-DWx1yzYO.js} +1 -1
- package/dist/shell/assets/{record-map-B57A7PQ0.js → record-map-C2M8dEjy.js} +2 -2
- package/dist/shell/assets/{records-map-BwsWXD8A.js → records-map-huS0J9xV.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-CE7mIalS.js → shared-protobase-ui-CsJxPCx6.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/data-grid/use-grid-scroll.js +0 -87
- package/dist/shell/assets/app-BBslM0jG.js +0 -66
- package/dist/shell/assets/index-CYWfU_c1.css +0 -2
|
@@ -15,7 +15,7 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
import { useTable } from "@tanstack/react-table";
|
|
16
16
|
//#region src/data-grid/data-grid.tsx
|
|
17
17
|
const headerHeight = 32;
|
|
18
|
-
const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0, onScrollIndex, latencyMs = 150, sort: controlledSort, onSortChange, onOpenRow, rowActions, bulkActions, pinKey, defaultPinned, rowNumbers = true, compact = false, selectable = true, fit = false, className }) => {
|
|
18
|
+
const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0, onScrollIndex, latencyMs = 150, sort: controlledSort, onSortChange, onOpenRow, rowActions, bulkActions, pinKey, defaultPinned, rowNumbers = true, compact = false, selectable = true, fit = false, scroll = "own", className }) => {
|
|
19
19
|
const rowHeight = compact ? 28 : 36;
|
|
20
20
|
const [pinned, setPinned] = usePinnedColumns(pinKey, defaultPinned);
|
|
21
21
|
const columns = useMemo(() => {
|
|
@@ -50,16 +50,16 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
50
50
|
reverse,
|
|
51
51
|
total
|
|
52
52
|
]);
|
|
53
|
-
const
|
|
53
|
+
const [startIndex] = useState(initialScrollIndex);
|
|
54
|
+
const { ref, top, geometry, rows: virtualRows, first: firstIndex, last: lastIndex } = useVirtualRows({
|
|
54
55
|
total,
|
|
55
56
|
rowHeight,
|
|
56
|
-
initialIndex:
|
|
57
|
+
initialIndex: startIndex,
|
|
57
58
|
inset: headerHeight
|
|
58
59
|
});
|
|
59
60
|
const narrow = !useIsDesktop();
|
|
60
61
|
const gutter = narrow || !rowNumbers ? 40 : gutterWidthFor(total);
|
|
61
|
-
const
|
|
62
|
-
const topIndex = Math.floor(top / rowHeight);
|
|
62
|
+
const topIndex = Math.floor(Math.max(0, top) / rowHeight);
|
|
63
63
|
useEffect(() => onScrollIndex?.(topIndex), [topIndex, onScrollIndex]);
|
|
64
64
|
const getRow = usePagedRows({
|
|
65
65
|
total,
|
|
@@ -99,9 +99,14 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
99
99
|
const trailing = rowActions && rowActions.length > 0 ? 40 : 0;
|
|
100
100
|
const minScale = narrow ? 1 : desktopMinScale;
|
|
101
101
|
const horizontal = useHorizontalScroll();
|
|
102
|
+
const headerRef = useRef(null);
|
|
103
|
+
const onBodyScroll = () => {
|
|
104
|
+
horizontal.onScroll();
|
|
105
|
+
if (headerRef.current && horizontal.ref.current) headerRef.current.scrollLeft = horizontal.ref.current.scrollLeft;
|
|
106
|
+
};
|
|
102
107
|
const [pins, setPins] = useState({});
|
|
103
108
|
useLayoutEffect(() => {
|
|
104
|
-
const header =
|
|
109
|
+
const header = headerRef.current?.querySelector("[role=row]");
|
|
105
110
|
if (!header) return;
|
|
106
111
|
let left = gutter;
|
|
107
112
|
const next = {};
|
|
@@ -118,89 +123,91 @@ const DataGrid = ({ columns: given, source, naturalSort, initialScrollIndex = 0,
|
|
|
118
123
|
sorted: header.column.getIsSorted(),
|
|
119
124
|
onSort: (event) => header.column.getToggleSortingHandler()?.(event)
|
|
120
125
|
})));
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
className: cn("
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
const minWidth = gridMinWidth(columns, gutter, trailing, minScale);
|
|
127
|
+
const own = scroll === "own";
|
|
128
|
+
const header = /* @__PURE__ */ jsx("div", {
|
|
129
|
+
ref: headerRef,
|
|
130
|
+
className: cn("sticky z-20 shrink-0 overflow-hidden", own ? "top-0" : "top-[var(--pb-sticky-top,0px)]"),
|
|
131
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
132
|
+
style: { minWidth },
|
|
133
|
+
children: /* @__PURE__ */ jsx(GridHeader, {
|
|
134
|
+
cells: headerCells,
|
|
135
|
+
specs: columns,
|
|
136
|
+
gutter,
|
|
137
|
+
trailing,
|
|
138
|
+
minScale,
|
|
139
|
+
pins,
|
|
140
|
+
lastPinned,
|
|
141
|
+
scrolled: horizontal.scrolled,
|
|
142
|
+
onTogglePin: togglePin,
|
|
143
|
+
selectable,
|
|
144
|
+
selectedCount,
|
|
145
|
+
onClearSelection: () => setRowSelection({})
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
});
|
|
149
|
+
const body = /* @__PURE__ */ jsx("div", {
|
|
150
|
+
ref: horizontal.attach,
|
|
151
|
+
onScroll: onBodyScroll,
|
|
152
|
+
className: "overflow-x-auto overflow-y-hidden overscroll-x-contain [-webkit-overflow-scrolling:touch]",
|
|
153
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
128
154
|
ref,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
offset,
|
|
166
|
-
height: rowHeight,
|
|
167
|
-
selectable,
|
|
168
|
-
trailing,
|
|
169
|
-
minScale,
|
|
170
|
-
scrolled: horizontal.scrolled,
|
|
171
|
-
pins,
|
|
172
|
-
lastPinned
|
|
173
|
-
};
|
|
174
|
-
const tableRow = row ? tableRows.get(row.id) : void 0;
|
|
175
|
-
if (!tableRow) return /* @__PURE__ */ jsx(SkeletonRow, { ...frame }, `skeleton-${index}`);
|
|
176
|
-
const actions = trailing && row ? availableActions(rowActions, row) : [];
|
|
177
|
-
return /* @__PURE__ */ jsx(DataRow, {
|
|
178
|
-
...frame,
|
|
179
|
-
selected: tableRow.getIsSelected(),
|
|
180
|
-
onToggle: () => tableRow.toggleSelected(),
|
|
181
|
-
onOpen: onOpenRow && row ? () => onOpenRow(row) : void 0,
|
|
182
|
-
menu: row && actions.length > 0 ? /* @__PURE__ */ jsx(RowMenu, {
|
|
183
|
-
row,
|
|
184
|
-
actions
|
|
185
|
-
}) : void 0,
|
|
186
|
-
cells: tableRow.getAllCells().map((cell) => ({
|
|
187
|
-
spec: columns.find((spec) => spec.id === cell.column.id),
|
|
188
|
-
content: /* @__PURE__ */ jsx(table.FlexRender, { cell })
|
|
189
|
-
}))
|
|
190
|
-
}, row?.id);
|
|
191
|
-
})
|
|
192
|
-
})]
|
|
193
|
-
})
|
|
194
|
-
}), horizontal.more && /* @__PURE__ */ jsx("div", {
|
|
195
|
-
"aria-hidden": true,
|
|
196
|
-
className: "pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-foreground/15 to-transparent"
|
|
197
|
-
})] })
|
|
198
|
-
})
|
|
155
|
+
className: "relative",
|
|
156
|
+
style: {
|
|
157
|
+
minWidth,
|
|
158
|
+
height: geometry.contentHeight
|
|
159
|
+
},
|
|
160
|
+
children: loaded.map(({ index, offset, row }) => {
|
|
161
|
+
const frame = {
|
|
162
|
+
specs: columns,
|
|
163
|
+
gutter,
|
|
164
|
+
index,
|
|
165
|
+
offset,
|
|
166
|
+
height: rowHeight,
|
|
167
|
+
selectable,
|
|
168
|
+
trailing,
|
|
169
|
+
minScale,
|
|
170
|
+
scrolled: horizontal.scrolled,
|
|
171
|
+
pins,
|
|
172
|
+
lastPinned
|
|
173
|
+
};
|
|
174
|
+
const tableRow = row ? tableRows.get(row.id) : void 0;
|
|
175
|
+
if (!tableRow) return /* @__PURE__ */ jsx(SkeletonRow, { ...frame }, `skeleton-${index}`);
|
|
176
|
+
const actions = trailing && row ? availableActions(rowActions, row) : [];
|
|
177
|
+
return /* @__PURE__ */ jsx(DataRow, {
|
|
178
|
+
...frame,
|
|
179
|
+
selected: tableRow.getIsSelected(),
|
|
180
|
+
onToggle: () => tableRow.toggleSelected(),
|
|
181
|
+
onOpen: onOpenRow && row ? () => onOpenRow(row) : void 0,
|
|
182
|
+
menu: row && actions.length > 0 ? /* @__PURE__ */ jsx(RowMenu, {
|
|
183
|
+
row,
|
|
184
|
+
actions
|
|
185
|
+
}) : void 0,
|
|
186
|
+
cells: tableRow.getAllCells().map((cell) => ({
|
|
187
|
+
spec: columns.find((spec) => spec.id === cell.column.id),
|
|
188
|
+
content: /* @__PURE__ */ jsx(table.FlexRender, { cell })
|
|
189
|
+
}))
|
|
190
|
+
}, row?.id);
|
|
199
191
|
})
|
|
192
|
+
})
|
|
193
|
+
});
|
|
194
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
195
|
+
role: "grid",
|
|
196
|
+
"aria-rowcount": total,
|
|
197
|
+
className: cn("flex flex-col rounded-lg border bg-background", own ? "min-h-0 overflow-hidden" : "overflow-clip", className),
|
|
198
|
+
style: fit && own ? { height: headerHeight + total * rowHeight + 2 } : void 0,
|
|
199
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
200
|
+
className: cn("relative", own && "flex min-h-0 flex-1 flex-col"),
|
|
201
|
+
children: [own ? /* @__PURE__ */ jsxs("div", {
|
|
202
|
+
className: "min-h-0 flex-1 overflow-y-auto overflow-x-hidden",
|
|
203
|
+
children: [header, body]
|
|
204
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [header, body] }), horizontal.more && /* @__PURE__ */ jsx("div", {
|
|
205
|
+
"aria-hidden": true,
|
|
206
|
+
className: "pointer-events-none absolute inset-y-0 right-0 z-20 w-8 bg-gradient-to-l from-foreground/15 to-transparent"
|
|
207
|
+
})]
|
|
200
208
|
}), !fit && /* @__PURE__ */ jsx(GridFooter, {
|
|
201
209
|
total,
|
|
202
|
-
|
|
203
|
-
last,
|
|
210
|
+
className: own ? void 0 : "sticky bottom-[var(--pb-sticky-bottom,0px)] z-20",
|
|
204
211
|
selectedCount,
|
|
205
212
|
bulkActions: bulkActions?.map((action) => ({
|
|
206
213
|
label: action.label(selectedCount),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//#region src/data-grid/geometry.ts
|
|
2
2
|
/** Browsers cap element height (about 17M px in the strictest); stay well below it. */
|
|
3
3
|
const maxContentHeight = 1e7;
|
|
4
|
+
/** The list's height in the page, `contentHeight`: its true height, or the cap when the rows need more. */
|
|
4
5
|
const gridGeometry = (total, rowHeight, viewportHeight) => {
|
|
5
6
|
const trueHeight = total * rowHeight;
|
|
6
7
|
const contentHeight = Math.min(trueHeight, maxContentHeight);
|
|
@@ -13,13 +14,32 @@ const gridGeometry = (total, rowHeight, viewportHeight) => {
|
|
|
13
14
|
maxScroll: Math.max(0, contentHeight - viewportHeight)
|
|
14
15
|
};
|
|
15
16
|
};
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The offset into the rows (the virtual offset) shown at `position`, how far the top of the view is into the list.
|
|
19
|
+
* They are the same unless the list is capped: then the capped range maps onto all the rows, and a position before or
|
|
20
|
+
* after it moves one to one.
|
|
21
|
+
*/
|
|
22
|
+
const topFromScroll = (geometry, position) => {
|
|
23
|
+
if (!geometry.scaled || position <= 0) return position;
|
|
24
|
+
if (position >= geometry.maxScroll) return geometry.maxTop + position - geometry.maxScroll;
|
|
25
|
+
return position / geometry.maxScroll * geometry.maxTop;
|
|
26
|
+
};
|
|
27
|
+
const scrollFromTop = (geometry, top) => {
|
|
28
|
+
if (!geometry.scaled || top <= 0) return top;
|
|
29
|
+
if (top >= geometry.maxTop) return geometry.maxScroll + top - geometry.maxTop;
|
|
30
|
+
return top / geometry.maxTop * geometry.maxScroll;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Where row `index` is drawn in the list when the view at `position` shows the rows from `top`: its own place, or in a
|
|
34
|
+
* capped list its place relative to the view, so that it scrolls with the page until the next draw.
|
|
35
|
+
*/
|
|
36
|
+
const rowOffset = (geometry, index, top, position) => {
|
|
37
|
+
const offset = index * geometry.rowHeight;
|
|
38
|
+
return geometry.scaled ? offset - top + position : offset;
|
|
39
|
+
};
|
|
20
40
|
/** The rows drawn for a scroll offset: those in a viewport of `height`, and `overscan` more on each side. */
|
|
21
41
|
const rowsInView = (total, top, rowHeight, height, overscan) => {
|
|
22
|
-
const first = Math.max(0, Math.floor(top / rowHeight) - overscan);
|
|
42
|
+
const first = Math.min(total, Math.max(0, Math.floor(top / rowHeight) - overscan));
|
|
23
43
|
const last = Math.min(total - 1, Math.floor((top + height) / rowHeight) + overscan);
|
|
24
44
|
return {
|
|
25
45
|
first,
|
|
@@ -27,4 +47,4 @@ const rowsInView = (total, top, rowHeight, height, overscan) => {
|
|
|
27
47
|
};
|
|
28
48
|
};
|
|
29
49
|
//#endregion
|
|
30
|
-
export {
|
|
50
|
+
export { gridGeometry, maxContentHeight, rowOffset, rowsInView, scrollFromTop, topFromScroll };
|
|
@@ -2,9 +2,10 @@ import { cn } from "../lib/cn.js";
|
|
|
2
2
|
import { formatApproxRows, formatInt } from "../format/number.js";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/data-grid/grid-footer.tsx
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/** How many rows match and, with rows selected, how many and what can be done with them. */
|
|
6
|
+
const GridFooter = ({ total, selectedCount, bulkActions = [], className }) => /* @__PURE__ */ jsx("div", {
|
|
7
|
+
className: cn("flex h-9 shrink-0 items-center border-t bg-surface px-3 text-xs text-muted-foreground", className),
|
|
8
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
8
9
|
className: "flex items-center tabular-nums",
|
|
9
10
|
children: [
|
|
10
11
|
formatApproxRows(total),
|
|
@@ -19,15 +20,7 @@ const GridFooter = ({ total, first, last, selectedCount, bulkActions = [] }) =>
|
|
|
19
20
|
children: action.label
|
|
20
21
|
}, action.label))
|
|
21
22
|
]
|
|
22
|
-
})
|
|
23
|
-
className: "tabular-nums",
|
|
24
|
-
children: [
|
|
25
|
-
"Rows ",
|
|
26
|
-
formatInt(first),
|
|
27
|
-
" – ",
|
|
28
|
-
formatInt(last)
|
|
29
|
-
]
|
|
30
|
-
})]
|
|
23
|
+
})
|
|
31
24
|
});
|
|
32
25
|
//#endregion
|
|
33
26
|
export { GridFooter };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/data-grid/scroll-parent.ts
|
|
2
|
+
/**
|
|
3
|
+
* The element that scrolls `element` up and down: its nearest ancestor that scrolls vertically, or the page's
|
|
4
|
+
* scrolling element when none does.
|
|
5
|
+
*/
|
|
6
|
+
const scrollParent = (element) => {
|
|
7
|
+
const page = document.scrollingElement ?? document.documentElement;
|
|
8
|
+
for (let parent = element.parentElement; parent && parent !== page && parent !== document.body; parent = parent.parentElement) if (/auto|scroll/.test(getComputedStyle(parent).overflowY)) return parent;
|
|
9
|
+
return page;
|
|
10
|
+
};
|
|
11
|
+
const isPage = (scroller) => scroller === (document.scrollingElement ?? document.documentElement);
|
|
12
|
+
/** Where scroll events of `scroller` are sent: the window for the page. */
|
|
13
|
+
const scrollEvents = (scroller) => isPage(scroller) ? window : scroller;
|
|
14
|
+
const padding = (value) => Number.parseFloat(value) || 0;
|
|
15
|
+
/**
|
|
16
|
+
* The part of `scroller` content shows in, in viewport coordinates: its box less its scroll padding, which is where
|
|
17
|
+
* sticky bars over it say they are.
|
|
18
|
+
*/
|
|
19
|
+
const visibleArea = (scroller) => {
|
|
20
|
+
const style = getComputedStyle(scroller);
|
|
21
|
+
const box = isPage(scroller) ? {
|
|
22
|
+
top: 0,
|
|
23
|
+
height: scroller.clientHeight
|
|
24
|
+
} : {
|
|
25
|
+
top: scroller.getBoundingClientRect().top + scroller.clientTop,
|
|
26
|
+
height: scroller.clientHeight
|
|
27
|
+
};
|
|
28
|
+
const top = padding(style.scrollPaddingTop);
|
|
29
|
+
return {
|
|
30
|
+
top: box.top + top,
|
|
31
|
+
height: Math.max(0, box.height - top - padding(style.scrollPaddingBottom))
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { scrollEvents, scrollParent, visibleArea };
|
|
@@ -1,30 +1,135 @@
|
|
|
1
|
-
import { rowsInView } from "./geometry.js";
|
|
2
|
-
import {
|
|
1
|
+
import { gridGeometry, rowOffset, rowsInView, scrollFromTop, topFromScroll } from "./geometry.js";
|
|
2
|
+
import { scrollEvents, scrollParent, visibleArea } from "./scroll-parent.js";
|
|
3
|
+
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
3
4
|
//#region src/data-grid/use-virtual-rows.ts
|
|
4
5
|
const overscan = 6;
|
|
6
|
+
const viewRows = (view, total, rowHeight) => {
|
|
7
|
+
const geometry = gridGeometry(total, rowHeight, view.height);
|
|
8
|
+
const top = topFromScroll(geometry, view.position);
|
|
9
|
+
return {
|
|
10
|
+
geometry,
|
|
11
|
+
top,
|
|
12
|
+
topIndex: Math.floor(Math.max(0, top) / rowHeight),
|
|
13
|
+
...rowsInView(total, top, rowHeight, view.height, overscan)
|
|
14
|
+
};
|
|
15
|
+
};
|
|
5
16
|
/**
|
|
6
|
-
* Rows of a fixed height
|
|
7
|
-
*
|
|
17
|
+
* Rows of a fixed height in a list as tall as all of them, scrolled natively by whatever scrolls it: the nearest
|
|
18
|
+
* scrolling ancestor, or the page. Only the rows in view, and `overscan` more on each side, are drawn: `ref` goes on the
|
|
19
|
+
* list's element, `geometry.contentHeight` tall, and each drawn row sits at `offset` from its top. Nothing listens for
|
|
20
|
+
* wheel or touch: the list follows the scroll position, and draws again only when other rows come into view.
|
|
8
21
|
* `first` and `last` are the indexes of the drawn rows, overscan included.
|
|
9
22
|
*/
|
|
10
|
-
const useVirtualRows = ({ total, rowHeight, initialIndex, inset = 0, settle }) => {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
23
|
+
const useVirtualRows = ({ total, rowHeight, initialIndex, inset = 0, settle, ready = true }) => {
|
|
24
|
+
const ref = useRef(null);
|
|
25
|
+
const scroller = useRef(null);
|
|
26
|
+
const [view, setView] = useState({
|
|
27
|
+
position: 0,
|
|
28
|
+
height: 0
|
|
29
|
+
});
|
|
30
|
+
const options = useRef({
|
|
31
|
+
total,
|
|
32
|
+
rowHeight,
|
|
33
|
+
initialIndex,
|
|
34
|
+
inset,
|
|
35
|
+
ready
|
|
36
|
+
});
|
|
37
|
+
options.current = {
|
|
38
|
+
total,
|
|
39
|
+
rowHeight,
|
|
40
|
+
initialIndex,
|
|
41
|
+
inset,
|
|
42
|
+
ready
|
|
43
|
+
};
|
|
44
|
+
const read = useCallback(() => {
|
|
45
|
+
const content = ref.current;
|
|
46
|
+
if (!content || !scroller.current) return void 0;
|
|
47
|
+
const area = visibleArea(scroller.current);
|
|
48
|
+
const hidden = options.current.inset;
|
|
49
|
+
return {
|
|
50
|
+
position: area.top + hidden - content.getBoundingClientRect().top,
|
|
51
|
+
height: Math.max(0, area.height - hidden)
|
|
52
|
+
};
|
|
53
|
+
}, []);
|
|
54
|
+
const shown = useRef(view);
|
|
55
|
+
shown.current = view;
|
|
56
|
+
const measure = useCallback(() => {
|
|
57
|
+
const next = read();
|
|
58
|
+
if (!next) return;
|
|
59
|
+
const current = shown.current;
|
|
60
|
+
const { total, rowHeight } = options.current;
|
|
61
|
+
const before = viewRows(current, total, rowHeight);
|
|
62
|
+
const after = viewRows(next, total, rowHeight);
|
|
63
|
+
const sameRows = !after.geometry.scaled && before.first === after.first && before.last === after.last && before.topIndex === after.topIndex;
|
|
64
|
+
if (current.height === next.height && (current.position === next.position || sameRows)) return;
|
|
65
|
+
shown.current = next;
|
|
66
|
+
setView(next);
|
|
67
|
+
}, [read]);
|
|
68
|
+
const start = useRef({
|
|
69
|
+
pending: true,
|
|
70
|
+
laidOut: false
|
|
71
|
+
});
|
|
72
|
+
const begin = useCallback(() => {
|
|
73
|
+
if (!start.current.pending || !start.current.laidOut || !options.current.ready) return;
|
|
74
|
+
const view = read();
|
|
75
|
+
const parent = scroller.current;
|
|
76
|
+
if (!view || !parent) return;
|
|
77
|
+
start.current.pending = false;
|
|
78
|
+
const { total, rowHeight, initialIndex } = options.current;
|
|
79
|
+
if (initialIndex > 0) parent.scrollTop += scrollFromTop(gridGeometry(total, rowHeight, view.height), initialIndex * rowHeight) - view.position;
|
|
80
|
+
else if (view.position > 0) parent.scrollTop = 0;
|
|
81
|
+
}, [read]);
|
|
82
|
+
useLayoutEffect(() => {
|
|
83
|
+
const content = ref.current;
|
|
84
|
+
if (!content) return;
|
|
85
|
+
const parent = scrollParent(content);
|
|
86
|
+
scroller.current = parent;
|
|
87
|
+
const events = scrollEvents(parent);
|
|
88
|
+
events.addEventListener("scroll", measure, { passive: true });
|
|
89
|
+
window.addEventListener("resize", measure, { passive: true });
|
|
90
|
+
const observer = new ResizeObserver(() => {
|
|
91
|
+
start.current.laidOut = true;
|
|
92
|
+
begin();
|
|
93
|
+
measure();
|
|
94
|
+
});
|
|
95
|
+
observer.observe(parent);
|
|
96
|
+
observer.observe(content);
|
|
97
|
+
measure();
|
|
98
|
+
return () => {
|
|
99
|
+
events.removeEventListener("scroll", measure);
|
|
100
|
+
window.removeEventListener("resize", measure);
|
|
101
|
+
observer.disconnect();
|
|
102
|
+
scroller.current = null;
|
|
103
|
+
};
|
|
104
|
+
}, [measure, begin]);
|
|
105
|
+
useLayoutEffect(() => {
|
|
106
|
+
begin();
|
|
107
|
+
measure();
|
|
108
|
+
});
|
|
109
|
+
/** Scrolls to a virtual offset, for a layout change that keeps a row in view. */
|
|
110
|
+
const scrollTo = useCallback((top) => {
|
|
111
|
+
const current = read();
|
|
112
|
+
if (!current || !scroller.current) return;
|
|
113
|
+
const geometry = gridGeometry(options.current.total, options.current.rowHeight, current.height);
|
|
114
|
+
scroller.current.scrollTop += scrollFromTop(geometry, top) - current.position;
|
|
115
|
+
measure();
|
|
116
|
+
}, [read, measure]);
|
|
117
|
+
const drawn = viewRows(view, total, rowHeight);
|
|
118
|
+
const top = settle ? settle(drawn.top) : drawn.top;
|
|
119
|
+
const { first, last } = settle ? rowsInView(total, top, rowHeight, view.height, overscan) : drawn;
|
|
120
|
+
const rows = Array.from({ length: last - first + 1 }, (_, offset) => ({
|
|
121
|
+
index: first + offset,
|
|
122
|
+
offset: rowOffset(drawn.geometry, first + offset, top, view.position)
|
|
123
|
+
}));
|
|
15
124
|
return {
|
|
16
125
|
ref,
|
|
17
|
-
|
|
18
|
-
viewport,
|
|
126
|
+
height: view.height,
|
|
19
127
|
top,
|
|
20
|
-
geometry,
|
|
21
|
-
|
|
22
|
-
rows: Array.from({ length: last - first + 1 }, (_, offset) => ({
|
|
23
|
-
index: first + offset,
|
|
24
|
-
offset: (first + offset) * rowHeight - top
|
|
25
|
-
})),
|
|
128
|
+
geometry: drawn.geometry,
|
|
129
|
+
rows,
|
|
26
130
|
first,
|
|
27
|
-
last
|
|
131
|
+
last,
|
|
132
|
+
scrollTo
|
|
28
133
|
};
|
|
29
134
|
};
|
|
30
135
|
//#endregion
|
package/dist/package.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "../app/pages/project-ui.js";
|
|
2
2
|
import { PluginRef } from "@protobase/client";
|
|
3
|
-
import "@protobase/schema";
|
|
3
|
+
import "@protobase/schema/internal";
|
|
4
4
|
//#region src/plugins/load-plugin.d.ts
|
|
5
5
|
/** What loading a plugin needs from the page: reading its plugin.json, adding its stylesheet and importing its module. */
|
|
6
6
|
export type PluginLoader = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseManifest } from "./plugin-manifest.js";
|
|
2
2
|
import { pluginUi } from "./plugin-ui.js";
|
|
3
3
|
import { pluginApi, protobaseVersion } from "./plugin-api.js";
|
|
4
|
-
import { pluginProblem } from "@protobase/schema";
|
|
4
|
+
import { pluginProblem } from "@protobase/schema/internal";
|
|
5
5
|
//#region src/plugins/load-plugin.ts
|
|
6
6
|
/** The page's own: `fetch` without a cached copy, a `<link>` per stylesheet, and `import()`. */
|
|
7
7
|
const browserLoader = {
|