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.
- package/CHANGELOG.md +112 -0
- package/LICENSE +0 -0
- package/README.md +2435 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/canvas.js +18 -0
- package/charts.js +21 -0
- package/core.js +14 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3165 -159
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +707 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
- package/dist/chunk-LWV23YVE.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
- package/dist/chunk-RMTRWWEO.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3165 -159
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3290 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/editor.js +20 -0
- package/flow.js +21 -0
- package/host.js +14 -0
- package/package.json +12 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +293 -0
- package/src/ui/components/cell_editors.js +480 -0
- package/src/ui/components/cell_grid.js +761 -0
- package/src/ui/components/cell_paste.js +243 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +696 -71
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +361 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/tsv.js +74 -0
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/tiles.js +27 -0
- package/widgets.js +69 -0
- package/wm.js +28 -0
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-B5Q53LAJ.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- 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
|
+
}
|