flexdesk 0.16.0 → 0.17.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 (251) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2261 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3373 -238
  7. package/css/overrides.css +16 -0
  8. package/css/reset.css +0 -0
  9. package/css/tokens.css +0 -0
  10. package/dist/canvas.js +6 -239
  11. package/dist/canvas.js.map +3 -3
  12. package/dist/charts.js +709 -18
  13. package/dist/charts.js.map +4 -4
  14. package/dist/chunk-3PHCPZHT.js +0 -0
  15. package/dist/chunk-3PHCPZHT.js.map +0 -0
  16. package/dist/chunk-4A7SJC3T.js +503 -0
  17. package/dist/chunk-4A7SJC3T.js.map +7 -0
  18. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  19. package/dist/chunk-CVMCZZX2.js.map +7 -0
  20. package/dist/chunk-D3COLZZ2.js +1311 -0
  21. package/dist/chunk-D3COLZZ2.js.map +7 -0
  22. package/dist/chunk-FOOS3T5L.js +0 -0
  23. package/dist/chunk-FOOS3T5L.js.map +0 -0
  24. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  25. package/dist/chunk-GOQLR5WR.js.map +7 -0
  26. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  27. package/dist/chunk-HNU6MM2B.js.map +7 -0
  28. package/dist/chunk-HYJYYEPK.js +375 -0
  29. package/dist/chunk-HYJYYEPK.js.map +7 -0
  30. package/dist/chunk-JKZLNKG6.js +2041 -0
  31. package/dist/chunk-JKZLNKG6.js.map +7 -0
  32. package/dist/chunk-JYWURG5T.js +0 -0
  33. package/dist/chunk-JYWURG5T.js.map +0 -0
  34. package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
  35. package/dist/chunk-MCIXFNVF.js.map +7 -0
  36. package/dist/chunk-N7HC3ADV.js +30 -0
  37. package/dist/chunk-N7HC3ADV.js.map +7 -0
  38. package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
  39. package/dist/chunk-Q36EPT2R.js.map +7 -0
  40. package/dist/chunk-QWQJUDPD.js +610 -0
  41. package/dist/chunk-QWQJUDPD.js.map +7 -0
  42. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  43. package/dist/chunk-UURZ37SD.js.map +7 -0
  44. package/dist/chunk-UWO43BZU.js +111 -0
  45. package/dist/chunk-UWO43BZU.js.map +7 -0
  46. package/dist/chunk-WVFGV5FT.js +0 -0
  47. package/dist/chunk-WVFGV5FT.js.map +0 -0
  48. package/dist/core.js +170 -118
  49. package/dist/core.js.map +2 -2
  50. package/dist/editor.js +156 -70
  51. package/dist/editor.js.map +2 -2
  52. package/dist/flexdesk.css +3373 -238
  53. package/dist/flow.js +2789 -306
  54. package/dist/flow.js.map +4 -4
  55. package/dist/host.js +109 -3
  56. package/dist/host.js.map +3 -3
  57. package/dist/reset.css +0 -0
  58. package/dist/tile_registry-6WZPTUZV.js +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  60. package/dist/tiles.js +1131 -253
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3254 -2112
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -345
  66. package/dist/wm.js.map +4 -4
  67. package/package.json +3 -1
  68. package/src/canvas/canvas_adapter.js +5 -0
  69. package/src/canvas/edge_router.js +0 -0
  70. package/src/canvas/graph/connector_router.js +0 -0
  71. package/src/canvas/graph/history_service.js +0 -0
  72. package/src/canvas/graph/node_platform.js +0 -0
  73. package/src/canvas/index.js +1 -0
  74. package/src/canvas/layered_layout.js +244 -0
  75. package/src/charts/chart_types.js +0 -0
  76. package/src/charts/data_series_plot_window.js +0 -0
  77. package/src/charts/downsample.js +2 -2
  78. package/src/charts/plot_config.js +53 -29
  79. package/src/charts/plot_config_editor.js +627 -0
  80. package/src/charts/plot_grid.js +449 -0
  81. package/src/charts/plot_popout_window.js +103 -317
  82. package/src/charts/plotly_wrapper.js +22 -2
  83. package/src/charts/series_transforms.js +206 -0
  84. package/src/core/event_bus.js +0 -0
  85. package/src/core/logging.js +1 -1
  86. package/src/core/settings.js +105 -19
  87. package/src/core/state_guard.js +93 -51
  88. package/src/core/state_guard_config.js +85 -74
  89. package/src/core/state_machine.js +0 -0
  90. package/src/editor/editor_pane.js +3 -3
  91. package/src/editor/monaco_editor_factory.js +124 -60
  92. package/src/editor/monaco_loader.js +103 -28
  93. package/src/editor/notebook_search.js +0 -0
  94. package/src/editor/notebook_tab_bar.js +38 -52
  95. package/src/editor/notebook_undo_manager.js +0 -0
  96. package/src/editor/split_pane_container.js +6 -3
  97. package/src/flow/canvas_editor/connections.js +90 -0
  98. package/src/flow/canvas_editor/editor.js +2351 -153
  99. package/src/flow/canvas_editor/index.js +2 -1
  100. package/src/flow/canvas_editor/layout.js +119 -12
  101. package/src/flow/canvas_editor/strings.js +80 -0
  102. package/src/flow/kit/always_before.js +0 -0
  103. package/src/flow/kit/catalogue.js +97 -3
  104. package/src/flow/kit/chip_input.js +0 -0
  105. package/src/flow/kit/dom.js +0 -0
  106. package/src/flow/kit/findings.js +0 -0
  107. package/src/flow/kit/history.js +0 -0
  108. package/src/flow/kit/index.js +3 -3
  109. package/src/flow/kit/keys.js +7 -1
  110. package/src/flow/kit/logic_graph.js +97 -15
  111. package/src/flow/kit/references.js +0 -0
  112. package/src/flow/kit/settings_panel.js +0 -0
  113. package/src/flow/kit/settings_schema.js +2 -1
  114. package/src/flow/kit/step_picker.js +0 -0
  115. package/src/flow/kit/strings.js +3 -1
  116. package/src/flow/kit/value_picker.js +0 -0
  117. package/src/flow/kit/widgets.js +86 -109
  118. package/src/flow/lanes/editor.js +136 -14
  119. package/src/flow/lanes/index.js +3 -1
  120. package/src/flow/lanes/layout.js +0 -0
  121. package/src/flow/lanes/pipeline.js +44 -2
  122. package/src/flow/lanes/preview.js +0 -0
  123. package/src/flow/lanes/strings.js +1 -0
  124. package/src/flow/outline/editor.js +0 -0
  125. package/src/flow/outline/index.js +0 -0
  126. package/src/flow/outline/mapping.js +0 -0
  127. package/src/flow/outline/menu.js +0 -0
  128. package/src/flow/outline/operations.js +0 -0
  129. package/src/flow/outline/recognise.js +0 -0
  130. package/src/flow/outline/strings.js +0 -0
  131. package/src/help/help_modal.js +168 -42
  132. package/src/help/help_registry.js +12 -1
  133. package/src/host/external_links.js +0 -0
  134. package/src/host/file_picker.js +117 -0
  135. package/src/host/host.js +10 -2
  136. package/src/host/photino_host.js +9 -0
  137. package/src/host/photino_host.md +21 -3
  138. package/src/host/pywebview_host.js +10 -0
  139. package/src/tiles/chart_tiles.js +53 -245
  140. package/src/tiles/config_schema.js +8 -0
  141. package/src/tiles/figure_tile.js +322 -0
  142. package/src/tiles/layout_persistence.js +3 -3
  143. package/src/tiles/list_tiles.js +422 -0
  144. package/src/tiles/scope_picker.js +96 -0
  145. package/src/tiles/tile_base.js +12 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +38 -1
  148. package/src/tiles/tile_registry.js +0 -0
  149. package/src/tiling/bottom_panel_tabs.js +67 -99
  150. package/src/tiling/command_palette.js +64 -19
  151. package/src/tiling/content_registry.js +4 -1
  152. package/src/tiling/desktops.js +11 -1
  153. package/src/tiling/entity_sources.js +0 -0
  154. package/src/tiling/keymap.js +403 -128
  155. package/src/tiling/kind_taxonomy.js +78 -0
  156. package/src/tiling/landing_table.js +0 -0
  157. package/src/tiling/loading_overlay.js +0 -0
  158. package/src/tiling/nav_panel.js +59 -10
  159. package/src/tiling/page_factory.js +5 -5
  160. package/src/tiling/panel_availability.js +49 -0
  161. package/src/tiling/panel_keys.js +0 -0
  162. package/src/tiling/shell.js +114 -12
  163. package/src/tiling/tab_strip.js +0 -0
  164. package/src/tiling/tile_breadcrumb.js +0 -0
  165. package/src/tiling/tile_renderer.js +105 -9
  166. package/src/tiling/tile_tab_menu.js +0 -0
  167. package/src/tiling/tile_tree.js +0 -0
  168. package/src/tiling/wm.js +562 -30
  169. package/src/tiling/zoom.js +0 -0
  170. package/src/ui/base/component_base.js +0 -0
  171. package/src/ui/base/controller_base.js +0 -0
  172. package/src/ui/base/page_base.js +0 -0
  173. package/src/ui/components/about_dialog.js +0 -0
  174. package/src/ui/components/action_dropdown.js +30 -2
  175. package/src/ui/components/alert_log.js +0 -0
  176. package/src/ui/components/alerts_pane.js +0 -0
  177. package/src/ui/components/attribute_list_editor.js +0 -0
  178. package/src/ui/components/autocomplete_field.js +0 -0
  179. package/src/ui/components/cell_editor_host.js +211 -0
  180. package/src/ui/components/cell_grid.js +648 -0
  181. package/src/ui/components/cell_selection.js +220 -0
  182. package/src/ui/components/choice_controls.js +351 -0
  183. package/src/ui/components/color_field.js +211 -0
  184. package/src/ui/components/color_list_field.js +210 -0
  185. package/src/ui/components/column_fit.js +0 -0
  186. package/src/ui/components/column_groups.js +314 -0
  187. package/src/ui/components/column_order.js +142 -0
  188. package/src/ui/components/computing_status_window.js +98 -25
  189. package/src/ui/components/confirm_dialog.js +81 -38
  190. package/src/ui/components/context_menu.js +2 -2
  191. package/src/ui/components/conversation_view.js +440 -0
  192. package/src/ui/components/crosstab.js +289 -0
  193. package/src/ui/components/data_table.js +601 -62
  194. package/src/ui/components/detail_header.js +22 -11
  195. package/src/ui/components/document_outline.js +397 -0
  196. package/src/ui/components/drag_reorder.js +0 -0
  197. package/src/ui/components/fact_table.js +0 -0
  198. package/src/ui/components/fill_handle.js +322 -0
  199. package/src/ui/components/gallery_picker.js +19 -8
  200. package/src/ui/components/header_actions.js +55 -0
  201. package/src/ui/components/inline_renamer.js +0 -0
  202. package/src/ui/components/job_progress.js +51 -0
  203. package/src/ui/components/managed_window.js +27 -4
  204. package/src/ui/components/menu_bar.js +745 -0
  205. package/src/ui/components/modal.js +449 -75
  206. package/src/ui/components/notification_history.js +3 -3
  207. package/src/ui/components/progress.js +483 -0
  208. package/src/ui/components/record_list.js +0 -0
  209. package/src/ui/components/row_activation.js +0 -0
  210. package/src/ui/components/section_stack.js +298 -0
  211. package/src/ui/components/series.js +173 -0
  212. package/src/ui/components/settings_view.js +634 -0
  213. package/src/ui/components/slide_out_panel.js +43 -6
  214. package/src/ui/components/slider_field.js +275 -18
  215. package/src/ui/components/sortable_list.js +26 -4
  216. package/src/ui/components/staged_edits.js +343 -0
  217. package/src/ui/components/staged_history.js +172 -0
  218. package/src/ui/components/table_filter_chips.js +0 -0
  219. package/src/ui/components/table_state_store.js +0 -0
  220. package/src/ui/components/table_structure.js +182 -0
  221. package/src/ui/components/table_values.js +0 -0
  222. package/src/ui/components/tabs.js +497 -0
  223. package/src/ui/components/tag_input.js +215 -0
  224. package/src/ui/components/toast.js +25 -8
  225. package/src/ui/components/toggle_field.js +175 -0
  226. package/src/ui/components/tree_view.js +55 -34
  227. package/src/ui/components/window_taskbar.js +0 -0
  228. package/src/ui/components/wizard.js +563 -0
  229. package/src/ui/controllers/window_chrome_controller.js +2 -2
  230. package/src/ui/notification_center.js +375 -119
  231. package/src/ui/utils/auto_scrollbars.js +0 -0
  232. package/src/ui/utils/markdown.js +709 -0
  233. package/src/ui/utils/native_context_menu.js +0 -0
  234. package/src/ui/utils/overlay_scrollbar.js +16 -12
  235. package/src/ui/utils/raf_resize_observer.js +0 -0
  236. package/src/ui/utils/scroll_lock.js +0 -0
  237. package/src/ui/utils/strip_scroll.js +55 -0
  238. package/src/ui/utils/tooltip_service.js +0 -8
  239. package/dist/chunk-6JSOVNID.js.map +0 -7
  240. package/dist/chunk-CWPMLX67.js +0 -475
  241. package/dist/chunk-CWPMLX67.js.map +0 -7
  242. package/dist/chunk-FBGP5KN5.js.map +0 -7
  243. package/dist/chunk-FL5KFNQH.js.map +0 -7
  244. package/dist/chunk-NLTOY57M.js.map +0 -7
  245. package/dist/chunk-NNFTETK3.js +0 -633
  246. package/dist/chunk-NNFTETK3.js.map +0 -7
  247. package/dist/chunk-O5OHMWBB.js +0 -284
  248. package/dist/chunk-O5OHMWBB.js.map +0 -7
  249. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  250. package/dist/chunk-SN4LSOAR.js.map +0 -7
  251. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,289 @@
