vastgrid 0.1.0 → 0.2.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.
Files changed (312) hide show
  1. package/README.md +175 -13
  2. package/dist/canvas/geometry.d.ts +92 -0
  3. package/dist/canvas/geometry.d.ts.map +1 -0
  4. package/dist/canvas/geometry.js +197 -0
  5. package/dist/canvas/geometry.js.map +1 -0
  6. package/dist/canvas/grid-canvas.d.ts +161 -0
  7. package/dist/canvas/grid-canvas.d.ts.map +1 -0
  8. package/dist/canvas/grid-canvas.js +1146 -0
  9. package/dist/canvas/grid-canvas.js.map +1 -0
  10. package/dist/canvas/hooks.d.ts +61 -0
  11. package/dist/canvas/hooks.d.ts.map +1 -0
  12. package/dist/canvas/hooks.js +2 -0
  13. package/dist/canvas/hooks.js.map +1 -0
  14. package/dist/canvas/keys.d.ts +26 -0
  15. package/dist/canvas/keys.d.ts.map +1 -0
  16. package/dist/canvas/keys.js +133 -0
  17. package/dist/canvas/keys.js.map +1 -0
  18. package/dist/canvas/layout.d.ts +26 -0
  19. package/dist/canvas/layout.d.ts.map +1 -0
  20. package/dist/canvas/layout.js +73 -0
  21. package/dist/canvas/layout.js.map +1 -0
  22. package/dist/canvas/scroll.d.ts +31 -0
  23. package/dist/canvas/scroll.d.ts.map +1 -0
  24. package/dist/canvas/scroll.js +111 -0
  25. package/dist/canvas/scroll.js.map +1 -0
  26. package/dist/canvas/text.d.ts +23 -0
  27. package/dist/canvas/text.d.ts.map +1 -0
  28. package/dist/canvas/text.js +142 -0
  29. package/dist/canvas/text.js.map +1 -0
  30. package/dist/{theme.d.ts → canvas/theme.d.ts} +8 -0
  31. package/dist/canvas/theme.d.ts.map +1 -0
  32. package/dist/canvas/theme.js +62 -0
  33. package/dist/canvas/theme.js.map +1 -0
  34. package/dist/core/cache.d.ts +48 -0
  35. package/dist/core/cache.d.ts.map +1 -0
  36. package/dist/core/cache.js +222 -0
  37. package/dist/core/cache.js.map +1 -0
  38. package/dist/core/checked.d.ts +20 -0
  39. package/dist/core/checked.d.ts.map +1 -0
  40. package/dist/core/checked.js +61 -0
  41. package/dist/core/checked.js.map +1 -0
  42. package/dist/core/client.d.ts +33 -0
  43. package/dist/core/client.d.ts.map +1 -0
  44. package/dist/core/client.js +103 -0
  45. package/dist/core/client.js.map +1 -0
  46. package/dist/core/conditions.d.ts +48 -0
  47. package/dist/core/conditions.d.ts.map +1 -0
  48. package/dist/core/conditions.js +105 -0
  49. package/dist/core/conditions.js.map +1 -0
  50. package/dist/core/evaluate.d.ts +20 -0
  51. package/dist/core/evaluate.d.ts.map +1 -0
  52. package/dist/core/evaluate.js +210 -0
  53. package/dist/core/evaluate.js.map +1 -0
  54. package/dist/core/filters.d.ts +51 -0
  55. package/dist/core/filters.d.ts.map +1 -0
  56. package/dist/core/filters.js +303 -0
  57. package/dist/core/filters.js.map +1 -0
  58. package/dist/core/format.d.ts +14 -0
  59. package/dist/core/format.d.ts.map +1 -0
  60. package/dist/core/format.js +303 -0
  61. package/dist/core/format.js.map +1 -0
  62. package/dist/core/measures.d.ts +26 -0
  63. package/dist/core/measures.d.ts.map +1 -0
  64. package/dist/core/measures.js +105 -0
  65. package/dist/core/measures.js.map +1 -0
  66. package/dist/core/request-ids.d.ts +20 -0
  67. package/dist/core/request-ids.d.ts.map +1 -0
  68. package/dist/core/request-ids.js +55 -0
  69. package/dist/core/request-ids.js.map +1 -0
  70. package/dist/core/rows.d.ts +79 -0
  71. package/dist/core/rows.d.ts.map +1 -0
  72. package/dist/core/rows.js +173 -0
  73. package/dist/core/rows.js.map +1 -0
  74. package/dist/core/store.d.ts +233 -0
  75. package/dist/core/store.d.ts.map +1 -0
  76. package/dist/core/store.js +1162 -0
  77. package/dist/core/store.js.map +1 -0
  78. package/dist/core/totals.d.ts +24 -0
  79. package/dist/core/totals.d.ts.map +1 -0
  80. package/dist/core/totals.js +49 -0
  81. package/dist/core/totals.js.map +1 -0
  82. package/dist/core/trace.d.ts +7 -0
  83. package/dist/core/trace.d.ts.map +1 -0
  84. package/dist/core/trace.js +26 -0
  85. package/dist/core/trace.js.map +1 -0
  86. package/dist/core/transports.d.ts +50 -0
  87. package/dist/core/transports.d.ts.map +1 -0
  88. package/dist/core/transports.js +160 -0
  89. package/dist/core/transports.js.map +1 -0
  90. package/dist/core/types.d.ts +150 -0
  91. package/dist/core/types.d.ts.map +1 -0
  92. package/dist/core/types.js +45 -0
  93. package/dist/core/types.js.map +1 -0
  94. package/dist/core.d.ts +12 -0
  95. package/dist/core.d.ts.map +1 -0
  96. package/dist/core.js +11 -0
  97. package/dist/core.js.map +1 -0
  98. package/dist/index.d.ts +15 -6
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +13 -7
  101. package/dist/index.js.map +1 -1
  102. package/dist/react/accessible-rows.d.ts +13 -0
  103. package/dist/react/accessible-rows.d.ts.map +1 -0
  104. package/dist/react/accessible-rows.js +320 -0
  105. package/dist/react/accessible-rows.js.map +1 -0
  106. package/dist/react/cell-editor.d.ts +22 -0
  107. package/dist/react/cell-editor.d.ts.map +1 -0
  108. package/dist/react/cell-editor.js +431 -0
  109. package/dist/react/cell-editor.js.map +1 -0
  110. package/dist/react/cell-menu.d.ts +18 -0
  111. package/dist/react/cell-menu.d.ts.map +1 -0
  112. package/dist/react/cell-menu.js +299 -0
  113. package/dist/react/cell-menu.js.map +1 -0
  114. package/dist/react/column-menu.d.ts +16 -0
  115. package/dist/react/column-menu.d.ts.map +1 -0
  116. package/dist/react/column-menu.js +1311 -0
  117. package/dist/react/column-menu.js.map +1 -0
  118. package/dist/react/columns-menu.d.ts +7 -0
  119. package/dist/react/columns-menu.d.ts.map +1 -0
  120. package/dist/react/columns-menu.js +249 -0
  121. package/dist/react/columns-menu.js.map +1 -0
  122. package/dist/react/conditions-menu.d.ts +10 -0
  123. package/dist/react/conditions-menu.d.ts.map +1 -0
  124. package/dist/react/conditions-menu.js +795 -0
  125. package/dist/react/conditions-menu.js.map +1 -0
  126. package/dist/react/context.d.ts +29 -0
  127. package/dist/react/context.d.ts.map +1 -0
  128. package/dist/react/context.js +18 -0
  129. package/dist/react/context.js.map +1 -0
  130. package/dist/react/data-grid.d.ts +79 -0
  131. package/dist/react/data-grid.d.ts.map +1 -0
  132. package/dist/react/data-grid.js +653 -0
  133. package/dist/react/data-grid.js.map +1 -0
  134. package/dist/react/detail-panel.d.ts +22 -0
  135. package/dist/react/detail-panel.d.ts.map +1 -0
  136. package/dist/react/detail-panel.js +242 -0
  137. package/dist/react/detail-panel.js.map +1 -0
  138. package/dist/react/export-menu.d.ts +11 -0
  139. package/dist/react/export-menu.d.ts.map +1 -0
  140. package/dist/react/export-menu.js +304 -0
  141. package/dist/react/export-menu.js.map +1 -0
  142. package/dist/react/find-bar.d.ts +11 -0
  143. package/dist/react/find-bar.d.ts.map +1 -0
  144. package/dist/react/find-bar.js +303 -0
  145. package/dist/react/find-bar.js.map +1 -0
  146. package/dist/react/format-form.d.ts +12 -0
  147. package/dist/react/format-form.d.ts.map +1 -0
  148. package/dist/react/format-form.js +863 -0
  149. package/dist/react/format-form.js.map +1 -0
  150. package/dist/react/formula-menu.d.ts +6 -0
  151. package/dist/react/formula-menu.d.ts.map +1 -0
  152. package/dist/react/formula-menu.js +343 -0
  153. package/dist/react/formula-menu.js.map +1 -0
  154. package/dist/react/grid-body.d.ts +33 -0
  155. package/dist/react/grid-body.d.ts.map +1 -0
  156. package/dist/react/grid-body.js +1389 -0
  157. package/dist/react/grid-body.js.map +1 -0
  158. package/dist/react/group-menu.d.ts +7 -0
  159. package/dist/react/group-menu.d.ts.map +1 -0
  160. package/dist/react/group-menu.js +508 -0
  161. package/dist/react/group-menu.js.map +1 -0
  162. package/dist/react/hooks.d.ts +33 -0
  163. package/dist/react/hooks.d.ts.map +1 -0
  164. package/dist/react/hooks.js +161 -0
  165. package/dist/react/hooks.js.map +1 -0
  166. package/dist/react/messages.d.ts +180 -0
  167. package/dist/react/messages.d.ts.map +1 -0
  168. package/dist/react/messages.js +414 -0
  169. package/dist/react/messages.js.map +1 -0
  170. package/dist/react/save.d.ts +9 -0
  171. package/dist/react/save.d.ts.map +1 -0
  172. package/dist/react/save.js +102 -0
  173. package/dist/react/save.js.map +1 -0
  174. package/dist/react/static-rows.d.ts +9 -0
  175. package/dist/react/static-rows.d.ts.map +1 -0
  176. package/dist/react/static-rows.js +176 -0
  177. package/dist/react/static-rows.js.map +1 -0
  178. package/dist/react/status-bar.d.ts +12 -0
  179. package/dist/react/status-bar.d.ts.map +1 -0
  180. package/dist/react/status-bar.js +413 -0
  181. package/dist/react/status-bar.js.map +1 -0
  182. package/dist/react/toolbar-popover.d.ts +14 -0
  183. package/dist/react/toolbar-popover.d.ts.map +1 -0
  184. package/dist/react/toolbar-popover.js +143 -0
  185. package/dist/react/toolbar-popover.js.map +1 -0
  186. package/dist/react/toolbar.d.ts +13 -0
  187. package/dist/react/toolbar.d.ts.map +1 -0
  188. package/dist/react/toolbar.js +344 -0
  189. package/dist/react/toolbar.js.map +1 -0
  190. package/dist/server.d.ts +22 -0
  191. package/dist/server.d.ts.map +1 -0
  192. package/dist/server.js +45 -0
  193. package/dist/server.js.map +1 -0
  194. package/dist/styles.css +496 -316
  195. package/package.json +43 -18
  196. package/src/canvas/geometry.ts +197 -0
  197. package/src/canvas/grid-canvas.ts +1222 -0
  198. package/src/canvas/hooks.ts +61 -0
  199. package/src/canvas/keys.ts +96 -0
  200. package/src/canvas/layout.ts +77 -0
  201. package/src/canvas/scroll.ts +120 -0
  202. package/src/canvas/text.ts +155 -0
  203. package/src/{theme.ts → canvas/theme.ts} +14 -1
  204. package/src/core/cache.ts +247 -0
  205. package/src/core/checked.ts +64 -0
  206. package/src/core/client.ts +103 -0
  207. package/src/core/conditions.ts +89 -0
  208. package/src/core/evaluate.ts +221 -0
  209. package/src/core/filters.ts +268 -0
  210. package/src/core/format.ts +309 -0
  211. package/src/core/measures.ts +99 -0
  212. package/src/core/request-ids.ts +54 -0
  213. package/src/core/rows.ts +213 -0
  214. package/src/core/store.ts +1122 -0
  215. package/src/core/totals.ts +59 -0
  216. package/src/core/trace.ts +44 -0
  217. package/src/core/transports.ts +177 -0
  218. package/src/core/types.ts +168 -0
  219. package/src/core.ts +39 -0
  220. package/src/index.ts +24 -12
  221. package/src/react/accessible-rows.tsx +242 -0
  222. package/src/react/cell-editor.tsx +216 -0
  223. package/src/react/cell-menu.tsx +166 -0
  224. package/src/react/column-menu.tsx +619 -0
  225. package/src/react/columns-menu.tsx +116 -0
  226. package/src/react/conditions-menu.tsx +289 -0
  227. package/src/react/context.ts +33 -0
  228. package/src/react/data-grid.tsx +286 -0
  229. package/src/react/detail-panel.tsx +92 -0
  230. package/src/react/export-menu.tsx +112 -0
  231. package/src/react/find-bar.tsx +112 -0
  232. package/src/react/format-form.tsx +421 -0
  233. package/src/react/formula-menu.tsx +163 -0
  234. package/src/react/grid-body.tsx +744 -0
  235. package/src/react/group-menu.tsx +214 -0
  236. package/src/react/hooks.ts +104 -0
  237. package/src/react/messages.ts +556 -0
  238. package/src/react/save.ts +42 -0
  239. package/src/react/static-rows.tsx +59 -0
  240. package/src/react/status-bar.tsx +175 -0
  241. package/src/react/toolbar-popover.tsx +69 -0
  242. package/src/react/toolbar.tsx +152 -0
  243. package/src/server.ts +58 -0
  244. package/src/styles.css +496 -316
  245. package/dist/components/bars.d.ts +0 -14
  246. package/dist/components/bars.d.ts.map +0 -1
  247. package/dist/components/bars.js +0 -67
  248. package/dist/components/bars.js.map +0 -1
  249. package/dist/components/cell-inspector.d.ts +0 -11
  250. package/dist/components/cell-inspector.d.ts.map +0 -1
  251. package/dist/components/cell-inspector.js +0 -37
  252. package/dist/components/cell-inspector.js.map +0 -1
  253. package/dist/components/columns-panel.d.ts +0 -12
  254. package/dist/components/columns-panel.d.ts.map +0 -1
  255. package/dist/components/columns-panel.js +0 -11
  256. package/dist/components/columns-panel.js.map +0 -1
  257. package/dist/components/filter-panel.d.ts +0 -17
  258. package/dist/components/filter-panel.d.ts.map +0 -1
  259. package/dist/components/filter-panel.js +0 -165
  260. package/dist/components/filter-panel.js.map +0 -1
  261. package/dist/components/find-bar.d.ts +0 -9
  262. package/dist/components/find-bar.d.ts.map +0 -1
  263. package/dist/components/find-bar.js +0 -46
  264. package/dist/components/find-bar.js.map +0 -1
  265. package/dist/components/popover.d.ts +0 -38
  266. package/dist/components/popover.d.ts.map +0 -1
  267. package/dist/components/popover.js +0 -104
  268. package/dist/components/popover.js.map +0 -1
  269. package/dist/components/stats-panel.d.ts +0 -12
  270. package/dist/components/stats-panel.d.ts.map +0 -1
  271. package/dist/components/stats-panel.js +0 -40
  272. package/dist/components/stats-panel.js.map +0 -1
  273. package/dist/controller.d.ts +0 -218
  274. package/dist/controller.d.ts.map +0 -1
  275. package/dist/controller.js +0 -788
  276. package/dist/controller.js.map +0 -1
  277. package/dist/data-grid.d.ts +0 -51
  278. package/dist/data-grid.d.ts.map +0 -1
  279. package/dist/data-grid.js +0 -698
  280. package/dist/data-grid.js.map +0 -1
  281. package/dist/labels.d.ts +0 -9
  282. package/dist/labels.d.ts.map +0 -1
  283. package/dist/labels.js +0 -38
  284. package/dist/labels.js.map +0 -1
  285. package/dist/messages.d.ts +0 -99
  286. package/dist/messages.d.ts.map +0 -1
  287. package/dist/messages.js +0 -276
  288. package/dist/messages.js.map +0 -1
  289. package/dist/renderer.d.ts +0 -52
  290. package/dist/renderer.d.ts.map +0 -1
  291. package/dist/renderer.js +0 -522
  292. package/dist/renderer.js.map +0 -1
  293. package/dist/text.d.ts +0 -14
  294. package/dist/text.d.ts.map +0 -1
  295. package/dist/text.js +0 -63
  296. package/dist/text.js.map +0 -1
  297. package/dist/theme.d.ts.map +0 -1
  298. package/dist/theme.js +0 -54
  299. package/dist/theme.js.map +0 -1
  300. package/src/components/bars.tsx +0 -179
  301. package/src/components/cell-inspector.tsx +0 -70
  302. package/src/components/columns-panel.tsx +0 -59
  303. package/src/components/filter-panel.tsx +0 -290
  304. package/src/components/find-bar.tsx +0 -104
  305. package/src/components/popover.tsx +0 -194
  306. package/src/components/stats-panel.tsx +0 -153
  307. package/src/controller.ts +0 -887
  308. package/src/data-grid.tsx +0 -1029
  309. package/src/labels.ts +0 -46
  310. package/src/messages.ts +0 -390
  311. package/src/renderer.ts +0 -535
  312. package/src/text.ts +0 -55
