flexdesk 0.16.1 → 0.17.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.
Files changed (262) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2435 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/canvas.js +18 -0
  6. package/charts.js +21 -0
  7. package/core.js +14 -0
  8. package/css/base.css +1834 -75
  9. package/css/flexdesk.css +3165 -159
  10. package/css/overrides.css +16 -0
  11. package/css/reset.css +0 -0
  12. package/css/tokens.css +0 -0
  13. package/dist/canvas.js +6 -239
  14. package/dist/canvas.js.map +3 -3
  15. package/dist/charts.js +707 -18
  16. package/dist/charts.js.map +4 -4
  17. package/dist/chunk-3PHCPZHT.js +0 -0
  18. package/dist/chunk-3PHCPZHT.js.map +0 -0
  19. package/dist/chunk-4A7SJC3T.js +503 -0
  20. package/dist/chunk-4A7SJC3T.js.map +7 -0
  21. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  22. package/dist/chunk-CVMCZZX2.js.map +7 -0
  23. package/dist/chunk-D3COLZZ2.js +1311 -0
  24. package/dist/chunk-D3COLZZ2.js.map +7 -0
  25. package/dist/chunk-FOOS3T5L.js +0 -0
  26. package/dist/chunk-FOOS3T5L.js.map +0 -0
  27. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  28. package/dist/chunk-GOQLR5WR.js.map +7 -0
  29. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  30. package/dist/chunk-HNU6MM2B.js.map +7 -0
  31. package/dist/chunk-HYJYYEPK.js +375 -0
  32. package/dist/chunk-HYJYYEPK.js.map +7 -0
  33. package/dist/chunk-JKZLNKG6.js +2041 -0
  34. package/dist/chunk-JKZLNKG6.js.map +7 -0
  35. package/dist/chunk-JYWURG5T.js +0 -0
  36. package/dist/chunk-JYWURG5T.js.map +0 -0
  37. package/dist/{chunk-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
  38. package/dist/chunk-LWV23YVE.js.map +7 -0
  39. package/dist/chunk-N7HC3ADV.js +30 -0
  40. package/dist/chunk-N7HC3ADV.js.map +7 -0
  41. package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
  42. package/dist/chunk-RMTRWWEO.js.map +7 -0
  43. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  44. package/dist/chunk-UURZ37SD.js.map +7 -0
  45. package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  46. package/dist/chunk-UWO43BZU.js.map +7 -0
  47. package/dist/chunk-WVFGV5FT.js +0 -0
  48. package/dist/chunk-WVFGV5FT.js.map +0 -0
  49. package/dist/core.js +170 -118
  50. package/dist/core.js.map +2 -2
  51. package/dist/editor.js +156 -70
  52. package/dist/editor.js.map +2 -2
  53. package/dist/flexdesk.css +3165 -159
  54. package/dist/flow.js +2789 -306
  55. package/dist/flow.js.map +4 -4
  56. package/dist/host.js +109 -3
  57. package/dist/host.js.map +3 -3
  58. package/dist/reset.css +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js +0 -0
  60. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  61. package/dist/tiles.js +657 -234
  62. package/dist/tiles.js.map +4 -4
  63. package/dist/tokens.css +0 -0
  64. package/dist/widgets.js +3290 -2124
  65. package/dist/widgets.js.map +4 -4
  66. package/dist/wm.js +1227 -347
  67. package/dist/wm.js.map +4 -4
  68. package/editor.js +20 -0
  69. package/flow.js +21 -0
  70. package/host.js +14 -0
  71. package/package.json +12 -1
  72. package/src/canvas/canvas_adapter.js +5 -0
  73. package/src/canvas/edge_router.js +0 -0
  74. package/src/canvas/graph/connector_router.js +0 -0
  75. package/src/canvas/graph/history_service.js +0 -0
  76. package/src/canvas/graph/node_platform.js +0 -0
  77. package/src/canvas/index.js +1 -0
  78. package/src/canvas/layered_layout.js +244 -0
  79. package/src/charts/chart_types.js +0 -0
  80. package/src/charts/data_series_plot_window.js +0 -0
  81. package/src/charts/downsample.js +2 -2
  82. package/src/charts/plot_config.js +53 -29
  83. package/src/charts/plot_config_editor.js +627 -0
  84. package/src/charts/plot_grid.js +449 -0
  85. package/src/charts/plot_popout_window.js +103 -317
  86. package/src/charts/plotly_wrapper.js +22 -2
  87. package/src/charts/series_transforms.js +206 -0
  88. package/src/core/event_bus.js +0 -0
  89. package/src/core/logging.js +1 -1
  90. package/src/core/settings.js +105 -19
  91. package/src/core/state_guard.js +93 -51
  92. package/src/core/state_guard_config.js +85 -74
  93. package/src/core/state_machine.js +0 -0
  94. package/src/editor/editor_pane.js +3 -3
  95. package/src/editor/monaco_editor_factory.js +124 -60
  96. package/src/editor/monaco_loader.js +103 -28
  97. package/src/editor/notebook_search.js +0 -0
  98. package/src/editor/notebook_tab_bar.js +38 -52
  99. package/src/editor/notebook_undo_manager.js +0 -0
  100. package/src/editor/split_pane_container.js +6 -3
  101. package/src/flow/canvas_editor/connections.js +90 -0
  102. package/src/flow/canvas_editor/editor.js +2351 -153
  103. package/src/flow/canvas_editor/index.js +2 -1
  104. package/src/flow/canvas_editor/layout.js +119 -12
  105. package/src/flow/canvas_editor/strings.js +80 -0
  106. package/src/flow/kit/always_before.js +0 -0
  107. package/src/flow/kit/catalogue.js +97 -3
  108. package/src/flow/kit/chip_input.js +0 -0
  109. package/src/flow/kit/dom.js +0 -0
  110. package/src/flow/kit/findings.js +0 -0
  111. package/src/flow/kit/history.js +0 -0
  112. package/src/flow/kit/index.js +3 -3
  113. package/src/flow/kit/keys.js +7 -1
  114. package/src/flow/kit/logic_graph.js +97 -15
  115. package/src/flow/kit/references.js +0 -0
  116. package/src/flow/kit/settings_panel.js +0 -0
  117. package/src/flow/kit/settings_schema.js +2 -1
  118. package/src/flow/kit/step_picker.js +0 -0
  119. package/src/flow/kit/strings.js +3 -1
  120. package/src/flow/kit/value_picker.js +0 -0
  121. package/src/flow/kit/widgets.js +86 -109
  122. package/src/flow/lanes/editor.js +136 -14
  123. package/src/flow/lanes/index.js +3 -1
  124. package/src/flow/lanes/layout.js +0 -0
  125. package/src/flow/lanes/pipeline.js +44 -2
  126. package/src/flow/lanes/preview.js +0 -0
  127. package/src/flow/lanes/strings.js +1 -0
  128. package/src/flow/outline/editor.js +0 -0
  129. package/src/flow/outline/index.js +0 -0
  130. package/src/flow/outline/mapping.js +0 -0
  131. package/src/flow/outline/menu.js +0 -0
  132. package/src/flow/outline/operations.js +0 -0
  133. package/src/flow/outline/recognise.js +0 -0
  134. package/src/flow/outline/strings.js +0 -0
  135. package/src/help/help_modal.js +168 -42
  136. package/src/help/help_registry.js +12 -1
  137. package/src/host/external_links.js +0 -0
  138. package/src/host/file_picker.js +117 -0
  139. package/src/host/host.js +10 -2
  140. package/src/host/photino_host.js +9 -0
  141. package/src/host/photino_host.md +21 -3
  142. package/src/host/pywebview_host.js +10 -0
  143. package/src/tiles/chart_tiles.js +53 -245
  144. package/src/tiles/config_schema.js +8 -0
  145. package/src/tiles/figure_tile.js +322 -0
  146. package/src/tiles/layout_persistence.js +3 -3
  147. package/src/tiles/list_tiles.js +7 -7
  148. package/src/tiles/scope_picker.js +0 -0
  149. package/src/tiles/tile_base.js +1 -1
  150. package/src/tiles/tile_config_form.js +442 -0
  151. package/src/tiles/tile_grid.js +2 -0
  152. package/src/tiles/tile_registry.js +0 -0
  153. package/src/tiling/bottom_panel_tabs.js +67 -99
  154. package/src/tiling/command_palette.js +64 -19
  155. package/src/tiling/content_registry.js +4 -1
  156. package/src/tiling/desktops.js +11 -1
  157. package/src/tiling/entity_sources.js +0 -0
  158. package/src/tiling/keymap.js +403 -128
  159. package/src/tiling/kind_taxonomy.js +78 -0
  160. package/src/tiling/landing_table.js +0 -0
  161. package/src/tiling/loading_overlay.js +0 -0
  162. package/src/tiling/nav_panel.js +59 -10
  163. package/src/tiling/page_factory.js +5 -5
  164. package/src/tiling/panel_availability.js +49 -0
  165. package/src/tiling/panel_keys.js +0 -0
  166. package/src/tiling/shell.js +114 -12
  167. package/src/tiling/tab_strip.js +0 -0
  168. package/src/tiling/tile_breadcrumb.js +0 -0
  169. package/src/tiling/tile_renderer.js +105 -9
  170. package/src/tiling/tile_tab_menu.js +0 -0
  171. package/src/tiling/tile_tree.js +0 -0
  172. package/src/tiling/wm.js +562 -30
  173. package/src/tiling/zoom.js +0 -0
  174. package/src/ui/base/component_base.js +0 -0
  175. package/src/ui/base/controller_base.js +0 -0
  176. package/src/ui/base/page_base.js +0 -0
  177. package/src/ui/components/about_dialog.js +0 -0
  178. package/src/ui/components/action_dropdown.js +30 -2
  179. package/src/ui/components/alert_log.js +0 -0
  180. package/src/ui/components/alerts_pane.js +0 -0
  181. package/src/ui/components/attribute_list_editor.js +0 -0
  182. package/src/ui/components/autocomplete_field.js +0 -0
  183. package/src/ui/components/cell_editor_host.js +293 -0
  184. package/src/ui/components/cell_editors.js +480 -0
  185. package/src/ui/components/cell_grid.js +761 -0
  186. package/src/ui/components/cell_paste.js +243 -0
  187. package/src/ui/components/cell_selection.js +220 -0
  188. package/src/ui/components/choice_controls.js +351 -0
  189. package/src/ui/components/color_field.js +211 -0
  190. package/src/ui/components/color_list_field.js +210 -0
  191. package/src/ui/components/column_fit.js +0 -0
  192. package/src/ui/components/column_groups.js +314 -0
  193. package/src/ui/components/column_order.js +142 -0
  194. package/src/ui/components/computing_status_window.js +98 -25
  195. package/src/ui/components/confirm_dialog.js +81 -38
  196. package/src/ui/components/context_menu.js +2 -2
  197. package/src/ui/components/conversation_view.js +440 -0
  198. package/src/ui/components/crosstab.js +289 -0
  199. package/src/ui/components/data_table.js +696 -71
  200. package/src/ui/components/detail_header.js +22 -11
  201. package/src/ui/components/document_outline.js +397 -0
  202. package/src/ui/components/drag_reorder.js +0 -0
  203. package/src/ui/components/fact_table.js +0 -0
  204. package/src/ui/components/fill_handle.js +322 -0
  205. package/src/ui/components/gallery_picker.js +19 -8
  206. package/src/ui/components/header_actions.js +55 -0
  207. package/src/ui/components/inline_renamer.js +0 -0
  208. package/src/ui/components/job_progress.js +51 -0
  209. package/src/ui/components/managed_window.js +27 -4
  210. package/src/ui/components/menu_bar.js +745 -0
  211. package/src/ui/components/modal.js +449 -75
  212. package/src/ui/components/notification_history.js +3 -3
  213. package/src/ui/components/progress.js +483 -0
  214. package/src/ui/components/record_list.js +0 -0
  215. package/src/ui/components/row_activation.js +0 -0
  216. package/src/ui/components/section_stack.js +298 -0
  217. package/src/ui/components/series.js +173 -0
  218. package/src/ui/components/settings_view.js +634 -0
  219. package/src/ui/components/slide_out_panel.js +43 -6
  220. package/src/ui/components/slider_field.js +275 -18
  221. package/src/ui/components/sortable_list.js +0 -0
  222. package/src/ui/components/staged_edits.js +361 -0
  223. package/src/ui/components/staged_history.js +172 -0
  224. package/src/ui/components/table_filter_chips.js +0 -0
  225. package/src/ui/components/table_state_store.js +0 -0
  226. package/src/ui/components/table_structure.js +182 -0
  227. package/src/ui/components/table_values.js +0 -0
  228. package/src/ui/components/tabs.js +497 -0
  229. package/src/ui/components/tag_input.js +215 -0
  230. package/src/ui/components/toast.js +25 -8
  231. package/src/ui/components/toggle_field.js +175 -0
  232. package/src/ui/components/tree_view.js +55 -34
  233. package/src/ui/components/tsv.js +74 -0
  234. package/src/ui/components/window_taskbar.js +0 -0
  235. package/src/ui/components/wizard.js +563 -0
  236. package/src/ui/controllers/window_chrome_controller.js +2 -2
  237. package/src/ui/notification_center.js +375 -119
  238. package/src/ui/utils/auto_scrollbars.js +0 -0
  239. package/src/ui/utils/markdown.js +709 -0
  240. package/src/ui/utils/native_context_menu.js +0 -0
  241. package/src/ui/utils/overlay_scrollbar.js +16 -12
  242. package/src/ui/utils/raf_resize_observer.js +0 -0
  243. package/src/ui/utils/scroll_lock.js +0 -0
  244. package/src/ui/utils/strip_scroll.js +55 -0
  245. package/src/ui/utils/tooltip_service.js +0 -8
  246. package/tiles.js +27 -0
  247. package/widgets.js +69 -0
  248. package/wm.js +28 -0
  249. package/dist/chunk-6JSOVNID.js.map +0 -7
  250. package/dist/chunk-B5Q53LAJ.js.map +0 -7
  251. package/dist/chunk-FBGP5KN5.js.map +0 -7
  252. package/dist/chunk-FL5KFNQH.js.map +0 -7
  253. package/dist/chunk-NLTOY57M.js.map +0 -7
  254. package/dist/chunk-NNFTETK3.js +0 -633
  255. package/dist/chunk-NNFTETK3.js.map +0 -7
  256. package/dist/chunk-O5OHMWBB.js +0 -284
  257. package/dist/chunk-O5OHMWBB.js.map +0 -7
  258. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  259. package/dist/chunk-SN4LSOAR.js.map +0 -7
  260. package/dist/chunk-W6FBJLPW.js +0 -367
  261. package/dist/chunk-W6FBJLPW.js.map +0 -7
  262. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,210 @@
1
+ /**
2
+ * ColorListField — an ordered list of colours: add, remove, and reorder by
3
+ * the grip (drag) or the up / down buttons, each row a `ColorField`.
4
+ *
5
+ * const list = new ColorListField({
6
+ * value: ['#1f77b4', 'rgb(255, 127, 14)'],
7
+ * onChange: (colors, { commit }) => { ... },
8
+ * });
9
+ * host.appendChild(list.render());
10
+ * list.getValue(); // ['#1f77b4', '#ff7f0e'] — or null, see below
11
+ *
12
+ * The list is a `SortableList` (the same add / remove / drag-reorder every
13
+ * list in the library uses) whose rows render a `ColorField` and a pair of
14
+ * move buttons, so the order can be changed without a pointer.
15
+ *
16
+ * `getValue()` is the colours in their current order as lowercase `#rrggbb`,
17
+ * or `null` while any row holds something that is not a hex colour —
18
+ * `invalidIndexes()` says which. Empty rows count as invalid: a list entry
19
+ * with no colour is not a colour.
20
+ *
21
+ * Given a `name`, the list also carries a hidden `<input name>` whose value is
22
+ * the rows as JSON, and fires `input` on it for every edit and `change` for
23
+ * every committed one (a row's `change`, add, remove, move). That is the
24
+ * surface a host form listens on; `openForm` reads the list through
25
+ * `getValue()`, not through that input.
26
+ */
27
+
28
+ import { SortableList } from './sortable_list.js';
29
+ import { ColorField, isHexColor, normalizeHex } from './color_field.js';
30
+
31
+ let _seq = 0;
32
+
33
+ /** A row's text as `#rrggbb`, or null when it is not a hex colour. */
34
+ const hexOf = (text) => {
35
+ const t = String(text ?? '').trim();
36
+ return isHexColor(t) ? normalizeHex(t) : null;
37
+ };
38
+
39
+ export class ColorListField {
40
+ /**
41
+ * @param {Object} [o]
42
+ * @param {string[]} [o.value] initial colours (hex or rgb())
43
+ * @param {string[]} [o.presets] preset colours offered on every row
44
+ * @param {string} [o.newColor] colour a newly added row starts on
45
+ * @param {string} [o.addLabel] text of the add button
46
+ * @param {string} [o.name] `name` of the hidden input, for a host form
47
+ * @param {Function} [o.onChange] `(colors, { commit })`, colors as `getValue()`
48
+ */
49
+ constructor({
50
+ value = [], presets = [], newColor = '#000000', addLabel = 'Add colour',
51
+ name = null, onChange = null,
52
+ } = {}) {
53
+ this.presets = Array.isArray(presets) ? presets : [];
54
+ // Converted once, as #toItems does, so an added row is what it shows.
55
+ this.newColor = normalizeHex(String(newColor ?? '')) ?? String(newColor ?? '');
56
+ this.addLabel = addLabel;
57
+ this.name = name;
58
+ this.onChange = typeof onChange === 'function' ? onChange : null;
59
+
60
+ /** Row index -> its ColorField; always exactly the rendered rows. */
61
+ this._fields = [];
62
+ /** Whether the SortableList change being reported is a committed one. */
63
+ this._commit = true;
64
+ /** True while a row's own edit is written into the list's items. */
65
+ this._rowEdit = false;
66
+ this.list = new SortableList({
67
+ items: this.#toItems(value),
68
+ addButtonText: addLabel,
69
+ emptyMessage: 'No colours',
70
+ createItem: () => ({ id: ++_seq, text: this.newColor }),
71
+ renderItem: (container, item, index, callbacks) =>
72
+ this.#renderRow(container, item, index, callbacks),
73
+ // Every SortableList change but a row's own edit (add, remove,
74
+ // move, drag) is followed by a refresh that rebuilds all rows —
75
+ // down to none — so the old fields go first.
76
+ onChange: () => {
77
+ if (!this._rowEdit) this.#dropFields();
78
+ this.#emit(this._commit);
79
+ },
80
+ });
81
+ this.root = null;
82
+ this.input = null;
83
+ }
84
+
85
+ render() {
86
+ if (this.root) return this.root;
87
+ const root = document.createElement('div');
88
+ root.className = 'twm-color-list';
89
+ if (this.name) {
90
+ const input = document.createElement('input');
91
+ input.type = 'hidden';
92
+ input.name = this.name;
93
+ root.appendChild(input);
94
+ this.input = input;
95
+ }
96
+ root.appendChild(this.list.render());
97
+ this.root = root;
98
+ this.#writeInput();
99
+ return root;
100
+ }
101
+
102
+ /** The colours in order as `#rrggbb`, or null while any row is not one. */
103
+ getValue() {
104
+ const out = this.getTexts().map(hexOf);
105
+ return out.includes(null) ? null : out;
106
+ }
107
+
108
+ /** The rows' text as it stands, valid or not. */
109
+ getTexts() {
110
+ return this.list.getItems().map((item) => String(item.text ?? ''));
111
+ }
112
+
113
+ /** Indexes of the rows that do not hold a hex colour. */
114
+ invalidIndexes() {
115
+ const bad = [];
116
+ this.getTexts().forEach((text, i) => { if (!hexOf(text)) bad.push(i); });
117
+ return bad;
118
+ }
119
+
120
+ /** Replace the colours without firing `onChange`. */
121
+ setValue(colors) {
122
+ this.#dropFields();
123
+ this.list.setItems(this.#toItems(colors));
124
+ this.#writeInput();
125
+ }
126
+
127
+ /** Focus the first invalid row, else the first row, else the add button. */
128
+ focus() {
129
+ const bad = this.invalidIndexes()[0];
130
+ const field = this._fields[bad ?? 0];
131
+ if (field) { field.focus(); return; }
132
+ this.root?.querySelector('.twm-sortable-list__add')?.focus();
133
+ }
134
+
135
+ dispose() {
136
+ this.#dropFields();
137
+ this.list.dispose();
138
+ this.root = null;
139
+ this.input = null;
140
+ }
141
+
142
+ #toItems(colors) {
143
+ // A colour that converts is kept as its hex; anything else is kept as
144
+ // typed, so it shows — and is reported — rather than vanishing.
145
+ return (Array.isArray(colors) ? colors : []).map((c) => ({
146
+ id: ++_seq,
147
+ text: normalizeHex(String(c ?? '')) ?? String(c ?? ''),
148
+ }));
149
+ }
150
+
151
+ /** Dispose every row's field: the rows are about to be rebuilt. */
152
+ #dropFields() {
153
+ for (const f of this._fields) f?.dispose();
154
+ this._fields = [];
155
+ }
156
+
157
+ #renderRow(container, item, index, callbacks) {
158
+ const count = this.list.getItems().length;
159
+
160
+ const row = document.createElement('div');
161
+ row.className = 'twm-color-list__row';
162
+ const field = new ColorField({
163
+ value: item.text,
164
+ presets: this.presets,
165
+ label: `Colour ${index + 1}`,
166
+ // Every edit goes into the list's items, so the list never holds
167
+ // a stale row; `_commit` tells its onChange which kind it was.
168
+ onChange: (_v, { commit }) => {
169
+ this._commit = commit;
170
+ this._rowEdit = true;
171
+ try { callbacks.update({ text: field.getText() }); }
172
+ finally { this._commit = true; this._rowEdit = false; }
173
+ },
174
+ });
175
+ this._fields[index] = field;
176
+ row.appendChild(field.render());
177
+
178
+ const move = (icon, label, disabled, fn) => {
179
+ const btn = document.createElement('button');
180
+ btn.type = 'button';
181
+ btn.className = 'twm-color-list__move';
182
+ btn.title = label;
183
+ btn.setAttribute('aria-label', label);
184
+ btn.disabled = disabled;
185
+ const glyph = document.createElement('span');
186
+ glyph.className = 'material-symbols-outlined';
187
+ glyph.setAttribute('aria-hidden', 'true');
188
+ glyph.textContent = icon;
189
+ btn.appendChild(glyph);
190
+ btn.addEventListener('click', fn);
191
+ row.appendChild(btn);
192
+ };
193
+ move('arrow_upward', 'Move up', index === 0, callbacks.moveUp);
194
+ move('arrow_downward', 'Move down', index === count - 1, callbacks.moveDown);
195
+ container.appendChild(row);
196
+ }
197
+
198
+ #writeInput() {
199
+ if (this.input) this.input.value = JSON.stringify(this.getTexts());
200
+ }
201
+
202
+ #emit(commit) {
203
+ this.#writeInput();
204
+ if (this.input) {
205
+ this.input.dispatchEvent(new Event('input', { bubbles: true }));
206
+ if (commit) this.input.dispatchEvent(new Event('change', { bubbles: true }));
207
+ }
208
+ this.onChange?.(this.getValue(), { commit });
209
+ }
210
+ }
File without changes
@@ -0,0 +1,314 @@
1
+ /**
2
+ * column_groups.js — STORY-0066. DataTable's COLUMN GROUPS (`columnGroups`).
3
+ * INTERNAL to the table: imported by `data_table.js` (and its span helpers by
4
+ * `crosstab.js`) only, and not part of the widgets barrel — it reads the
5
+ * table's private fields.
6
+ *
7
+ * A group is `{key, label?, columns: number[]}` (data column indices). The
8
+ * groups draw a second header row above the column headers, one cell a run of
9
+ * shown columns of the same group spanning them, a blank cell over the row
10
+ * number / select column and over any run of columns in no group.
11
+ *
12
+ * ── WHY A TABLE OF ITS OWN ────────────────────────────────────────────────
13
+ *
14
+ * The group row is a separate `<table class="twm-dt-groups">` in the header
15
+ * wrap, above the header table, rather than a first row of the header's
16
+ * `<thead>`. Every width pass of the table (`_measureNaturalWidths`,
17
+ * `_syncHeaderWidths`, the resize drag, `_applyTableWidth`) reads and writes
18
+ * the header rows CELL BY CELL at the column's index, and under
19
+ * `table-layout: fixed` the first row decides the track — a row of spanning
20
+ * cells in either place would misalign every column. So the group table has a
21
+ * `<colgroup>` of its own, one `<col>` a drawn column, and `align()` copies the
22
+ * header's widths, table width, layout and sideways scroll onto it after every
23
+ * pass that changes them (DataTable `_syncHeaderScroll`). The header wrap
24
+ * scrolls both tables together.
25
+ *
26
+ * ── AN EMPTY GROUP KEEPS A PLACEHOLDER COLUMN ─────────────────────────────
27
+ *
28
+ * A group with no columns would have no cell to draw, so it gets one drawn
29
+ * column of its own: a header cell `th.twm-dt-col--placeholder` (holding the
30
+ * group's add control under `onColumnAdd`), an empty filter cell, and an
31
+ * empty `td.twm-dt-cell--placeholder` in every body row. It is NO DATA
32
+ * column: `_dataIdxOfDom` answers null for it, it is not in a copy, the CSV,
33
+ * `ctx.colIdx` or the cell mode. It stands after the shown columns of the
34
+ * groups listed before it.
35
+ *
36
+ * A table without `columnGroups` never builds this.
37
+ */
38
+
39
+ import { callOption, headerActionButton } from './header_actions.js';
40
+
41
+ /**
42
+ * A row of spanning cells, cleaned (shared with STORY-0091's crosstab header
43
+ * rows): the data columns that exist, each column in the first cell that lists
44
+ * it, `key`, a label, and `index`, the cell's place in `spec` (the consumer's
45
+ * object is looked up there when drawing, never kept). Under `requireKey` a
46
+ * cell without a key is dropped; a cell without a label is labelled
47
+ * `defaultLabel(cell)`.
48
+ */
49
+ export function normalizeSpans(spec, n, { requireKey = false, defaultLabel = () => '' } = {}) {
50
+ const taken = new Set();
51
+ const out = [];
52
+ (Array.isArray(spec) ? spec : []).forEach((g, index) => {
53
+ if (!g || (requireKey && g.key == null)) return;
54
+ const columns = [];
55
+ for (const c of Array.isArray(g.columns) ? g.columns : []) {
56
+ const i = Number(c);
57
+ if (!Number.isInteger(i) || i < 0 || i >= n || taken.has(i)) continue;
58
+ taken.add(i);
59
+ columns.push(i);
60
+ }
61
+ out.push({ key: g.key, label: g.label == null ? defaultLabel(g) : String(g.label), columns, index });
62
+ });
63
+ return out;
64
+ }
65
+
66
+ /** The groups the table was given, cleaned: a key each, the key as the label
67
+ * when none is given. */
68
+ function normalize(spec, n) {
69
+ return normalizeSpans(spec, n, { requireKey: true, defaultLabel: (g) => String(g.key) });
70
+ }
71
+
72
+ /** A spanning header cell over a run of columns in no group (or no cell). */
73
+ export function blankSpanCell(span) {
74
+ const th = document.createElement('th');
75
+ th.className = 'twm-dt-group twm-dt-group--none';
76
+ th.colSpan = span;
77
+ return th;
78
+ }
79
+
80
+ /** A spanning header cell holding a label, its tooltip `title`. */
81
+ export function labelSpanCell(span, label, title = label) {
82
+ const th = document.createElement('th');
83
+ th.className = 'twm-dt-group';
84
+ th.colSpan = span;
85
+ th.title = title;
86
+ const el = document.createElement('span');
87
+ el.className = 'twm-dt-group__label';
88
+ el.textContent = label;
89
+ th.appendChild(el);
90
+ return th;
91
+ }
92
+
93
+ export class ColumnGroups {
94
+ /** @param {import('./data_table.js').DataTable} table */
95
+ constructor(table) {
96
+ this.table = table;
97
+ this._memoKey = null;
98
+ this._memo = null;
99
+ this._el = null;
100
+ }
101
+
102
+ _cfg() {
103
+ return this.table.config;
104
+ }
105
+
106
+ /**
107
+ * The drawn columns after the lead, in order: `{col, group}` for a shown
108
+ * data column (`group` the index in `groups`, or null) and
109
+ * `{placeholder: true, group}` for an empty group's column. The data
110
+ * columns come in the order the table draws them (STORY-0086's
111
+ * `columnOrder`, hidden ones left out). Memoised on the groups' content,
112
+ * the column count and the drawn columns.
113
+ */
114
+ layout() {
115
+ const cfg = this._cfg();
116
+ const n = cfg.headers?.length || 0;
117
+ const shown = this.table._shownColumns();
118
+ const spec = Array.isArray(cfg.columnGroups) ? cfg.columnGroups : [];
119
+ const key = `${n}|${shown.join(',')}|${spec.map((g) => (g ? `${g.key}\x01${g.label}\x01${(g.columns || []).join(',')}` : '')).join('\x02')}`;
120
+ if (key === this._memoKey) return this._memo;
121
+
122
+ const groups = normalize(spec, n);
123
+ const groupOf = new Array(n).fill(null);
124
+ groups.forEach((g, gi) => { for (const c of g.columns) groupOf[c] = gi; });
125
+ const empty = groups.map((g) => g.columns.length === 0);
126
+ const placed = new Set();
127
+ const slots = [];
128
+ const placeEmptyBefore = (gi) => {
129
+ for (let e = 0; e < gi; e++) {
130
+ if (empty[e] && !placed.has(e)) { placed.add(e); slots.push({ placeholder: true, group: e }); }
131
+ }
132
+ };
133
+ for (const c of shown) {
134
+ const gi = groupOf[c];
135
+ if (gi !== null) placeEmptyBefore(gi);
136
+ slots.push({ col: c, group: gi });
137
+ }
138
+ placeEmptyBefore(groups.length);
139
+
140
+ const slotOfCol = new Map();
141
+ slots.forEach((s, i) => { if (!s.placeholder) slotOfCol.set(s.col, i); });
142
+ const placeholders = slots.filter((s) => s.placeholder).length;
143
+ this._memoKey = key;
144
+ this._memo = { groups, slots, slotOfCol, placeholders };
145
+ return this._memo;
146
+ }
147
+
148
+ /** How many placeholder columns are drawn. */
149
+ placeholderCount() {
150
+ return this.layout().placeholders;
151
+ }
152
+
153
+ /** A shown data column's place after the lead, or -1 when the layout
154
+ * has no placeholder (the table's own arithmetic then holds). */
155
+ slotOf(colIdx) {
156
+ const l = this.layout();
157
+ if (!l.placeholders) return -1;
158
+ return l.slotOfCol.has(colIdx) ? l.slotOfCol.get(colIdx) : -1;
159
+ }
160
+
161
+ /** What the drawn column `i` after the lead is: the data column, null for
162
+ * a placeholder or past the end, `undefined` when the layout has no
163
+ * placeholder (the table's own arithmetic then holds). */
164
+ slotAt(i) {
165
+ const l = this.layout();
166
+ if (!l.placeholders) return undefined;
167
+ const s = l.slots[i];
168
+ return s && !s.placeholder ? s.col : null;
169
+ }
170
+
171
+ /** Part of the table's column signature: where the placeholders stand. */
172
+ sig() {
173
+ const l = this.layout();
174
+ if (!l.placeholders) return '';
175
+ return `|g${l.slots.map((s, i) => (s.placeholder ? i : '')).filter((x) => x !== '').join(',')}`;
176
+ }
177
+
178
+ /** The drawn positions (after the lead) of the placeholders, ascending,
179
+ * with their groups. */
180
+ _placeholderSlots() {
181
+ const out = [];
182
+ this.layout().slots.forEach((s, i) => { if (s.placeholder) out.push({ at: i, group: s.group }); });
183
+ return out;
184
+ }
185
+
186
+ /** Put the placeholders' cells into a drawn row whose cells are the lead
187
+ * and the shown data columns, in order. */
188
+ _insert(tr, make) {
189
+ const lead = this.table._lead().all;
190
+ for (const { at, group } of this._placeholderSlots()) {
191
+ const ref = tr.children[lead + at] || null;
192
+ tr.insertBefore(make(this.layout().groups[group]), ref);
193
+ }
194
+ }
195
+
196
+ /** STORY-0066. A body row gets an empty cell under each placeholder. */
197
+ fillRow(tr) {
198
+ if (!this.placeholderCount()) return;
199
+ this._insert(tr, () => {
200
+ const td = document.createElement('td');
201
+ td.className = 'twm-dt-cell--placeholder';
202
+ return td;
203
+ });
204
+ }
205
+
206
+ /**
207
+ * After the header is built and in the header table: the placeholders'
208
+ * header and filter cells, and the group table above the header table.
209
+ */
210
+ decorate(headerWrap, headerTable) {
211
+ this._el = null;
212
+ if (!headerWrap || !headerTable) return;
213
+ const thead = headerTable.querySelector('thead');
214
+ const labelRow = thead?.querySelector('tr');
215
+ if (!labelRow) return;
216
+ if (this.placeholderCount()) {
217
+ this._insert(labelRow, (g) => this._placeholderHead(g));
218
+ for (const tr of thead.querySelectorAll('tr.twm-data-table__filter-row')) {
219
+ this._insert(tr, () => {
220
+ const th = document.createElement('th');
221
+ th.className = 'data-table__filter-cell data-table__filter-cell--empty twm-dt-col--placeholder';
222
+ return th;
223
+ });
224
+ }
225
+ }
226
+ const groupTable = this._buildGroupTable(headerTable);
227
+ headerWrap.insertBefore(groupTable, headerTable);
228
+ this._el = groupTable;
229
+ this.align();
230
+ }
231
+
232
+ _placeholderHead(group) {
233
+ const th = document.createElement('th');
234
+ th.className = 'text twm-dt-col--placeholder';
235
+ if (typeof this._cfg().onColumnAdd === 'function') {
236
+ th.appendChild(this._addButton(group));
237
+ }
238
+ return th;
239
+ }
240
+
241
+ _addButton(group) {
242
+ return headerActionButton('twm-dt-group__add', { icon: 'add', title: `Add column to ${group.label}` },
243
+ () => callOption(this._cfg(), 'onColumnAdd', group.key));
244
+ }
245
+
246
+ _buildGroupTable(headerTable) {
247
+ const { groups, slots } = this.layout();
248
+ const lead = this.table._lead().all;
249
+ const table = document.createElement('table');
250
+ table.className = `${headerTable.className} twm-dt-groups`;
251
+ const colgroup = document.createElement('colgroup');
252
+ for (let i = 0; i < lead + slots.length; i++) colgroup.appendChild(document.createElement('col'));
253
+ table.appendChild(colgroup);
254
+ const thead = document.createElement('thead');
255
+ const tr = document.createElement('tr');
256
+ tr.className = 'twm-dt-group-row';
257
+ if (lead) {
258
+ const th = document.createElement('th');
259
+ th.className = 'twm-dt-group twm-dt-group--lead';
260
+ th.colSpan = lead;
261
+ tr.appendChild(th);
262
+ }
263
+ // Runs of the same group (or of no group) are one cell.
264
+ let i = 0;
265
+ while (i < slots.length) {
266
+ const gi = slots[i].group;
267
+ let j = i + 1;
268
+ while (j < slots.length && slots[j].group === gi) j++;
269
+ tr.appendChild(gi === null ? blankSpanCell(j - i) : this._groupCell(groups[gi], j - i, slots[i].placeholder));
270
+ i = j;
271
+ }
272
+ thead.appendChild(tr);
273
+ table.appendChild(thead);
274
+ return table;
275
+ }
276
+
277
+ _groupCell(group, span, empty) {
278
+ const th = labelSpanCell(span, group.label);
279
+ if (empty) th.classList.add('twm-dt-group--empty');
280
+
281
+ const actions = document.createElement('span');
282
+ actions.className = 'twm-dt-group__actions';
283
+ const cfg = this._cfg();
284
+ // The consumer's group object from the live config: the layout is
285
+ // memoised on key, label and columns, so a group whose other fields
286
+ // changed is still asked about with its current object.
287
+ const items = callOption(cfg, 'groupActions', cfg.columnGroups[group.index], { key: group.key, columns: group.columns.slice(), table: this.table });
288
+ for (const item of Array.isArray(items) ? items.filter(Boolean) : []) {
289
+ actions.appendChild(headerActionButton('twm-dt-group__action', item,
290
+ () => callOption(cfg, 'onGroupAction', item.action, group.key)));
291
+ }
292
+ if (typeof cfg.onColumnAdd === 'function') actions.appendChild(this._addButton(group));
293
+ if (actions.childNodes.length) th.appendChild(actions);
294
+ return th;
295
+ }
296
+
297
+ /** The group table follows the header table: the same column widths,
298
+ * table width, layout and sideways offset. */
299
+ align() {
300
+ const groupTable = this._el;
301
+ const headerTable = this.table._headerTableEl;
302
+ if (!groupTable || !headerTable) return;
303
+ const labelRow = headerTable.querySelector('thead > tr');
304
+ const cols = groupTable.querySelector('colgroup')?.children || [];
305
+ for (let i = 0; i < cols.length; i++) {
306
+ const th = labelRow?.children[i];
307
+ const w = th ? parseFloat(th.style.width) : NaN;
308
+ cols[i].style.width = Number.isFinite(w) ? `${w}px` : '';
309
+ }
310
+ groupTable.style.width = headerTable.style.width;
311
+ groupTable.style.tableLayout = headerTable.style.tableLayout;
312
+ groupTable.style.transform = headerTable.style.transform;
313
+ }
314
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * column_order.js — STORY-0086. The order DataTable draws its data columns in
3
+ * (`reorderable`, `columnOrder`, `moveColumn`, `getColumnOrder`), and the drag
4
+ * on the header that changes it. Internal to the table, like `cell_grid.js`.
5
+ *
6
+ * ══ AN ORDER IS A LIST OF DATA INDICES ══════════════════════════════════
7
+ *
8
+ * The order is every data column's index (hidden ones included), in the order
9
+ * shown: `[0, 3, 1, 2]` draws column 3 second. Everything else the table keeps
10
+ * by column — `hiddenColumns`, the sort, the filters, `renderCell`'s `colIdx`,
11
+ * the cell mode's `col` — stays a data index and so stays on its column. Only
12
+ * what is BY DRAWN POSITION (the cells of a row, the widths) follows the order.
13
+ *
14
+ * A table with no order asked for (`null`) draws 0, 1, 2, … as in 0.16. An
15
+ * order that is not a permutation of today's columns — a saved one from when
16
+ * the table had a different number of columns — is no order: the natural one
17
+ * is drawn.
18
+ */
19
+
20
+ /** Whether `order` is a permutation of `0..n-1`. */
21
+ export function isColumnOrder(order, n) {
22
+ if (!Array.isArray(order) || order.length !== n) return false;
23
+ const seen = new Set();
24
+ for (const c of order) {
25
+ if (!Number.isInteger(c) || c < 0 || c >= n || seen.has(c)) return false;
26
+ seen.add(c);
27
+ }
28
+ return true;
29
+ }
30
+
31
+ /** `0..n-1`. */
32
+ export function naturalOrder(n) {
33
+ return Array.from({ length: Math.max(0, n) }, (_, i) => i);
34
+ }
35
+
36
+ /**
37
+ * The data indices of a row of `count` cells in the order drawn: `order`
38
+ * where it holds, and any cell past the headers (a row longer than them) after
39
+ * it, as 0.16 drew them.
40
+ */
41
+ export function cellOrder(order, count) {
42
+ if (!order) return naturalOrder(count);
43
+ const out = order.filter((c) => c < count);
44
+ for (let c = order.length; c < count; c++) out.push(c);
45
+ return out;
46
+ }
47
+
48
+ /** `order` with the column at position `from` taken out and put back at
49
+ * position `to` (positions in `order`). A copy; `order` is not changed. */
50
+ export function moveInOrder(order, from, to) {
51
+ const next = order.slice();
52
+ const [moved] = next.splice(from, 1);
53
+ next.splice(to, 0, moved);
54
+ return next;
55
+ }
56
+
57
+ /**
58
+ * The header drag. Every data column's header cell is draggable; dropping one
59
+ * on another data column's header moves it to that column's place. A press
60
+ * that starts on a resize grip is the resize's, never a drag. The row-number
61
+ * and select columns are no data columns: they do not drag and take no drop,
62
+ * so nothing lands in front of them.
63
+ */
64
+ export class ColumnReorder {
65
+ /** @param {import('./data_table.js').DataTable} table */
66
+ constructor(table) {
67
+ this.table = table;
68
+ /** The data column being dragged, or null. */
69
+ this.dragging = null;
70
+ /** Whether the last press on a header cell was on its resize grip. */
71
+ this.onGrip = false;
72
+ }
73
+
74
+ /** Wire the header row of a fresh render. Its cells are new each render,
75
+ * so nothing needs taking off. */
76
+ install(headRow) {
77
+ if (!headRow) return;
78
+ [...headRow.children].forEach((th, domIdx) => {
79
+ const col = this.table._dataIdxOfDom(domIdx);
80
+ if (col === null) return;
81
+ th.draggable = true;
82
+ th.classList.add('twm-dt-col--movable');
83
+ // Capture: the grip stops its own mousedown, so a listener on the
84
+ // way up would never hear it.
85
+ th.addEventListener('mousedown', (ev) => {
86
+ this.onGrip = !!ev.target?.closest?.('.twm-dt-col-resizer');
87
+ }, true);
88
+ th.addEventListener('dragstart', (ev) => {
89
+ if (this.onGrip || ev.target?.closest?.('.twm-dt-col-resizer')) {
90
+ ev.preventDefault();
91
+ return;
92
+ }
93
+ this.dragging = col;
94
+ try {
95
+ ev.dataTransfer?.setData('text/plain', String(col));
96
+ if (ev.dataTransfer) ev.dataTransfer.effectAllowed = 'move';
97
+ } catch (_) { /* a drag with no data transfer */ }
98
+ th.classList.add('twm-dt-col--dragging');
99
+ });
100
+ th.addEventListener('dragover', (ev) => {
101
+ if (this.dragging === null || this.dragging === col) return;
102
+ ev.preventDefault(); // a drop is welcome here
103
+ if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
104
+ // Moved right it lands AFTER the column it is dropped on.
105
+ const order = this.table.getColumnOrder();
106
+ th.classList.add('twm-dt-col--drop');
107
+ th.classList.toggle('twm-dt-col--drop-after', order.indexOf(col) > order.indexOf(this.dragging));
108
+ });
109
+ th.addEventListener('dragleave', () => th.classList.remove('twm-dt-col--drop', 'twm-dt-col--drop-after'));
110
+ th.addEventListener('drop', (ev) => {
111
+ th.classList.remove('twm-dt-col--drop', 'twm-dt-col--drop-after');
112
+ const from = this.dragging;
113
+ this.dragging = null;
114
+ if (from === null || from === col) return;
115
+ ev.preventDefault();
116
+ const order = this.table.getColumnOrder();
117
+ this.table.moveColumn(order.indexOf(from), order.indexOf(col));
118
+ });
119
+ th.addEventListener('dragend', () => {
120
+ th.classList.remove('twm-dt-col--dragging');
121
+ this.dragging = null;
122
+ });
123
+ });
124
+ }
125
+
126
+ /** "Move left" / "Move right" for the header menu of data column `col`:
127
+ * past the shown column next to it. */
128
+ menuItems(col) {
129
+ const shown = this.table._shownColumns();
130
+ const at = shown.indexOf(col);
131
+ if (at < 0) return [];
132
+ const order = this.table.getColumnOrder();
133
+ const step = (to) => () => this.table.moveColumn(order.indexOf(col), order.indexOf(to));
134
+ return [
135
+ { label: 'Move left', icon: 'arrow_back', action: 'move-left', disabled: at === 0,
136
+ onChoose: at === 0 ? undefined : step(shown[at - 1]) },
137
+ { label: 'Move right', icon: 'arrow_forward', action: 'move-right',
138
+ disabled: at === shown.length - 1,
139
+ onChoose: at === shown.length - 1 ? undefined : step(shown[at + 1]) },
140
+ ];
141
+ }
142
+ }