1
+ /**
2
+ * crosstab.js — STORY-0091. DataTable's CROSSTAB LAYOUT (`crosstab`).
3
+ * INTERNAL to the table: imported by `data_table.js` only, and not part of the
4
+ * widgets barrel — it reads the table's private fields.
5
+ *
6
+ * `crosstab: {labelColumns, headerRows, emptyLabel}` draws the table as a
7
+ * matrix: k rows of spanned headers over the body columns, the leading
8
+ * `labelColumns` drawn data columns as row labels, and the whole of it in ONE
9
+ * `<table>` in ONE scroller. The consumer hands over every value and every
10
+ * label — rows and columns that add others up, their words and their classes
11
+ * (`rowClass`, `cellClass`, a header cell's `className`) are the consumer's;
12
+ * this module only lays the cells out and pins them.
13
+ *
14
+ * ── ONE TABLE, ONE SCROLLER ───────────────────────────────────────────────
15
+ *
16
+ * The ordinary DataTable lifts its `<thead>` into a header table of its own
17
+ * above the body's scroll box and copies the body's widths onto it. A header
18
+ * of spans breaks that copy (every width pass reads the header CELL BY CELL at
19
+ * the column's index), and two scrollers cannot pin a label column in both the
20
+ * header and the body. So here the `<thead>` stays in the body table and
21
+ * pinning is `position: sticky` against the body's scroll box (`_tableWrapEl`),
22
+ * the only box that scrolls: the header rows to the top, the label columns
23
+ * (with the row number and select columns in front of them) to the left, and
24
+ * the corner — the header cells over the label columns — to both. No header
25
+ * table is built, so no width pass, resize grip, header menu or group row of
26
+ * the split layout runs.
27
+ *
28
+ * ── OFFSETS ARE MEASURED, NOT ASSUMED ─────────────────────────────────────
29
+ *
30
+ * A sticky header row needs its own `top`: the height of every header row
31
+ * above it. A sticky label column needs its own `left`: the width of every
32
+ * pinned column before it. Both are read after layout (`pin()`, on the frame
33
+ * after a render) and again whenever a header row or a pinned header cell
34
+ * changes size (a label that wraps, a font, a zoom) through a ResizeObserver.
35
+ * They are read with `getBoundingClientRect`, which keeps the fraction
36
+ * `offsetHeight` rounds away, and scaled back out of any CSS zoom, because
37
+ * `style.top`/`style.left` are written in the unzoomed space inside it.
38
+ *
39
+ * ── AN EMPTY AXIS ─────────────────────────────────────────────────────────
40
+ *
41
+ * With no body column at all the table still draws: one column headed
42
+ * `emptyLabel` ('(none)'), an empty cell under it in every row, and no spanned
43
+ * header rows. It is no data column (`_dataIdxOfDom` answers null for it).
44
+ *
45
+ * A table without `crosstab` never builds this.
46
+ */
47
+
48
+ import { createRafResizeObserver } from '../utils/raf_resize_observer.js';
49
+ import { normalizeSpans, blankSpanCell, labelSpanCell } from './column_groups.js';
50
+
51
+ const DEFAULT_EMPTY_LABEL = '(none)';
52
+
53
+ /** The CSS zoom an element is drawn in, as the factor that takes a measured
54
+ * (zoomed) length back to the space `style` lengths are written in. */
55
+ function unzoom(el) {
56
+ return 1 / (el?.currentCSSZoom || 1);
57
+ }
58
+
59
+ export class Crosstab {
60
+ /** @param {import('./data_table.js').DataTable} table */
61
+ constructor(table) {
62
+ this.table = table;
63
+ /** The pinned columns' `left`, by drawn index, as last measured. */
64
+ this._lefts = [];
65
+ this._leafRow = null;
66
+ }
67
+
68
+ _spec() {
69
+ const s = this.table.config.crosstab;
70
+ return s && typeof s === 'object' ? s : {};
71
+ }
72
+
73
+ /** How many of the drawn data columns are row labels: the leading ones. */
74
+ labelCount() {
75
+ const k = Math.floor(Number(this._spec().labelColumns) || 0);
76
+ return Math.max(0, Math.min(k, this.table._shownColumns().length));
77
+ }
78
+
79
+ /** The drawn columns that pin to the left: the select and row-number
80
+ * columns, and the label columns. */
81
+ pinnedCount() {
82
+ return this.table._lead().all + this.labelCount();
83
+ }
84
+
85
+ /** The drawn data columns after the label columns, in order. */
86
+ bodyColumns() {
87
+ return this.table._shownColumns().slice(this.labelCount());
88
+ }
89
+
90
+ /** Columns drawn that are no data column: the empty axis's one. */
91
+ extraColumns() {
92
+ return this.bodyColumns().length ? 0 : 1;
93
+ }
94
+
95
+ /** The header rows as drawn: per row, `{cell, span}` runs over the body
96
+ * columns (`cell` null for a run in no cell of that row). None for an
97
+ * empty axis. */
98
+ headerRuns() {
99
+ const body = this.bodyColumns();
100
+ if (!body.length) return [];
101
+ const spec = this._spec().headerRows;
102
+ const n = this.table.config.headers?.length || 0;
103
+ return (Array.isArray(spec) ? spec : []).map((rowSpec) => {
104
+ const cells = normalizeSpans(rowSpec, n);
105
+ const cellOf = new Map();
106
+ cells.forEach((c) => { for (const col of c.columns) cellOf.set(col, c); });
107
+ const runs = [];
108
+ for (const col of body) {
109
+ const cell = cellOf.get(col) || null;
110
+ const last = runs[runs.length - 1];
111
+ if (last && last.cell === cell) last.span++;
112
+ else runs.push({ cell, span: 1, rowSpec });
113
+ }
114
+ return runs;
115
+ });
116
+ }
117
+
118
+ /** STORY-0091. A body row: its pinned cells marked and put at the measured
119
+ * offsets (a row the virtual window draws later lines up at once), and
120
+ * the empty axis's cell. */
121
+ fillRow(tr) {
122
+ if (this.extraColumns()) {
123
+ const td = document.createElement('td');
124
+ td.className = 'twm-dt-crosstab__none';
125
+ tr.appendChild(td);
126
+ }
127
+ this._pinRowCells(tr);
128
+ }
129
+
130
+ _pinRowCells(tr) {
131
+ const p = this.pinnedCount();
132
+ for (let j = 0; j < p && j < tr.children.length; j++) {
133
+ const cell = tr.children[j];
134
+ cell.classList.add('twm-dt-pin');
135
+ if (j === p - 1) cell.classList.add('twm-dt-pin--last');
136
+ cell.__pinCol = j;
137
+ if (this._lefts[j] != null) cell.style.left = `${this._lefts[j]}px`;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * After `_createTable`: the header rows above the column headers, the
143
+ * corner, the pinned header cells and the empty axis's header — all in the
144
+ * body table's own `<thead>`.
145
+ */
146
+ decorate(tableEl) {
147
+ this._leafRow = null;
148
+ const thead = tableEl?.querySelector('thead');
149
+ const leaf = thead?.querySelector('tr');
150
+ if (!leaf) return;
151
+ tableEl.classList.add('twm-dt-crosstab');
152
+ this._leafRow = leaf;
153
+ leaf.classList.add('twm-dt-crosstab__leaf');
154
+ const filters = [...thead.querySelectorAll('tr.twm-data-table__filter-row')];
155
+ if (this.extraColumns()) {
156
+ const th = document.createElement('th');
157
+ th.className = 'text twm-dt-crosstab__none';
158
+ const label = this._spec().emptyLabel;
159
+ th.textContent = label == null ? DEFAULT_EMPTY_LABEL : String(label);
160
+ leaf.appendChild(th);
161
+ for (const tr of filters) {
162
+ const cell = document.createElement('th');
163
+ cell.className = 'data-table__filter-cell data-table__filter-cell--empty twm-dt-crosstab__none';
164
+ tr.appendChild(cell);
165
+ }
166
+ }
167
+ for (const tr of [leaf, ...filters]) this._pinRowCells(tr);
168
+
169
+ const levels = this.headerRuns();
170
+ const p = this.pinnedCount();
171
+ levels.forEach((runs, r) => {
172
+ const tr = document.createElement('tr');
173
+ tr.className = 'twm-dt-crosstab__band';
174
+ // THE CORNER: one cell per pinned column, spanning every header
175
+ // row down to the column headers, so each pins at its own `left`.
176
+ if (r === 0) {
177
+ for (let j = 0; j < p; j++) {
178
+ const th = document.createElement('th');
179
+ th.className = 'twm-dt-crosstab__corner twm-dt-pin';
180
+ if (j === p - 1) th.classList.add('twm-dt-pin--last');
181
+ th.rowSpan = levels.length;
182
+ th.__pinCol = j;
183
+ tr.appendChild(th);
184
+ }
185
+ }
186
+ for (const run of runs) tr.appendChild(this._bandCell(run));
187
+ thead.insertBefore(tr, leaf);
188
+ });
189
+ }
190
+
191
+ /** A header row's cell: STORY-0066's spanning cells, the consumer's
192
+ * `className` and `title` on a labelled one. */
193
+ _bandCell({ cell, span, rowSpec }) {
194
+ if (!cell) return blankSpanCell(span);
195
+ const own = rowSpec[cell.index];
196
+ const th = labelSpanCell(span, cell.label, own?.title == null ? cell.label : String(own.title));
197
+ if (own?.className) this.table._addClasses(th, own.className);
198
+ return th;
199
+ }
200
+
201
+ /**
202
+ * Once the table is in the document: pin now (a layout already there is
203
+ * used at once), on the next frame (when it has settled), and whenever a
204
+ * header row, a pinned header cell or the table's box changes size. The
205
+ * observer goes with the render (`_disposers`).
206
+ *
207
+ * The split layout's own resize observer and settle frame hang off its
208
+ * header table, which a crosstab has none of; so the virtual window's
209
+ * re-measure and resize (`_virtRemeasure`, `_virtUpdate`) run from here.
210
+ */
211
+ install() {
212
+ const table = this.table;
213
+ const tableEl = table._tableEl;
214
+ if (!tableEl || !this._leafRow) return;
215
+ this.pin();
216
+ requestAnimationFrame(() => {
217
+ if (table._tableEl !== tableEl) return;
218
+ this.pin();
219
+ table._virtRemeasure();
220
+ });
221
+ if (typeof ResizeObserver === 'undefined') return;
222
+ const observer = createRafResizeObserver(() => {
223
+ if (table._tableEl !== tableEl) return;
224
+ this.pin();
225
+ table._virtUpdate();
226
+ });
227
+ if (table._wrapperEl) observer.observe(table._wrapperEl);
228
+ const thead = tableEl.querySelector('thead');
229
+ for (const tr of thead.querySelectorAll('tr')) observer.observe(tr);
230
+ for (let j = 0; j < this.pinnedCount(); j++) {
231
+ const cell = this._leafRow.children[j];
232
+ if (cell) observer.observe(cell);
233
+ }
234
+ table._disposers.push(() => {
235
+ try { observer.disconnect(); } catch (_) { /* gone */ }
236
+ });
237
+ }
238
+
239
+ /** Measure and write every offset: each header row's `top`, each pinned
240
+ * column's `left`. */
241
+ pin() {
242
+ const tableEl = this.table._tableEl;
243
+ const thead = tableEl?.querySelector('thead');
244
+ if (!thead || !this._leafRow) return;
245
+ const rows = [...thead.rows];
246
+ const scale = unzoom(thead);
247
+ const origin = rows[0].getBoundingClientRect().top;
248
+ for (const tr of rows) {
249
+ const top = Math.max(0, (tr.getBoundingClientRect().top - origin) * scale);
250
+ const css = `${+top.toFixed(2)}px`;
251
+ for (const th of tr.cells) th.style.top = css;
252
+ }
253
+
254
+ const p = this.pinnedCount();
255
+ const lefts = [0];
256
+ for (let j = 1; j < p; j++) {
257
+ const cell = this._leafRow.children[j - 1];
258
+ const w = cell ? cell.getBoundingClientRect().width * scale : 0;
259
+ lefts[j] = +(lefts[j - 1] + w).toFixed(2);
260
+ }
261
+ this._lefts = p ? lefts : [];
262
+ for (const cell of tableEl.querySelectorAll('.twm-dt-pin')) {
263
+ const left = this._lefts[cell.__pinCol];
264
+ if (left != null) cell.style.left = `${left}px`;
265
+ }
266
+ this._lockColumns(scale);
267
+ }
268
+
269
+ /**
270
+ * Under `virtualize` the rows drawn change with every scroll, and with
271
+ * them the widths an automatic layout gives the columns. The first pass
272
+ * that sees a layout writes the widths it measured on the column headers
273
+ * into the `<colgroup>` and fixes the layout, so the columns hold still.
274
+ * No layout (every width 0) is no measurement, and nothing is written.
275
+ */
276
+ _lockColumns(scale) {
277
+ const cols = this.table._cols();
278
+ const tableEl = this.table._tableEl;
279
+ if (!cols || !tableEl || tableEl.__crosstabLocked) return;
280
+ const cells = this._leafRow.children;
281
+ if (cells.length !== cols.length) return;
282
+ const widths = [...cells].map((c) => c.getBoundingClientRect().width * scale);
283
+ if (!widths.every((w) => w > 0)) return;
284
+ widths.forEach((w, i) => { cols[i].style.width = `${+w.toFixed(2)}px`; });
285
+ tableEl.style.tableLayout = 'fixed';
286
+ tableEl.style.width = `${+widths.reduce((a, b) => a + b, 0).toFixed(2)}px`;
287
+ tableEl.__crosstabLocked = true;
288
+ }
289
+ }