package/src/controller.ts DELETED
@@ -1,887 +0,0 @@
1
- import {
2
- type CellValue,
3
- ColumnLayout,
4
- type ColumnSchema,
5
- type ColumnStats,
6
- type DistinctValues,
7
- type Filter,
8
- isAbortError,
9
- isNumericType,
10
- plainText,
11
- RowCache,
12
- ScrollAxis,
13
- SelectionModel,
14
- type SelectionSummary,
15
- type SortDirection,
16
- type TableInfo,
17
- type TableSource,
18
- toHtmlTable,
19
- toMarkdownTable,
20
- toTsv,
21
- ValueFormatter,
22
- type ViewInfo,
23
- type ViewQuery,
24
- } from "vastgrid-core";
25
-
26
- export interface GridOptions {
27
- rowHeight: number;
28
- headerHeight: number;
29
- locale?: string;
30
- nullText: string;
31
- frozenColumns: number;
32
- minColumnWidth: number;
33
- maxAutoWidth: number;
34
- /** Cells copied at most at once. */
35
- copyLimit: number;
36
- }
37
-
38
- export interface ColumnState {
39
- /** Index in the table schema. */
40
- index: number;
41
- width: number;
42
- hidden: boolean;
43
- /** Width set by the user or fitted to the content, not to be changed automatically. */
44
- sized: boolean;
45
- }
46
-
47
- export type HitZone = "corner" | "header" | "gutter" | "cell" | "outside";
48
-
49
- export interface Hit {
50
- zone: HitZone;
51
- /** Row in the view, or -1. */
52
- row: number;
53
- /** Visible column, or -1. */
54
- col: number;
55
- /** Visible column whose right edge is under the pointer (for resizing), or -1. */
56
- resize: number;
57
- /** The pointer is over the header's menu button. */
58
- menu: boolean;
59
- }
60
-
61
- export class CopyTooLargeError extends Error {
62
- constructor(readonly limit: number) {
63
- super(`Select at most ${limit} cells to copy.`);
64
- }
65
- }
66
-
67
- const defaultWidths: Partial<Record<ColumnSchema["type"], number>> = {
68
- integer: 96,
69
- bigint: 120,
70
- float: 110,
71
- decimal: 110,
72
- boolean: 72,
73
- date: 104,
74
- time: 96,
75
- timestamp: 176,
76
- timestamptz: 196,
77
- uuid: 290,
78
- };
79
-
80
- /** Header space around the name: padding, the type mark and the menu button. */
81
- const HEADER_CHROME = 62;
82
-
83
- function sleep(ms: number, signal: AbortSignal) {
84
- return new Promise<void>((resolve) => {
85
- const timer = setTimeout(resolve, ms);
86
- signal.addEventListener(
87
- "abort",
88
- () => {
89
- clearTimeout(timer);
90
- resolve();
91
- },
92
- { once: true },
93
- );
94
- });
95
- }
96
-
97
- function messageOf(error: unknown) {
98
- return error instanceof Error ? error.message : String(error);
99
- }
100
-
101
- function hasQuery(query: ViewQuery) {
102
- return Boolean(query.sort?.length || query.filters?.length || query.search?.trim());
103
- }
104
-
105
- /** Filters on one column (the grid edits one filter per column). */
106
- export function filterColumn(filter: Filter) {
107
- return filter.column;
108
- }
109
-
110
- /**
111
- * State and behavior of one grid: the table and its view, columns, selection, scrolling and the row
112
- * cache. It is independent of React; the component forwards DOM events here and draws with
113
- * `CanvasRenderer`. Frequent changes (scrolling, hover) only request a frame; changes that the React
114
- * UI shows bump `version` and notify subscribers.
115
- */
116
- export class GridController {
117
- readonly source: TableSource;
118
- readonly options: GridOptions;
119
- readonly cache: RowCache;
120
- readonly selection = new SelectionModel();
121
- readonly scrollX = new ScrollAxis();
122
- readonly scrollY = new ScrollAxis();
123
- formatter: ValueFormatter;
124
- /** Changes whenever `formatter` is replaced, so that cached display text is rebuilt. */
125
- formatVersion = 0;
126
-
127
- info: TableInfo | null = null;
128
- loadError: string | null = null;
129
- query: ViewQuery = {};
130
- view: ViewInfo = { id: null, rowCount: 0, query: {} };
131
- viewPending = false;
132
- viewError: string | null = null;
133
- columns: ColumnState[] = [];
134
- visible: ColumnState[] = [];
135
- layout = new ColumnLayout([]);
136
- frozen = 0;
137
- frozenWidth = 0;
138
- gutterWidth = 48;
139
- width = 0;
140
- height = 0;
141
- hover: Hit | null = null;
142
- /** The visible column being resized, for drawing the guide. */
143
- resizing = -1;
144
- findText = "";
145
- findCaseSensitive = false;
146
- summary: SelectionSummary | null = null;
147
- summaryPending = false;
148
- /** Increments on every change shown by the React UI. */
149
- version = 0;
150
- /** Measures text in the cell or header font; set by the renderer. */
151
- measure: ((text: string, header: boolean) => number) | null = null;
152
-
153
- private scroller: HTMLElement | null = null;
154
- private sizer: HTMLElement | null = null;
155
- private readonly stateListeners = new Set<() => void>();
156
- private readonly renderListeners = new Set<() => void>();
157
- private lifecycle = new AbortController();
158
- private viewAbort: AbortController | null = null;
159
- private summaryAbort: AbortController | null = null;
160
- private summaryTimer: ReturnType<typeof setTimeout> | undefined;
161
- private autoSized = false;
162
- private disposed = false;
163
- private polling = false;
164
- private activeValue: CellValue | undefined;
165
-
166
- constructor(source: TableSource, options: GridOptions) {
167
- this.source = source;
168
- this.options = options;
169
- this.formatter = new ValueFormatter({ locale: options.locale, nullText: options.nullText });
170
- this.cache = new RowCache(source);
171
- this.cache.onChange(() => {
172
- this.autoSize();
173
- this.requestRender();
174
- const active = this.selection.active;
175
- const value = this.value(active.row, active.col);
176
- if (value !== this.activeValue) {
177
- this.activeValue = value;
178
- this.emit();
179
- }
180
- });
181
- }
182
-
183
- /** Changes the locale and the text for blanks. */
184
- setFormat(locale: string | undefined, nullText: string) {
185
- this.options.locale = locale;
186
- this.options.nullText = nullText;
187
- this.formatter = new ValueFormatter({ locale, nullText });
188
- this.formatVersion++;
189
- this.requestRender();
190
- }
191
-
192
- // ----- subscriptions -----
193
-
194
- subscribe = (listener: () => void) => {
195
- this.stateListeners.add(listener);
196
- return () => {
197
- this.stateListeners.delete(listener);
198
- };
199
- };
200
-
201
- getVersion = () => this.version;
202
-
203
- onRender(listener: () => void) {
204
- this.renderListeners.add(listener);
205
- return () => {
206
- this.renderListeners.delete(listener);
207
- };
208
- }
209
-
210
- emit() {
211
- this.version++;
212
- for (const listener of this.stateListeners) listener();
213
- }
214
-
215
- requestRender() {
216
- for (const listener of this.renderListeners) listener();
217
- }
218
-
219
- // ----- lifecycle -----
220
-
221
- attach(scroller: HTMLElement, sizer: HTMLElement) {
222
- this.scroller = scroller;
223
- this.sizer = sizer;
224
- this.syncSizer();
225
- }
226
-
227
- /** Starts reading the table. Safe to call again after `stop()`. */
228
- start() {
229
- if (this.polling && !this.lifecycle.signal.aborted) return;
230
- if (this.lifecycle.signal.aborted) this.lifecycle = new AbortController();
231
- this.polling = true;
232
- const signal = this.lifecycle.signal;
233
- void this.poll(signal).finally(() => {
234
- if (this.lifecycle.signal === signal) this.polling = false;
235
- });
236
- }
237
-
238
- /** Reads the table again (for example after the file changed). */
239
- reload() {
240
- this.lifecycle.abort();
241
- this.polling = false;
242
- this.loadError = null;
243
- this.start();
244
- }
245
-
246
- /** Stops loading and cancels requests. `start()` resumes. */
247
- stop() {
248
- this.lifecycle.abort();
249
- this.polling = false;
250
- this.viewAbort?.abort();
251
- this.summaryAbort?.abort();
252
- clearTimeout(this.summaryTimer);
253
- if (this.viewPending) {
254
- this.viewPending = false;
255
- this.emit();
256
- }
257
- }
258
-
259
- dispose() {
260
- this.disposed = true;
261
- this.stop();
262
- this.cache.clear();
263
- this.stateListeners.clear();
264
- this.renderListeners.clear();
265
- }
266
-
267
- private async poll(signal: AbortSignal) {
268
- let delay = 100;
269
- while (!signal.aborted) {
270
- let info: TableInfo;
271
- try {
272
- info = await this.source.info(signal);
273
- } catch (error) {
274
- if (signal.aborted) return;
275
- if (isAbortError(error)) {
276
- // Someone else's request was cancelled; ask again.
277
- await sleep(delay, signal);
278
- continue;
279
- }
280
- this.loadError = messageOf(error);
281
- this.emit();
282
- this.requestRender();
283
- return;
284
- }
285
- if (signal.aborted) return;
286
- this.applyInfo(info);
287
- if (info.status !== "loading") return;
288
- await sleep(delay, signal);
289
- delay = Math.min(500, delay * 1.4);
290
- }
291
- }
292
-
293
- private applyInfo(info: TableInfo) {
294
- const previous = this.info;
295
- this.info = info;
296
- this.loadError = null;
297
- const names = (table: TableInfo | null) => table?.columns.map((column) => column.name).join("\u0000");
298
- const types = (table: TableInfo | null) => table?.columns.map((column) => column.type).join("\u0000");
299
- const reset = !previous || previous.revision !== info.revision || names(previous) !== names(info);
300
- if (reset) {
301
- const old = new Map(this.columns.map((column) => [info.columns[column.index]?.name ?? "", column]));
302
- this.columns = info.columns.map((column, index) => {
303
- const kept =
304
- previous && names(previous) === names(info)
305
- ? this.columns.find((item) => item.index === index)
306
- : old.get(column.name);
307
- return {
308
- index,
309
- width: kept?.width ?? defaultWidths[column.type] ?? 150,
310
- hidden: kept?.hidden ?? false,
311
- sized: kept?.sized ?? false,
312
- };
313
- });
314
- if (!previous) this.frozen = Math.min(this.options.frozenColumns, this.columns.length);
315
- this.autoSized = this.columns.every((column) => column.sized);
316
- this.viewAbort?.abort();
317
- this.view = { id: null, rowCount: info.rowCount ?? info.readableRows, query: {} };
318
- this.viewPending = false;
319
- this.cache.clear();
320
- this.selection.select({ row: 0, col: 0 });
321
- } else if (previous.status !== info.status && types(previous) !== types(info)) {
322
- // The final types differ from the preview's (for example all columns read as text).
323
- this.cache.clear();
324
- }
325
- if (this.view.id === null) {
326
- this.view = { id: null, rowCount: info.rowCount ?? info.readableRows, query: {} };
327
- }
328
- this.updateLayout();
329
- if (info.status === "ready" && (reset || previous?.status !== "ready") && hasQuery(this.query)) {
330
- void this.setQuery(this.query);
331
- }
332
- this.emit();
333
- this.requestRender();
334
- }
335
-
336
- // ----- derived values -----
337
-
338
- get status() {
339
- return this.loadError ? "error" : (this.info?.status ?? "loading");
340
- }
341
-
342
- get schema(): ColumnSchema[] {
343
- return this.info?.columns ?? [];
344
- }
345
-
346
- /** Rows in the current view. */
347
- get rowCount() {
348
- return this.view.id !== null ? this.view.rowCount : (this.info?.rowCount ?? this.info?.readableRows ?? 0);
349
- }
350
-
351
- /** Rows that can be read now (less than `rowCount` while a table loads). */
352
- get readableRows() {
353
- return this.view.id !== null ? this.view.rowCount : (this.info?.readableRows ?? 0);
354
- }
355
-
356
- get totalRows() {
357
- return this.info?.rowCount ?? null;
358
- }
359
-
360
- columnSchema(col: number): ColumnSchema | undefined {
361
- const state = this.visible[col];
362
- return state ? this.schema[state.index] : undefined;
363
- }
364
-
365
- get bodyHeight() {
366
- return Math.max(0, this.height - this.options.headerHeight);
367
- }
368
-
369
- /** Row labels: the position in the original table for views, otherwise the row number. */
370
- rowLabel(row: number) {
371
- if (this.view.id === null) return row + 1;
372
- const id = this.cache.rowId(row);
373
- return id === undefined ? null : id + 1;
374
- }
375
-
376
- updateLayout() {
377
- this.visible = this.columns.filter((column) => !column.hidden);
378
- this.frozen = Math.min(this.frozen, this.visible.length);
379
- this.layout = new ColumnLayout(this.visible.map((column) => column.width));
380
- this.frozenWidth = this.layout.left(this.frozen);
381
- const largest = Math.max(this.info?.rowCount ?? 0, this.rowCount, 1);
382
- const digits = String(largest).length;
383
- this.gutterWidth = Math.max(
384
- 44,
385
- Math.ceil(this.measure ? this.measure("0".repeat(digits), false) : digits * 8) + 22,
386
- );
387
- this.selection.setBounds(this.rowCount, this.visible.length);
388
- this.cache.setView(this.view.id, this.rowCount, this.readableRows, this.schema.length);
389
- this.syncSizer();
390
- }
391
-
392
- /** Sizes the native scroll content and applies the current positions. */
393
- syncSizer() {
394
- const nativeWidth = this.scrollX.resize(this.gutterWidth + this.layout.total, this.width);
395
- const nativeHeight = this.scrollY.resize(
396
- this.options.headerHeight + this.rowCount * this.options.rowHeight,
397
- this.height,
398
- );
399
- if (this.sizer) {
400
- this.sizer.style.width = `${Math.ceil(nativeWidth)}px`;
401
- this.sizer.style.height = `${Math.ceil(nativeHeight)}px`;
402
- }
403
- if (this.scroller) {
404
- const left = this.scrollX.scrollTo(this.scrollX.position);
405
- const top = this.scrollY.scrollTo(this.scrollY.position);
406
- if (Math.abs(this.scroller.scrollLeft - left) > 1) this.scroller.scrollLeft = left;
407
- if (Math.abs(this.scroller.scrollTop - top) > 1) this.scroller.scrollTop = top;
408
- }
409
- }
410
-
411
- setViewport(width: number, height: number) {
412
- if (width === this.width && height === this.height) return;
413
- this.width = width;
414
- this.height = height;
415
- this.syncSizer();
416
- this.requestRender();
417
- }
418
-
419
- handleScroll() {
420
- if (!this.scroller) return;
421
- this.scrollX.onNativeScroll(this.scroller.scrollLeft);
422
- this.scrollY.onNativeScroll(this.scroller.scrollTop);
423
- this.requestRender();
424
- }
425
-
426
- /** Visible rows `[first, last)`. */
427
- visibleRows(): [number, number] {
428
- const rowHeight = this.options.rowHeight;
429
- const first = Math.max(0, Math.floor(this.scrollY.position / rowHeight));
430
- const last = Math.min(this.rowCount, Math.ceil((this.scrollY.position + this.bodyHeight) / rowHeight));
431
- return [first, Math.max(first, last)];
432
- }
433
-
434
- /** Visible scrollable columns `[first, last]` (inclusive), or `[0, -1]`. */
435
- visibleScrollColumns(): [number, number] {
436
- const count = this.visible.length;
437
- if (count <= this.frozen) return [0, -1];
438
- const start = this.frozenWidth + this.scrollX.position;
439
- const end = this.scrollX.position + this.width - this.gutterWidth;
440
- if (start >= this.layout.total) return [0, -1];
441
- const first = Math.max(this.frozen, this.layout.indexAt(start));
442
- const last = Math.min(count - 1, this.layout.indexAt(Math.max(start, end - 0.001)));
443
- return [first, last];
444
- }
445
-
446
- /** Screen x of a visible column's left edge. */
447
- columnX(col: number) {
448
- return col < this.frozen
449
- ? this.gutterWidth + this.layout.left(col)
450
- : this.gutterWidth + this.layout.left(col) - this.scrollX.position;
451
- }
452
-
453
- rowY(row: number) {
454
- return this.options.headerHeight + row * this.options.rowHeight - this.scrollY.position;
455
- }
456
-
457
- /** The visible column at screen x, or -1. */
458
- columnAt(x: number) {
459
- if (x < this.gutterWidth || this.visible.length === 0) return -1;
460
- const frozenRight = this.gutterWidth + this.frozenWidth;
461
- if (x < frozenRight) return this.layout.indexAt(x - this.gutterWidth);
462
- const position = x - this.gutterWidth + this.scrollX.position;
463
- if (position >= this.layout.total) return -1;
464
- return Math.max(this.frozen, this.layout.indexAt(position));
465
- }
466
-
467
- rowAt(y: number) {
468
- if (y < this.options.headerHeight) return -1;
469
- const row = Math.floor((y - this.options.headerHeight + this.scrollY.position) / this.options.rowHeight);
470
- return row >= 0 && row < this.rowCount ? row : -1;
471
- }
472
-
473
- hitTest(x: number, y: number): Hit {
474
- const header = y < this.options.headerHeight;
475
- const gutter = x < this.gutterWidth;
476
- const col = gutter ? -1 : this.columnAt(x);
477
- const row = header ? -1 : this.rowAt(y);
478
- let zone: HitZone = "outside";
479
- if (header && gutter) zone = "corner";
480
- else if (header && col >= 0) zone = "header";
481
- else if (gutter && row >= 0) zone = "gutter";
482
- else if (row >= 0 && col >= 0) zone = "cell";
483
- let resize = -1;
484
- let menu = false;
485
- if (header && !gutter) {
486
- const nearest = col >= 0 ? col : this.visible.length - 1;
487
- const right = this.columnX(nearest) + this.layout.width(nearest);
488
- const left = this.columnX(nearest);
489
- if (Math.abs(x - right) <= 5) resize = nearest;
490
- else if (nearest > 0 && Math.abs(x - left) <= 5 && (nearest !== this.frozen || this.scrollX.position === 0))
491
- resize = nearest - 1;
492
- menu = col >= 0 && resize < 0 && x >= right - 24 && x < right - 4;
493
- }
494
- return { zone, row, col, resize, menu };
495
- }
496
-
497
- // ----- scrolling -----
498
-
499
- scrollToVirtual(left: number | null, top: number | null) {
500
- if (!this.scroller) return;
501
- if (top !== null) this.scroller.scrollTop = this.scrollY.scrollTo(top);
502
- if (left !== null) this.scroller.scrollLeft = this.scrollX.scrollTo(left);
503
- this.requestRender();
504
- }
505
-
506
- /** Scrolls the least amount that shows the cell. */
507
- ensureVisible(row: number, col: number) {
508
- const rowHeight = this.options.rowHeight;
509
- let top: number | null = null;
510
- if (row >= 0) {
511
- const y = row * rowHeight;
512
- if (y < this.scrollY.position) top = y;
513
- else if (y + rowHeight > this.scrollY.position + this.bodyHeight) top = y + rowHeight - this.bodyHeight;
514
- }
515
- let left: number | null = null;
516
- if (col >= this.frozen && col < this.visible.length) {
517
- const start = this.layout.left(col) - this.frozenWidth;
518
- const end = this.layout.right(col) - this.frozenWidth;
519
- const view = this.width - this.gutterWidth - this.frozenWidth;
520
- if (start < this.scrollX.position) left = start;
521
- else if (end > this.scrollX.position + view) left = Math.min(start, end - view);
522
- }
523
- if (top !== null || left !== null) this.scrollToVirtual(left, top);
524
- }
525
-
526
- // ----- selection -----
527
-
528
- /** Rows a page key moves. */
529
- get pageRows() {
530
- return Math.max(1, Math.floor(this.bodyHeight / this.options.rowHeight) - 1);
531
- }
532
-
533
- selectionChanged(scrollTo?: { row: number; col: number }) {
534
- if (scrollTo) this.ensureVisible(scrollTo.row, scrollTo.col);
535
- this.scheduleSummary();
536
- this.requestRender();
537
- this.emit();
538
- }
539
-
540
- private scheduleSummary() {
541
- clearTimeout(this.summaryTimer);
542
- this.summaryAbort?.abort();
543
- const ranges = this.selectionRanges();
544
- const cells = ranges.reduce((total, range) => total + (range.end - range.start) * range.columns.length, 0);
545
- if (cells <= 1 || !this.source.aggregate || this.info?.status !== "ready") {
546
- this.summary = null;
547
- this.summaryPending = false;
548
- return;
549
- }
550
- this.summaryPending = true;
551
- this.summaryTimer = setTimeout(() => {
552
- const controller = new AbortController();
553
- this.summaryAbort = controller;
554
- this.source
555
- .aggregate?.({ view: this.view.id, ranges }, controller.signal)
556
- .then((summary) => {
557
- if (controller.signal.aborted) return;
558
- this.summary = summary;
559
- this.summaryPending = false;
560
- this.emit();
561
- })
562
- .catch((error: unknown) => {
563
- if (controller.signal.aborted || isAbortError(error)) return;
564
- this.summary = null;
565
- this.summaryPending = false;
566
- this.emit();
567
- });
568
- }, 180);
569
- }
570
-
571
- /** The selection as row ranges of schema columns. */
572
- selectionRanges() {
573
- return this.selection.ranges.map((range) => ({
574
- start: range.top,
575
- end: range.bottom + 1,
576
- columns: this.visible.slice(range.left, range.right + 1).map((column) => column.index),
577
- }));
578
- }
579
-
580
- // ----- reading -----
581
-
582
- value(row: number, col: number): CellValue | undefined {
583
- const state = this.visible[col];
584
- return state ? this.cache.value(row, state.index) : undefined;
585
- }
586
-
587
- /** Reads a cell with a large text limit, for showing its full value. */
588
- async fullValue(row: number, col: number, signal?: AbortSignal): Promise<CellValue> {
589
- const state = this.visible[col];
590
- if (!state) return null;
591
- const block = await this.source.readRows(
592
- { view: this.view.id, start: row, end: row + 1, columns: [state.index], textLimit: 1_000_000 },
593
- signal,
594
- );
595
- return block.columns[0]?.[0] ?? null;
596
- }
597
-
598
- /** Builds clipboard text from values per column. */
599
- private clipboardText(
600
- columns: ColumnState[],
601
- values: CellValue[][],
602
- rows: number,
603
- options: { headers?: boolean; markdown?: boolean },
604
- ) {
605
- const header = columns.map((column) => this.schema[column.index]?.name ?? "");
606
- const body: string[][] = [];
607
- for (let row = 0; row < rows; row++) body.push(values.map((column) => plainText(column[row] ?? null)));
608
- if (options.markdown) return { text: toMarkdownTable(header, body), html: null, cells: rows * columns.length };
609
- const table = options.headers ? [header, ...body] : body;
610
- return { text: toTsv(table), html: toHtmlTable(table, options.headers ? 1 : 0), cells: rows * columns.length };
611
- }
612
-
613
- /** The current range's values from the cache, or null when some are not loaded (or it is large). */
614
- private cachedRange() {
615
- const range = this.selection.current;
616
- if (!range) return null;
617
- const columns = this.visible.slice(range.left, range.right + 1);
618
- const rows = range.bottom - range.top + 1;
619
- if (rows * columns.length > 50_000) return null;
620
- const values: CellValue[][] = [];
621
- for (const column of columns) {
622
- const list: CellValue[] = [];
623
- for (let row = range.top; row <= range.bottom; row++) {
624
- const value = this.cache.value(row, column.index);
625
- if (value === undefined) return null;
626
- list.push(value);
627
- }
628
- values.push(list);
629
- }
630
- return { columns, values, rows };
631
- }
632
-
633
- /** Clipboard text for the current range when every cell is loaded; otherwise null (use `copySelection`). */
634
- copySelectionSync(options: { headers?: boolean; markdown?: boolean }) {
635
- const cached = this.cachedRange();
636
- return cached ? this.clipboardText(cached.columns, cached.values, cached.rows, options) : null;
637
- }
638
-
639
- /** Copies the current range as TSV and HTML (or Markdown), reading cells that are not loaded. */
640
- async copySelection(
641
- options: { headers?: boolean; markdown?: boolean },
642
- signal?: AbortSignal,
643
- ): Promise<{ text: string; html: string | null; cells: number }> {
644
- const range = this.selection.current;
645
- if (!range) return { text: "", html: null, cells: 0 };
646
- const columns = this.visible.slice(range.left, range.right + 1);
647
- const rows = range.bottom - range.top + 1;
648
- if (rows * columns.length > this.options.copyLimit) throw new CopyTooLargeError(this.options.copyLimit);
649
- const cached = this.cachedRange();
650
- if (cached) return this.clipboardText(cached.columns, cached.values, cached.rows, options);
651
- const values = await this.cache.read(
652
- range.top,
653
- range.bottom + 1,
654
- columns.map((column) => column.index),
655
- signal,
656
- );
657
- return this.clipboardText(columns, values, rows, options);
658
- }
659
-
660
- // ----- views -----
661
-
662
- async setQuery(query: ViewQuery) {
663
- this.query = query;
664
- this.viewAbort?.abort();
665
- if (this.info?.status !== "ready") {
666
- this.emit();
667
- return;
668
- }
669
- const controller = new AbortController();
670
- this.viewAbort = controller;
671
- this.viewPending = true;
672
- this.viewError = null;
673
- this.emit();
674
- this.requestRender();
675
- try {
676
- const view = await this.source.createView(query, controller.signal);
677
- if (controller.signal.aborted || this.disposed) return;
678
- this.view = view;
679
- this.viewPending = false;
680
- this.cache.clear();
681
- this.updateLayout();
682
- this.selection.select({ row: 0, col: Math.min(this.selection.active.col, Math.max(0, this.visible.length - 1)) });
683
- this.scrollToVirtual(null, 0);
684
- this.scheduleSummary();
685
- } catch (error) {
686
- if (controller.signal.aborted || isAbortError(error)) return;
687
- if (error instanceof Error && "code" in error && (error as { code: string }).code === "cancelled") return;
688
- this.viewPending = false;
689
- this.viewError = messageOf(error);
690
- }
691
- this.emit();
692
- this.requestRender();
693
- }
694
-
695
- sortBy(col: number, direction: SortDirection, add = false) {
696
- const state = this.visible[col];
697
- if (!state) return;
698
- const others = add ? (this.query.sort ?? []).filter((key) => key.column !== state.index) : [];
699
- void this.setQuery({ ...this.query, sort: [...others, { column: state.index, direction }] });
700
- }
701
-
702
- clearSort() {
703
- void this.setQuery({ ...this.query, sort: [] });
704
- }
705
-
706
- /** Replaces the filter of a schema column (`null` removes it). */
707
- setFilter(column: number, filter: Filter | null) {
708
- const filters = (this.query.filters ?? []).filter((item) => item.column !== column);
709
- if (filter) filters.push(filter);
710
- void this.setQuery({ ...this.query, filters });
711
- }
712
-
713
- filterOf(column: number) {
714
- return this.query.filters?.find((filter) => filter.column === column) ?? null;
715
- }
716
-
717
- clearFilters() {
718
- void this.setQuery({ ...this.query, filters: [], search: "" });
719
- }
720
-
721
- setSearch(search: string) {
722
- void this.setQuery({ ...this.query, search });
723
- }
724
-
725
- sortOf(column: number): { direction: SortDirection; order: number } | null {
726
- const sort = this.query.sort ?? [];
727
- const index = sort.findIndex((key) => key.column === column);
728
- return index < 0 ? null : { direction: sort[index].direction, order: index + 1 };
729
- }
730
-
731
- // ----- columns -----
732
-
733
- private changeColumns() {
734
- this.updateLayout();
735
- this.selectionChanged();
736
- }
737
-
738
- resizeColumn(col: number, width: number) {
739
- const state = this.visible[col];
740
- if (!state) return;
741
- state.width = Math.max(this.options.minColumnWidth, Math.round(width));
742
- state.sized = true;
743
- this.updateLayout();
744
- this.requestRender();
745
- }
746
-
747
- hideColumn(col: number) {
748
- const state = this.visible[col];
749
- if (!state || this.visible.length <= 1) return;
750
- state.hidden = true;
751
- if (col < this.frozen) this.frozen--;
752
- this.changeColumns();
753
- }
754
-
755
- setColumnHidden(index: number, hidden: boolean) {
756
- const state = this.columns.find((column) => column.index === index);
757
- if (!state) return;
758
- if (hidden && this.visible.length <= 1) return;
759
- state.hidden = hidden;
760
- this.changeColumns();
761
- }
762
-
763
- showAllColumns() {
764
- for (const column of this.columns) column.hidden = false;
765
- this.changeColumns();
766
- }
767
-
768
- freezeUpTo(col: number) {
769
- this.frozen = Math.max(0, Math.min(col + 1, this.visible.length));
770
- this.scrollToVirtual(0, null);
771
- this.changeColumns();
772
- }
773
-
774
- /** Moves a visible column before another visible column. */
775
- moveColumn(from: number, to: number) {
776
- const moving = this.visible[from];
777
- const target = this.visible[to];
778
- if (!moving || !target || from === to) return;
779
- const list = this.columns.filter((column) => column !== moving);
780
- const at = list.indexOf(target);
781
- list.splice(from < to ? at + 1 : at, 0, moving);
782
- this.columns = list;
783
- this.changeColumns();
784
- }
785
-
786
- /** Width that fits the header and the loaded values of a column. */
787
- fittedWidth(col: number) {
788
- const state = this.visible[col];
789
- const schema = this.columnSchema(col);
790
- if (!state || !schema || !this.measure) return state?.width ?? 120;
791
- let widest = this.measure(schema.name, true) + HEADER_CHROME;
792
- const [first, last] = this.visibleRows();
793
- const end = Math.min(this.readableRows, Math.max(last, first + 200));
794
- for (let row = first; row < end; row++) {
795
- const value = this.cache.value(row, state.index);
796
- if (value === undefined) continue;
797
- widest = Math.max(widest, this.measure(this.formatter.format(value, schema.type), false) + 18);
798
- }
799
- return Math.max(this.options.minColumnWidth, Math.ceil(widest));
800
- }
801
-
802
- autoFit(col: number) {
803
- this.resizeColumn(col, Math.min(this.fittedWidth(col), 1000));
804
- }
805
-
806
- /**
807
- * Widens columns whose numbers did not fit (numbers are never cut, since a cut number reads as a
808
- * different number). Called by the renderer with the widths it needed; columns the user sized
809
- * keep their width.
810
- */
811
- growColumns(widths: Map<number, number>) {
812
- let changed = false;
813
- for (const [col, width] of widths) {
814
- const state = this.visible[col];
815
- if (!state || state.sized || width <= state.width) continue;
816
- state.width = Math.min(Math.ceil(width), 600);
817
- changed = true;
818
- }
819
- if (changed) {
820
- this.updateLayout();
821
- this.requestRender();
822
- }
823
- }
824
-
825
- /** Fits columns to the first loaded rows once, unless the user sized them. */
826
- private autoSize() {
827
- if (this.autoSized || !this.measure || this.view.id !== null || this.readableRows === 0) return;
828
- const first = this.visible[0];
829
- if (!first || this.cache.value(0, first.index) === undefined) return;
830
- this.autoSized = true;
831
- this.visible.forEach((state, col) => {
832
- if (state.sized) return;
833
- const schema = this.schema[state.index];
834
- if (!schema || !this.measure) return;
835
- let widest = this.measure(schema.name, true) + HEADER_CHROME;
836
- for (let row = 0; row < Math.min(this.readableRows, 200); row++) {
837
- const value = this.cache.value(row, state.index);
838
- if (value === undefined || value === null) continue;
839
- widest = Math.max(widest, this.measure(this.formatter.format(value, schema.type), false) + 18);
840
- }
841
- const cap = isNumericType(schema.type) ? 240 : this.options.maxAutoWidth;
842
- state.width = Math.max(this.options.minColumnWidth, Math.min(cap, Math.ceil(widest)));
843
- void col;
844
- });
845
- this.updateLayout();
846
- this.emit();
847
- }
848
-
849
- // ----- statistics, values, find -----
850
-
851
- stats(col: number, signal?: AbortSignal): Promise<ColumnStats> | null {
852
- const state = this.visible[col];
853
- if (!state || !this.source.columnStats) return null;
854
- return this.source.columnStats(this.view.id, state.index, signal);
855
- }
856
-
857
- distinct(column: number, search: string, signal?: AbortSignal): Promise<DistinctValues> | null {
858
- if (!this.source.distinctValues) return null;
859
- // Values come from the table as a whole, so that filtering a column keeps showing its other values.
860
- return this.source.distinctValues({ view: null, column, search, limit: 1000 }, signal);
861
- }
862
-
863
- /** Moves to the next cell containing `text`. Returns whether one was found. */
864
- async find(text: string, direction: 1 | -1, caseSensitive: boolean, signal?: AbortSignal) {
865
- this.findText = text;
866
- this.findCaseSensitive = caseSensitive;
867
- this.requestRender();
868
- if (!text || !this.source.find) return false;
869
- const active = this.selection.active;
870
- const result = await this.source.find(
871
- {
872
- view: this.view.id,
873
- text,
874
- columns: this.visible.map((column) => column.index),
875
- row: Math.min(active.row, Math.max(0, this.rowCount - 1)),
876
- column: active.col,
877
- direction,
878
- caseSensitive,
879
- },
880
- signal,
881
- );
882
- if (!result || signal?.aborted) return false;
883
- this.selection.select({ row: result.row, col: result.column });
884
- this.selectionChanged(result ? { row: result.row, col: result.column } : undefined);
885
- return true;
886
- }
887
- }