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.
- package/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +2261 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3373 -238
- 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 +709 -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-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
- package/dist/chunk-MCIXFNVF.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
- package/dist/chunk-Q36EPT2R.js.map +7 -0
- package/dist/chunk-QWQJUDPD.js +610 -0
- package/dist/chunk-QWQJUDPD.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-UWO43BZU.js +111 -0
- 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 +3373 -238
- 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 +1131 -253
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3254 -2112
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -345
- package/dist/wm.js.map +4 -4
- package/package.json +3 -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 +422 -0
- package/src/tiles/scope_picker.js +96 -0
- package/src/tiles/tile_base.js +12 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +38 -1
- 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 +211 -0
- package/src/ui/components/cell_grid.js +648 -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 +601 -62
- 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 +26 -4
- package/src/ui/components/staged_edits.js +343 -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/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/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-CWPMLX67.js +0 -475
- package/dist/chunk-CWPMLX67.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/src/ui/controllers/panel_state_machine.js +0 -634
|
@@ -1,7 +1,368 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isRowActivation,
|
|
3
3
|
isRowControl
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-CVMCZZX2.js";
|
|
5
|
+
|
|
6
|
+
// src/ui/components/cell_editor_host.js
|
|
7
|
+
function createTextEditor({ value, initialText = null }) {
|
|
8
|
+
const input = document.createElement("input");
|
|
9
|
+
input.type = "text";
|
|
10
|
+
input.className = "twm-dt-editor";
|
|
11
|
+
input.spellcheck = false;
|
|
12
|
+
input.setAttribute("autocomplete", "off");
|
|
13
|
+
const start = value == null ? "" : String(value);
|
|
14
|
+
input.value = initialText != null ? String(initialText) : start;
|
|
15
|
+
return {
|
|
16
|
+
el: input,
|
|
17
|
+
getValue: () => initialText == null && input.value === start ? value : input.value,
|
|
18
|
+
focus: () => {
|
|
19
|
+
input.focus({ preventScroll: true });
|
|
20
|
+
const end = input.value.length;
|
|
21
|
+
try {
|
|
22
|
+
input.setSelectionRange(end, end);
|
|
23
|
+
} catch (_) {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function normalise(made) {
|
|
29
|
+
if (!made) return null;
|
|
30
|
+
const isNode = typeof Node !== "undefined" && made instanceof Node;
|
|
31
|
+
const el = isNode ? made : made.el;
|
|
32
|
+
if (!el || typeof Node !== "undefined" && !(el instanceof Node)) return null;
|
|
33
|
+
const own = isNode ? {} : made;
|
|
34
|
+
let getValue = typeof own.getValue === "function" ? own.getValue : null;
|
|
35
|
+
if (!getValue && el.matches?.("input, select, textarea")) getValue = () => el.value;
|
|
36
|
+
return {
|
|
37
|
+
el,
|
|
38
|
+
getValue,
|
|
39
|
+
focus: typeof own.focus === "function" ? own.focus : null,
|
|
40
|
+
destroy: typeof own.destroy === "function" ? own.destroy : null
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
var CellEditorHost = class {
|
|
44
|
+
/**
|
|
45
|
+
* @param {object} o
|
|
46
|
+
* @param {(session, value, move, reason) => void} o.onCommit after the
|
|
47
|
+
* cell is drawn as it was again; `move` is 'down', 'up', 'right', 'left'
|
|
48
|
+
* or null; `reason` is 'key', 'blur', 'editor' or 'scroll'.
|
|
49
|
+
* @param {(session, reason) => void} o.onCancel
|
|
50
|
+
*/
|
|
51
|
+
constructor({ onCommit, onCancel }) {
|
|
52
|
+
this._onCommit = onCommit;
|
|
53
|
+
this._onCancel = onCancel;
|
|
54
|
+
this.session = null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Open an editor in `td`. Returns whether one opened (a factory that
|
|
58
|
+
* throws or returns nothing opens none, and the cell is untouched).
|
|
59
|
+
*/
|
|
60
|
+
open({ td, factory, value, row, rowIdx, colIdx, column, initialText = null, ...extra }) {
|
|
61
|
+
if (this.session) return false;
|
|
62
|
+
const s = {
|
|
63
|
+
...extra,
|
|
64
|
+
td,
|
|
65
|
+
value,
|
|
66
|
+
row,
|
|
67
|
+
rowIdx,
|
|
68
|
+
colIdx,
|
|
69
|
+
column,
|
|
70
|
+
previous: value,
|
|
71
|
+
closed: false,
|
|
72
|
+
offs: [],
|
|
73
|
+
saved: null,
|
|
74
|
+
editor: null
|
|
75
|
+
};
|
|
76
|
+
let made = null;
|
|
77
|
+
try {
|
|
78
|
+
made = factory({
|
|
79
|
+
value,
|
|
80
|
+
row,
|
|
81
|
+
rowIdx,
|
|
82
|
+
colIdx,
|
|
83
|
+
column,
|
|
84
|
+
initialText,
|
|
85
|
+
commit: (v) => this._finish(s, v, null, "editor"),
|
|
86
|
+
cancel: () => this._cancel(s, "editor")
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error("[DataTable] cell editor factory threw", err);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const editor = normalise(made);
|
|
93
|
+
if (!editor) return false;
|
|
94
|
+
s.editor = editor;
|
|
95
|
+
s.saved = [...td.childNodes];
|
|
96
|
+
s.savedTitle = td.getAttribute("title");
|
|
97
|
+
this.session = s;
|
|
98
|
+
td.classList.add("twm-dt-cell--editing");
|
|
99
|
+
td.replaceChildren(editor.el);
|
|
100
|
+
const on = (type, fn) => {
|
|
101
|
+
editor.el.addEventListener(type, fn);
|
|
102
|
+
s.offs.push(() => editor.el.removeEventListener(type, fn));
|
|
103
|
+
};
|
|
104
|
+
on("keydown", (ev) => {
|
|
105
|
+
if (ev.isComposing) return;
|
|
106
|
+
ev.stopPropagation();
|
|
107
|
+
if (ev.key === "Escape") {
|
|
108
|
+
ev.preventDefault();
|
|
109
|
+
this._cancel(s, "key");
|
|
110
|
+
} else if (ev.key === "Enter" && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.getValue) {
|
|
111
|
+
ev.preventDefault();
|
|
112
|
+
this._finish(s, editor.getValue(), ev.shiftKey ? "up" : "down", "key");
|
|
113
|
+
} else if (ev.key === "Tab" && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.getValue) {
|
|
114
|
+
ev.preventDefault();
|
|
115
|
+
this._finish(s, editor.getValue(), ev.shiftKey ? "left" : "right", "key");
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
for (const type of ["click", "dblclick", "mousedown", "contextmenu"]) {
|
|
119
|
+
on(type, (ev) => ev.stopPropagation());
|
|
120
|
+
}
|
|
121
|
+
on("focusout", (ev) => {
|
|
122
|
+
if (s.closed || !editor.getValue) return;
|
|
123
|
+
const to = ev.relatedTarget;
|
|
124
|
+
if (to && editor.el.contains(to)) return;
|
|
125
|
+
const doc = editor.el.ownerDocument;
|
|
126
|
+
if (!to && doc && typeof doc.hasFocus === "function" && !doc.hasFocus()) return;
|
|
127
|
+
this._finish(s, editor.getValue(), null, "blur");
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
if (editor.focus) editor.focus();
|
|
131
|
+
else editor.el.focus?.({ preventScroll: true });
|
|
132
|
+
} catch (_) {
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
/** Commit the open session with what its editor holds. */
|
|
137
|
+
commit(move = null, reason = "editor") {
|
|
138
|
+
const s = this.session;
|
|
139
|
+
if (!s) return false;
|
|
140
|
+
if (!s.editor.getValue) {
|
|
141
|
+
this._cancel(s, reason);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return this._finish(s, s.editor.getValue(), move, reason);
|
|
145
|
+
}
|
|
146
|
+
/** Cancel the open session: the cell is drawn as it was. */
|
|
147
|
+
cancel(reason = "editor") {
|
|
148
|
+
const s = this.session;
|
|
149
|
+
if (!s) return false;
|
|
150
|
+
return this._cancel(s, reason);
|
|
151
|
+
}
|
|
152
|
+
/** Take the editor out and put the cell's own content back. */
|
|
153
|
+
_close(s) {
|
|
154
|
+
s.closed = true;
|
|
155
|
+
if (this.session === s) this.session = null;
|
|
156
|
+
for (const off of s.offs) {
|
|
157
|
+
try {
|
|
158
|
+
off();
|
|
159
|
+
} catch (_) {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
s.offs = [];
|
|
163
|
+
try {
|
|
164
|
+
s.editor.destroy?.();
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error("[DataTable] cell editor destroy threw", err);
|
|
167
|
+
}
|
|
168
|
+
s.td.classList.remove("twm-dt-cell--editing");
|
|
169
|
+
s.td.replaceChildren(...s.saved);
|
|
170
|
+
if (s.savedTitle == null) s.td.removeAttribute("title");
|
|
171
|
+
else s.td.setAttribute("title", s.savedTitle);
|
|
172
|
+
}
|
|
173
|
+
_finish(s, value, move, reason) {
|
|
174
|
+
if (s.closed) return false;
|
|
175
|
+
this._close(s);
|
|
176
|
+
this._onCommit?.(s, value, move, reason);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
_cancel(s, reason) {
|
|
180
|
+
if (s.closed) return false;
|
|
181
|
+
this._close(s);
|
|
182
|
+
this._onCancel?.(s, reason);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/ui/components/cell_selection.js
|
|
188
|
+
var CellSelection = class {
|
|
189
|
+
constructor() {
|
|
190
|
+
this.anchor = null;
|
|
191
|
+
this.focus = null;
|
|
192
|
+
this.first = 0;
|
|
193
|
+
this.last = -1;
|
|
194
|
+
this.cols = [];
|
|
195
|
+
}
|
|
196
|
+
/** The rows (positions, inclusive) and columns (data indices, shown order)
|
|
197
|
+
* the cursor may stand on. A selection outside them is pulled in. */
|
|
198
|
+
setBounds(first, last, cols) {
|
|
199
|
+
this.first = first;
|
|
200
|
+
this.last = last;
|
|
201
|
+
this.cols = Array.isArray(cols) ? cols.slice() : [];
|
|
202
|
+
if (this.isEmptyGrid()) {
|
|
203
|
+
this.anchor = null;
|
|
204
|
+
this.focus = null;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (this.focus) this.focus = this._clamp(this.focus);
|
|
208
|
+
if (this.anchor) this.anchor = this._clamp(this.anchor);
|
|
209
|
+
}
|
|
210
|
+
isEmptyGrid() {
|
|
211
|
+
return this.last < this.first || this.cols.length === 0;
|
|
212
|
+
}
|
|
213
|
+
_ord(col) {
|
|
214
|
+
const i = this.cols.indexOf(col);
|
|
215
|
+
return i >= 0 ? i : 0;
|
|
216
|
+
}
|
|
217
|
+
_clamp({ row, col }) {
|
|
218
|
+
const r = Math.max(this.first, Math.min(this.last, row));
|
|
219
|
+
const c = this.cols.includes(col) ? col : this.cols[Math.max(0, Math.min(this.cols.length - 1, this._nearestOrd(col)))];
|
|
220
|
+
return { row: r, col: c };
|
|
221
|
+
}
|
|
222
|
+
/** A column that is not shown any more: the shown one nearest its index. */
|
|
223
|
+
_nearestOrd(col) {
|
|
224
|
+
let best = 0;
|
|
225
|
+
for (let i = 0; i < this.cols.length; i++) if (this.cols[i] <= col) best = i;
|
|
226
|
+
return best;
|
|
227
|
+
}
|
|
228
|
+
_inside(row, ord) {
|
|
229
|
+
return row >= this.first && row <= this.last && ord >= 0 && ord < this.cols.length;
|
|
230
|
+
}
|
|
231
|
+
/** Put the cursor on one cell and collapse the range onto it. */
|
|
232
|
+
setCursor(row, col) {
|
|
233
|
+
if (this.isEmptyGrid()) return false;
|
|
234
|
+
this.focus = this._clamp({ row, col });
|
|
235
|
+
this.anchor = { ...this.focus };
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
/** Move the cursor to a cell and keep the anchor: the range grows. */
|
|
239
|
+
extendTo(row, col) {
|
|
240
|
+
if (!this.anchor) return this.setCursor(row, col);
|
|
241
|
+
if (this.isEmptyGrid()) return false;
|
|
242
|
+
this.focus = this._clamp({ row, col });
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
_go(cell, extend) {
|
|
246
|
+
return extend ? this.extendTo(cell.row, cell.col) : this.setCursor(cell.row, cell.col);
|
|
247
|
+
}
|
|
248
|
+
/** One step: the arrows. At an edge the cursor stays put. */
|
|
249
|
+
move(dRow, dCol, { extend = false } = {}) {
|
|
250
|
+
if (!this.focus) return this.setCursor(this.first, this.cols[0]);
|
|
251
|
+
const ord = Math.max(0, Math.min(this.cols.length - 1, this._ord(this.focus.col) + dCol));
|
|
252
|
+
const row = Math.max(this.first, Math.min(this.last, this.focus.row + dRow));
|
|
253
|
+
return this._go({ row, col: this.cols[ord] }, extend);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Ctrl+arrow: to the edge of the block of filled cells, as a spreadsheet
|
|
257
|
+
* does. From a filled cell whose neighbour is filled, to the last filled
|
|
258
|
+
* cell of that run; otherwise to the next filled cell along the line, or
|
|
259
|
+
* the edge of the grid when there is none.
|
|
260
|
+
*
|
|
261
|
+
* @param {number} dRow -1, 0 or 1
|
|
262
|
+
* @param {number} dCol -1, 0 or 1
|
|
263
|
+
* @param {(row:number, col:number) => boolean} filled
|
|
264
|
+
*/
|
|
265
|
+
jump(dRow, dCol, filled, { extend = false } = {}) {
|
|
266
|
+
if (!this.focus) return this.setCursor(this.first, this.cols[0]);
|
|
267
|
+
let r = this.focus.row;
|
|
268
|
+
let o = this._ord(this.focus.col);
|
|
269
|
+
const isFilled = (row, ord) => !!filled(row, this.cols[ord]);
|
|
270
|
+
if (this._inside(r + dRow, o + dCol)) {
|
|
271
|
+
if (isFilled(r, o) && isFilled(r + dRow, o + dCol)) {
|
|
272
|
+
while (this._inside(r + dRow, o + dCol) && isFilled(r + dRow, o + dCol)) {
|
|
273
|
+
r += dRow;
|
|
274
|
+
o += dCol;
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
r += dRow;
|
|
278
|
+
o += dCol;
|
|
279
|
+
while (!isFilled(r, o) && this._inside(r + dRow, o + dCol)) {
|
|
280
|
+
r += dRow;
|
|
281
|
+
o += dCol;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return this._go({ row: r, col: this.cols[o] }, extend);
|
|
286
|
+
}
|
|
287
|
+
/** Home / End: the first or last column of the cursor's row; with
|
|
288
|
+
* `corner` (Ctrl), the first or last cell of the grid. */
|
|
289
|
+
lineEnd(toEnd, { extend = false, corner = false } = {}) {
|
|
290
|
+
if (!this.focus) return this.setCursor(this.first, this.cols[0]);
|
|
291
|
+
const col = toEnd ? this.cols[this.cols.length - 1] : this.cols[0];
|
|
292
|
+
const row = corner ? toEnd ? this.last : this.first : this.focus.row;
|
|
293
|
+
return this._go({ row, col }, extend);
|
|
294
|
+
}
|
|
295
|
+
/** Tab: one cell right, wrapping to the first cell of the next row (Shift:
|
|
296
|
+
* left, wrapping to the last cell of the row before). The last cell of
|
|
297
|
+
* the grid (the first, going back) stays put. */
|
|
298
|
+
tab(back = false) {
|
|
299
|
+
if (!this.focus) return this.setCursor(this.first, this.cols[0]);
|
|
300
|
+
let row = this.focus.row;
|
|
301
|
+
let ord = this._ord(this.focus.col) + (back ? -1 : 1);
|
|
302
|
+
if (ord >= this.cols.length) {
|
|
303
|
+
if (row >= this.last) return this.setCursor(row, this.focus.col);
|
|
304
|
+
row += 1;
|
|
305
|
+
ord = 0;
|
|
306
|
+
} else if (ord < 0) {
|
|
307
|
+
if (row <= this.first) return this.setCursor(row, this.focus.col);
|
|
308
|
+
row -= 1;
|
|
309
|
+
ord = this.cols.length - 1;
|
|
310
|
+
}
|
|
311
|
+
return this.setCursor(row, this.cols[ord]);
|
|
312
|
+
}
|
|
313
|
+
/** Whether Tab (Shift+Tab with `back`) has nowhere to go: the cursor is
|
|
314
|
+
* on the grid's last cell (its first, going back). The grid then lets
|
|
315
|
+
* the key move focus on, out of the table. */
|
|
316
|
+
tabEnds(back = false) {
|
|
317
|
+
if (!this.focus || this.isEmptyGrid()) return false;
|
|
318
|
+
const col = back ? this.cols[0] : this.cols[this.cols.length - 1];
|
|
319
|
+
const row = back ? this.first : this.last;
|
|
320
|
+
return this.focus.row === row && this.focus.col === col;
|
|
321
|
+
}
|
|
322
|
+
/** Every cell of the grid: from its first cell to its last, which is
|
|
323
|
+
* where the cursor goes. */
|
|
324
|
+
selectAll() {
|
|
325
|
+
if (this.isEmptyGrid()) return false;
|
|
326
|
+
this.anchor = { row: this.first, col: this.cols[0] };
|
|
327
|
+
this.focus = { row: this.last, col: this.cols[this.cols.length - 1] };
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
/** The range collapses onto the cursor. */
|
|
331
|
+
collapse() {
|
|
332
|
+
if (!this.focus) return false;
|
|
333
|
+
const was = this.anchor && (this.anchor.row !== this.focus.row || this.anchor.col !== this.focus.col);
|
|
334
|
+
this.anchor = { ...this.focus };
|
|
335
|
+
return !!was;
|
|
336
|
+
}
|
|
337
|
+
clear() {
|
|
338
|
+
this.anchor = null;
|
|
339
|
+
this.focus = null;
|
|
340
|
+
}
|
|
341
|
+
/** `{top, bottom, left, right, columns}` — rows as positions, `left` and
|
|
342
|
+
* `right` and `columns` as data indices — or null with nothing selected. */
|
|
343
|
+
range() {
|
|
344
|
+
if (!this.focus || !this.anchor) return null;
|
|
345
|
+
const a = this._ord(this.anchor.col);
|
|
346
|
+
const b = this._ord(this.focus.col);
|
|
347
|
+
const lo = Math.min(a, b);
|
|
348
|
+
const hi = Math.max(a, b);
|
|
349
|
+
return {
|
|
350
|
+
top: Math.min(this.anchor.row, this.focus.row),
|
|
351
|
+
bottom: Math.max(this.anchor.row, this.focus.row),
|
|
352
|
+
left: this.cols[lo],
|
|
353
|
+
right: this.cols[hi],
|
|
354
|
+
columns: this.cols.slice(lo, hi + 1)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
contains(row, col) {
|
|
358
|
+
const r = this.range();
|
|
359
|
+
if (!r || row < r.top || row > r.bottom) return false;
|
|
360
|
+
return r.columns.includes(col);
|
|
361
|
+
}
|
|
362
|
+
isCursor(row, col) {
|
|
363
|
+
return !!this.focus && this.focus.row === row && this.focus.col === col;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
5
366
|
|
|
6
367
|
// src/ui/utils/raf_resize_observer.js
|
|
7
368
|
function createRafResizeObserver(callback) {
|
|
@@ -355,116 +716,2403 @@ function matchDurationFilter(ms, filterText, shown) {
|
|
|
355
716
|
return ms !== target;
|
|
356
717
|
}
|
|
357
718
|
}
|
|
358
|
-
return String(shown ?? "").toLowerCase().includes(text.toLowerCase());
|
|
359
|
-
}
|
|
719
|
+
return String(shown ?? "").toLowerCase().includes(text.toLowerCase());
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/ui/components/table_filter_chips.js
|
|
723
|
+
var isAll = (v) => v === "" || v == null;
|
|
724
|
+
function createChipsFilter(ctx = {}, spec = {}) {
|
|
725
|
+
const options = Array.isArray(spec.options) ? spec.options : [];
|
|
726
|
+
const multiple = !!spec.multiple;
|
|
727
|
+
const showCounts = spec.counts !== false;
|
|
728
|
+
const el = document.createElement("div");
|
|
729
|
+
el.className = "twm-dt-filter-chips" + (multiple ? " twm-dt-filter-chips--multiple" : "");
|
|
730
|
+
el.setAttribute("role", "group");
|
|
731
|
+
const name = spec.label || ctx.header;
|
|
732
|
+
if (name) el.setAttribute("aria-label", String(name));
|
|
733
|
+
const valueOf = /* @__PURE__ */ new Map();
|
|
734
|
+
const chips = /* @__PURE__ */ new Map();
|
|
735
|
+
let value = null;
|
|
736
|
+
const keyOf = (v) => String(v ?? "");
|
|
737
|
+
const normalise2 = (v) => {
|
|
738
|
+
if (multiple) {
|
|
739
|
+
const list = Array.isArray(v) ? v : isAll(v) ? [] : [v];
|
|
740
|
+
return list.filter((x) => !isAll(x)).map(keyOf);
|
|
741
|
+
}
|
|
742
|
+
if (Array.isArray(v)) v = v[0];
|
|
743
|
+
return isAll(v) ? null : keyOf(v);
|
|
744
|
+
};
|
|
745
|
+
const chosen = () => multiple ? value.length === 0 : value === null;
|
|
746
|
+
for (const opt of options) {
|
|
747
|
+
const key3 = keyOf(opt.value);
|
|
748
|
+
valueOf.set(key3, opt.value);
|
|
749
|
+
const btn = document.createElement("button");
|
|
750
|
+
btn.type = "button";
|
|
751
|
+
btn.className = "twm-dt-filter-chip";
|
|
752
|
+
btn.dataset.value = key3;
|
|
753
|
+
if (opt.title) btn.title = String(opt.title);
|
|
754
|
+
if (opt.icon) {
|
|
755
|
+
const icon = document.createElement("span");
|
|
756
|
+
icon.className = "material-symbols-outlined twm-dt-filter-chip__icon";
|
|
757
|
+
icon.setAttribute("aria-hidden", "true");
|
|
758
|
+
icon.textContent = String(opt.icon);
|
|
759
|
+
btn.appendChild(icon);
|
|
760
|
+
}
|
|
761
|
+
const label = document.createElement("span");
|
|
762
|
+
label.className = "twm-dt-filter-chip__label";
|
|
763
|
+
label.textContent = String(opt.label ?? key3);
|
|
764
|
+
btn.appendChild(label);
|
|
765
|
+
let count = null;
|
|
766
|
+
if (showCounts) {
|
|
767
|
+
count = document.createElement("span");
|
|
768
|
+
count.className = "twm-dt-filter-chip__count";
|
|
769
|
+
count.textContent = "\u2013";
|
|
770
|
+
btn.appendChild(count);
|
|
771
|
+
}
|
|
772
|
+
btn.addEventListener("click", (ev) => {
|
|
773
|
+
ev.stopPropagation();
|
|
774
|
+
pick(key3);
|
|
775
|
+
});
|
|
776
|
+
chips.set(key3, { btn, count });
|
|
777
|
+
el.appendChild(btn);
|
|
778
|
+
}
|
|
779
|
+
function outward() {
|
|
780
|
+
if (multiple) return value.length ? value.map((k) => valueOf.get(k)) : null;
|
|
781
|
+
return value === null ? null : valueOf.get(value);
|
|
782
|
+
}
|
|
783
|
+
function pick(key3) {
|
|
784
|
+
if (isAll(key3)) value = multiple ? [] : null;
|
|
785
|
+
else if (multiple) {
|
|
786
|
+
value = value.includes(key3) ? value.filter((k) => k !== key3) : [...value, key3];
|
|
787
|
+
} else {
|
|
788
|
+
value = value === key3 ? null : key3;
|
|
789
|
+
}
|
|
790
|
+
paint();
|
|
791
|
+
const out = outward();
|
|
792
|
+
if (typeof ctx.setValue === "function") ctx.setValue(out);
|
|
793
|
+
try {
|
|
794
|
+
spec.onChange?.(out);
|
|
795
|
+
} catch (err) {
|
|
796
|
+
console.error("[chips filter] onChange threw", err);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
function paint() {
|
|
800
|
+
for (const [key3, { btn }] of chips) {
|
|
801
|
+
const on = isAll(key3) ? chosen() : multiple ? value.includes(key3) : value === key3;
|
|
802
|
+
btn.classList.toggle("twm-dt-filter-chip--on", on);
|
|
803
|
+
btn.setAttribute("aria-pressed", on ? "true" : "false");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function setValue(v) {
|
|
807
|
+
value = normalise2(v);
|
|
808
|
+
paint();
|
|
809
|
+
}
|
|
810
|
+
function setCounts(counts) {
|
|
811
|
+
for (const [key3, { btn, count }] of chips) {
|
|
812
|
+
const n = counts == null ? void 0 : counts[key3];
|
|
813
|
+
const known = typeof n === "number" && Number.isFinite(n);
|
|
814
|
+
if (count) count.textContent = known ? n.toLocaleString() : "\u2013";
|
|
815
|
+
btn.classList.toggle("twm-dt-filter-chip--empty", known && n === 0);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
setValue(ctx.value);
|
|
819
|
+
if (ctx.counts) setCounts(ctx.counts);
|
|
820
|
+
return {
|
|
821
|
+
el,
|
|
822
|
+
setValue,
|
|
823
|
+
setCounts,
|
|
824
|
+
getValue: outward,
|
|
825
|
+
destroy() {
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/ui/components/staged_history.js
|
|
831
|
+
var ACTIONS = Object.freeze(["cell:stage"]);
|
|
832
|
+
var DEFAULT_HISTORY_LIMIT = 100;
|
|
833
|
+
var StagedHistory = class {
|
|
834
|
+
/**
|
|
835
|
+
* @param {object} cfg
|
|
836
|
+
* @param {(entry, direction: 'undo'|'redo') => void} cfg.apply writes one
|
|
837
|
+
* leaf entry's `before` (undo) or `after` (redo) into the staged cells.
|
|
838
|
+
* Called once per leaf; the ring holds no reference to those cells.
|
|
839
|
+
* @param {({canUndo, canRedo, depth}) => void} [cfg.onState] after every
|
|
840
|
+
* change of the stacks.
|
|
841
|
+
* @param {number} [cfg.limit=100] entries kept; the oldest fall off.
|
|
842
|
+
*/
|
|
843
|
+
constructor({ apply, onState = null, limit = DEFAULT_HISTORY_LIMIT } = {}) {
|
|
844
|
+
if (typeof apply !== "function") {
|
|
845
|
+
throw new Error("StagedHistory needs an `apply(entry, direction)`: a ring with no writer would report every undo as done and change nothing.");
|
|
846
|
+
}
|
|
847
|
+
this.apply = apply;
|
|
848
|
+
this.onState = onState;
|
|
849
|
+
this.limit = Number.isFinite(limit) && limit >= 1 ? Math.floor(limit) : DEFAULT_HISTORY_LIMIT;
|
|
850
|
+
this.undoStack = [];
|
|
851
|
+
this.redoStack = [];
|
|
852
|
+
this.group = null;
|
|
853
|
+
this.depthOpen = 0;
|
|
854
|
+
this.applying = false;
|
|
855
|
+
}
|
|
856
|
+
get canUndo() {
|
|
857
|
+
return this.undoStack.length > 0;
|
|
858
|
+
}
|
|
859
|
+
get canRedo() {
|
|
860
|
+
return this.redoStack.length > 0;
|
|
861
|
+
}
|
|
862
|
+
get depth() {
|
|
863
|
+
return this.undoStack.length;
|
|
864
|
+
}
|
|
865
|
+
/** Open a group: everything recorded until the matching `endGroup` is one
|
|
866
|
+
* undo (a fill, a paste). */
|
|
867
|
+
beginGroup(label = "") {
|
|
868
|
+
this.depthOpen += 1;
|
|
869
|
+
if (this.depthOpen === 1) this.group = { action: "batch", label, entries: [] };
|
|
870
|
+
}
|
|
871
|
+
/** Close a group. An empty one is dropped, so it costs no undo step; one
|
|
872
|
+
* holding a single entry is that entry. */
|
|
873
|
+
endGroup() {
|
|
874
|
+
if (!this.depthOpen) return;
|
|
875
|
+
this.depthOpen -= 1;
|
|
876
|
+
if (this.depthOpen) return;
|
|
877
|
+
const group = this.group;
|
|
878
|
+
this.group = null;
|
|
879
|
+
if (!group) return;
|
|
880
|
+
if (group.entries.length === 1) this._push(group.entries[0]);
|
|
881
|
+
else if (group.entries.length) this._push(group);
|
|
882
|
+
}
|
|
883
|
+
/** Record one delta. Returns whether it was recorded (false while an
|
|
884
|
+
* entry is being applied). Throws on an action that is not in `ACTIONS`. */
|
|
885
|
+
record(entry) {
|
|
886
|
+
if (this.applying) return false;
|
|
887
|
+
if (!entry || !ACTIONS.includes(entry.action)) {
|
|
888
|
+
throw new Error(`${entry?.action} is not an undoable action: the ring reverses staged cells only, never a commit.`);
|
|
889
|
+
}
|
|
890
|
+
if (this.group) {
|
|
891
|
+
this.group.entries.push(entry);
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
this._push(entry);
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
undo() {
|
|
898
|
+
this._closeOpenGroup();
|
|
899
|
+
const entry = this.undoStack.pop();
|
|
900
|
+
if (!entry) return false;
|
|
901
|
+
this._run(entry, "undo");
|
|
902
|
+
this.redoStack.push(entry);
|
|
903
|
+
this._publish();
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
redo() {
|
|
907
|
+
this._closeOpenGroup();
|
|
908
|
+
const entry = this.redoStack.pop();
|
|
909
|
+
if (!entry) return false;
|
|
910
|
+
this._run(entry, "redo");
|
|
911
|
+
this.undoStack.push(entry);
|
|
912
|
+
this._publish();
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
/** Forget everything — after a commit or a discard, whose cells the
|
|
916
|
+
* entries no longer describe. An open group is closed with it. */
|
|
917
|
+
reset() {
|
|
918
|
+
this.undoStack = [];
|
|
919
|
+
this.redoStack = [];
|
|
920
|
+
this.group = null;
|
|
921
|
+
this.depthOpen = 0;
|
|
922
|
+
this._publish();
|
|
923
|
+
}
|
|
924
|
+
/** A group left open (a caller that threw) is closed before an undo, so it
|
|
925
|
+
* cannot swallow every later entry. */
|
|
926
|
+
_closeOpenGroup() {
|
|
927
|
+
if (!this.depthOpen) return;
|
|
928
|
+
this.depthOpen = 1;
|
|
929
|
+
this.endGroup();
|
|
930
|
+
}
|
|
931
|
+
_push(entry) {
|
|
932
|
+
this.undoStack.push(entry);
|
|
933
|
+
this.redoStack = [];
|
|
934
|
+
if (this.undoStack.length > this.limit) this.undoStack.splice(0, this.undoStack.length - this.limit);
|
|
935
|
+
this._publish();
|
|
936
|
+
}
|
|
937
|
+
_publish() {
|
|
938
|
+
try {
|
|
939
|
+
this.onState?.({ canUndo: this.canUndo, canRedo: this.canRedo, depth: this.undoStack.length });
|
|
940
|
+
} catch (err) {
|
|
941
|
+
console.error("[StagedHistory] onState threw", err);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
_run(entry, direction) {
|
|
945
|
+
this.applying = true;
|
|
946
|
+
try {
|
|
947
|
+
this._applyOne(entry, direction);
|
|
948
|
+
} finally {
|
|
949
|
+
this.applying = false;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
/** A group replays in REVERSE on the way back (two writes to one cell must
|
|
953
|
+
* unwind last first) and in order on the way forward. */
|
|
954
|
+
_applyOne(entry, direction) {
|
|
955
|
+
if (entry.action === "batch") {
|
|
956
|
+
const entries = direction === "undo" ? [...entry.entries].reverse() : entry.entries;
|
|
957
|
+
for (const child of entries) this._applyOne(child, direction);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
this.apply(entry, direction);
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// src/ui/components/series.js
|
|
965
|
+
var ISO_DATE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
966
|
+
var TRAILING_NUMBER = /^(.*?)(\d+)(\D*)$/;
|
|
967
|
+
var NUMERIC_TEXT = /^[-+]?(\d+\.?\d*|\.\d+)$/;
|
|
968
|
+
var DAY_MS = 864e5;
|
|
969
|
+
var isBlank = (v) => v === null || v === void 0 || v === "";
|
|
970
|
+
function detectSeries(values) {
|
|
971
|
+
if (!Array.isArray(values) || values.length === 0) return null;
|
|
972
|
+
if (values.every(isBlank)) return null;
|
|
973
|
+
if (values.length === 1) return { kind: "copy", step: 0 };
|
|
974
|
+
const numbers = values.map(toNumber);
|
|
975
|
+
if (numbers.every((n) => n !== null)) {
|
|
976
|
+
const step = numbers[1] - numbers[0];
|
|
977
|
+
const constant = numbers.every((n, i) => i === 0 || nearlyEqual(n - numbers[i - 1], step));
|
|
978
|
+
if (constant && step !== 0) return { kind: "number", step };
|
|
979
|
+
if (constant) return { kind: "copy", step: 0 };
|
|
980
|
+
return { kind: "cycle", step: 0 };
|
|
981
|
+
}
|
|
982
|
+
const days = values.map(toDayMs);
|
|
983
|
+
if (days.every((d) => d !== null)) {
|
|
984
|
+
const step = Math.round((days[1] - days[0]) / DAY_MS);
|
|
985
|
+
const constant = days.every((d, i) => i === 0 || Math.round((d - days[i - 1]) / DAY_MS) === step);
|
|
986
|
+
if (constant && step !== 0) return { kind: "date", step };
|
|
987
|
+
if (constant) return { kind: "copy", step: 0 };
|
|
988
|
+
return { kind: "cycle", step: 0 };
|
|
989
|
+
}
|
|
990
|
+
const parts = values.map((v) => typeof v === "string" ? TRAILING_NUMBER.exec(v) : null);
|
|
991
|
+
if (parts.every(Boolean)) {
|
|
992
|
+
const prefix = parts[0][1];
|
|
993
|
+
const suffix = parts[0][3];
|
|
994
|
+
if (parts.every((p) => p[1] === prefix && p[3] === suffix)) {
|
|
995
|
+
const nums = parts.map((p) => Number(p[2]));
|
|
996
|
+
const step = nums[1] - nums[0];
|
|
997
|
+
const constant = nums.every((n, i) => i === 0 || n - nums[i - 1] === step);
|
|
998
|
+
if (constant && step !== 0) {
|
|
999
|
+
return { kind: "text-number", step, prefix, suffix, pad: parts[0][2].length };
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return { kind: "cycle", step: 0 };
|
|
1004
|
+
}
|
|
1005
|
+
function projectSeries(values, count, { forceCopy = false } = {}) {
|
|
1006
|
+
const out = [];
|
|
1007
|
+
const n = Math.max(0, Math.floor(Number(count) || 0));
|
|
1008
|
+
if (!Array.isArray(values) || values.length === 0 || n === 0) return out;
|
|
1009
|
+
const series = forceCopy ? null : detectSeries(values);
|
|
1010
|
+
const last = values[values.length - 1];
|
|
1011
|
+
if (series?.kind === "number") {
|
|
1012
|
+
const base = toNumber(last);
|
|
1013
|
+
const decimals = Math.min(15, Math.max(...values.map(decimalsOf)));
|
|
1014
|
+
const allNumbers = values.every((v) => typeof v === "number");
|
|
1015
|
+
for (let i = 1; i <= n; i++) {
|
|
1016
|
+
const next = base + series.step * i;
|
|
1017
|
+
out.push(allNumbers ? Number(next.toFixed(decimals)) : next.toFixed(decimals));
|
|
1018
|
+
}
|
|
1019
|
+
return out;
|
|
1020
|
+
}
|
|
1021
|
+
if (series?.kind === "date") {
|
|
1022
|
+
const asDate = last instanceof Date;
|
|
1023
|
+
for (let i = 1; i <= n; i++) {
|
|
1024
|
+
if (asDate) {
|
|
1025
|
+
const next = new Date(last.getTime());
|
|
1026
|
+
next.setDate(next.getDate() + series.step * i);
|
|
1027
|
+
out.push(next);
|
|
1028
|
+
} else {
|
|
1029
|
+
const next = new Date(toDayMs(last));
|
|
1030
|
+
next.setUTCDate(next.getUTCDate() + series.step * i);
|
|
1031
|
+
out.push(next.toISOString().slice(0, 10));
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return out;
|
|
1035
|
+
}
|
|
1036
|
+
if (series?.kind === "text-number") {
|
|
1037
|
+
const base = Number(TRAILING_NUMBER.exec(last)[2]);
|
|
1038
|
+
for (let i = 1; i <= n; i++) {
|
|
1039
|
+
const next = base + series.step * i;
|
|
1040
|
+
const digits = String(Math.abs(next)).padStart(series.pad, "0");
|
|
1041
|
+
out.push(`${series.prefix}${next < 0 ? "-" : ""}${digits}${series.suffix}`);
|
|
1042
|
+
}
|
|
1043
|
+
return out;
|
|
1044
|
+
}
|
|
1045
|
+
for (let i = 0; i < n; i++) out.push(values[i % values.length]);
|
|
1046
|
+
return out;
|
|
1047
|
+
}
|
|
1048
|
+
function toNumber(value) {
|
|
1049
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
1050
|
+
if (typeof value !== "string") return null;
|
|
1051
|
+
const text = value.trim();
|
|
1052
|
+
return NUMERIC_TEXT.test(text) ? Number(text) : null;
|
|
1053
|
+
}
|
|
1054
|
+
function toDayMs(value) {
|
|
1055
|
+
if (value instanceof Date) {
|
|
1056
|
+
const t2 = value.getTime();
|
|
1057
|
+
return Number.isNaN(t2) ? null : t2;
|
|
1058
|
+
}
|
|
1059
|
+
if (typeof value !== "string" || !ISO_DATE2.test(value.trim())) return null;
|
|
1060
|
+
const text = value.trim();
|
|
1061
|
+
const t = (/* @__PURE__ */ new Date(`${text}T00:00:00Z`)).getTime();
|
|
1062
|
+
if (Number.isNaN(t) || new Date(t).toISOString().slice(0, 10) !== text) return null;
|
|
1063
|
+
return t;
|
|
1064
|
+
}
|
|
1065
|
+
function decimalsOf(value) {
|
|
1066
|
+
const text = typeof value === "number" ? String(value) : String(value).trim();
|
|
1067
|
+
if (/e/i.test(text)) return 15;
|
|
1068
|
+
return (text.split(".")[1] || "").length;
|
|
1069
|
+
}
|
|
1070
|
+
function nearlyEqual(a, b) {
|
|
1071
|
+
return Math.abs(a - b) < 1e-9;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/ui/components/inline_renamer.js
|
|
1075
|
+
function assertLabel(label) {
|
|
1076
|
+
if (!label || typeof label.appendChild !== "function") {
|
|
1077
|
+
throw new Error("[InlineRenamer] options.label must be an HTMLElement");
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function buildInput(label, options) {
|
|
1081
|
+
const input = document.createElement("input");
|
|
1082
|
+
input.type = "text";
|
|
1083
|
+
input.className = ["twm-node-edit-input", options?.inputClass || ""].join(" ").trim();
|
|
1084
|
+
input.placeholder = options?.placeholder || label.textContent || "Name";
|
|
1085
|
+
input.value = options?.initialValue ?? label.textContent ?? "";
|
|
1086
|
+
input.spellcheck = false;
|
|
1087
|
+
input.autocomplete = "off";
|
|
1088
|
+
input.setAttribute("aria-label", "Edit name");
|
|
1089
|
+
return input;
|
|
1090
|
+
}
|
|
1091
|
+
function attachInlineRenamer(options = {}) {
|
|
1092
|
+
const label = options.label;
|
|
1093
|
+
assertLabel(label);
|
|
1094
|
+
let input = null;
|
|
1095
|
+
let editing = false;
|
|
1096
|
+
let destroyed = false;
|
|
1097
|
+
let originalText = label.textContent || "";
|
|
1098
|
+
const cleanup = () => {
|
|
1099
|
+
if (input && input.parentNode === label) {
|
|
1100
|
+
input.remove();
|
|
1101
|
+
}
|
|
1102
|
+
input = null;
|
|
1103
|
+
editing = false;
|
|
1104
|
+
};
|
|
1105
|
+
const cancel = () => {
|
|
1106
|
+
if (destroyed) return;
|
|
1107
|
+
cleanup();
|
|
1108
|
+
label.textContent = originalText;
|
|
1109
|
+
options.onCancel?.({ value: originalText });
|
|
1110
|
+
};
|
|
1111
|
+
const commit = async (reason = "commit") => {
|
|
1112
|
+
if (destroyed || !input) return;
|
|
1113
|
+
const raw = input.value;
|
|
1114
|
+
let next = raw;
|
|
1115
|
+
try {
|
|
1116
|
+
next = options.transformInput ? options.transformInput(raw, { reason }) : raw;
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
window.logger?.warn("inline-renamer", "transformInput failed", err);
|
|
1119
|
+
}
|
|
1120
|
+
try {
|
|
1121
|
+
const result = await options.onCommit?.(next, { reason }) || {};
|
|
1122
|
+
if (result.success === false) {
|
|
1123
|
+
if (result.keepEditing) {
|
|
1124
|
+
input.focus();
|
|
1125
|
+
input.select();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
cancel();
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
const finalValue = result.newValue ?? next ?? "";
|
|
1132
|
+
label.textContent = finalValue;
|
|
1133
|
+
cleanup();
|
|
1134
|
+
options.onFinish?.({ value: finalValue });
|
|
1135
|
+
} catch (err) {
|
|
1136
|
+
window.logger?.warn("inline-renamer", "onCommit failed", err);
|
|
1137
|
+
cancel();
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
const start = (triggerEvent = null) => {
|
|
1141
|
+
if (destroyed || editing) return;
|
|
1142
|
+
editing = true;
|
|
1143
|
+
originalText = label.textContent || "";
|
|
1144
|
+
label.textContent = "";
|
|
1145
|
+
input = buildInput(label, { ...options, initialValue: originalText });
|
|
1146
|
+
label.appendChild(input);
|
|
1147
|
+
const stopEvents = (ev) => ev.stopPropagation();
|
|
1148
|
+
input.addEventListener("mousedown", stopEvents);
|
|
1149
|
+
input.addEventListener("click", stopEvents);
|
|
1150
|
+
input.addEventListener("keydown", (ev) => {
|
|
1151
|
+
if (ev.key === "Enter") {
|
|
1152
|
+
ev.preventDefault();
|
|
1153
|
+
commit("enter");
|
|
1154
|
+
} else if (ev.key === "Escape") {
|
|
1155
|
+
ev.preventDefault();
|
|
1156
|
+
cancel();
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
input.addEventListener("blur", () => commit("blur"));
|
|
1160
|
+
options.onStart?.({ value: originalText, triggerEvent });
|
|
1161
|
+
setTimeout(() => {
|
|
1162
|
+
input?.focus();
|
|
1163
|
+
input?.select?.();
|
|
1164
|
+
}, 0);
|
|
1165
|
+
};
|
|
1166
|
+
const destroy = () => {
|
|
1167
|
+
destroyed = true;
|
|
1168
|
+
cleanup();
|
|
1169
|
+
};
|
|
1170
|
+
return { start, cancel, commit, destroy };
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/ui/components/staged_edits.js
|
|
1174
|
+
var key = (rowIdx, colIdx) => `${rowIdx}:${colIdx}`;
|
|
1175
|
+
var FROZEN_SELECTORS = [
|
|
1176
|
+
"th.sortable",
|
|
1177
|
+
".data-table__filter-cell:not(.data-table__filter-cell--empty)",
|
|
1178
|
+
".data-table__filter-cell:not(.data-table__filter-cell--empty) input",
|
|
1179
|
+
".data-table__filter-cell:not(.data-table__filter-cell--empty) button",
|
|
1180
|
+
".data-table__filter-cell:not(.data-table__filter-cell--empty) select",
|
|
1181
|
+
".data-table__filter-cell:not(.data-table__filter-cell--empty) textarea",
|
|
1182
|
+
".twm-pagination-controls button:not(.twm-pagination-btn--export)",
|
|
1183
|
+
".twm-pagination-controls input",
|
|
1184
|
+
".twm-dt-paging button",
|
|
1185
|
+
".twm-dt-paging input"
|
|
1186
|
+
].join(", ");
|
|
1187
|
+
var SAVED_ATTRS = ["title", "data-refusal", "data-tooltip", "aria-disabled"];
|
|
1188
|
+
var StagedEdits = class {
|
|
1189
|
+
/** @param {import('./cell_grid.js').CellGrid} grid */
|
|
1190
|
+
constructor(grid) {
|
|
1191
|
+
this.grid = grid;
|
|
1192
|
+
this.table = grid.table;
|
|
1193
|
+
this.cells = /* @__PURE__ */ new Map();
|
|
1194
|
+
this.errors = /* @__PURE__ */ new Map();
|
|
1195
|
+
this.committing = null;
|
|
1196
|
+
this.generation = 0;
|
|
1197
|
+
this._touched = null;
|
|
1198
|
+
this._frozen = /* @__PURE__ */ new Map();
|
|
1199
|
+
const limit = this.table.config.historyLimit;
|
|
1200
|
+
this.history = new StagedHistory({
|
|
1201
|
+
apply: (entry, direction) => this._apply(entry, direction),
|
|
1202
|
+
limit: limit == null ? DEFAULT_HISTORY_LIMIT : limit
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
// ── what is staged ───────────────────────────────────────────────────
|
|
1206
|
+
has(k) {
|
|
1207
|
+
return this.cells.has(k);
|
|
1208
|
+
}
|
|
1209
|
+
value(k) {
|
|
1210
|
+
return this.cells.get(k)?.value;
|
|
1211
|
+
}
|
|
1212
|
+
/** The value the row holds for a cell: what "no change" means. */
|
|
1213
|
+
original(rowIdx, colIdx) {
|
|
1214
|
+
const row = this.grid.currentRow(rowIdx);
|
|
1215
|
+
return row ? row[colIdx] : void 0;
|
|
1216
|
+
}
|
|
1217
|
+
/** Every staged cell as `{rowIdx, colIdx, value, previous}`, by row then
|
|
1218
|
+
* column; `previous` is the value the row holds. */
|
|
1219
|
+
edits() {
|
|
1220
|
+
return [...this.cells.values()].sort((a, b) => a.rowIdx - b.rowIdx || a.colIdx - b.colIdx).map(({ rowIdx, colIdx, value }) => ({
|
|
1221
|
+
rowIdx,
|
|
1222
|
+
colIdx,
|
|
1223
|
+
value,
|
|
1224
|
+
previous: this.original(rowIdx, colIdx)
|
|
1225
|
+
}));
|
|
1226
|
+
}
|
|
1227
|
+
_side(k) {
|
|
1228
|
+
return this.cells.has(k) ? { has: true, value: this.cells.get(k).value } : { has: false, value: null };
|
|
1229
|
+
}
|
|
1230
|
+
/** Write one side of a delta: stage the value, or take the cell out. */
|
|
1231
|
+
_write(rowIdx, colIdx, side) {
|
|
1232
|
+
const k = key(rowIdx, colIdx);
|
|
1233
|
+
if (side.has) this.cells.set(k, { rowIdx, colIdx, value: side.value });
|
|
1234
|
+
else this.cells.delete(k);
|
|
1235
|
+
this.errors.delete(k);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Stage a value in a cell, as one undo entry (inside an open group, part
|
|
1239
|
+
* of it). The value its row holds takes the cell back to no change.
|
|
1240
|
+
* Returns whether anything changed.
|
|
1241
|
+
*/
|
|
1242
|
+
stage(rowIdx, colIdx, value) {
|
|
1243
|
+
if (this.committing || !this._stageOne(rowIdx, colIdx, value)) return false;
|
|
1244
|
+
this.grid._redraw(rowIdx, colIdx);
|
|
1245
|
+
this._changed();
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* STORY-0089. Stage many cells (`[{rowIdx, colIdx, value}]`) as ONE undo
|
|
1250
|
+
* entry — a fill, a paste — each as `stage` would; the rows are drawn and
|
|
1251
|
+
* the listeners told once, at the end. Returns how many cells changed.
|
|
1252
|
+
*/
|
|
1253
|
+
stageMany(cells, label = "") {
|
|
1254
|
+
if (this.committing) return 0;
|
|
1255
|
+
const rows = /* @__PURE__ */ new Set();
|
|
1256
|
+
let changed = 0;
|
|
1257
|
+
this.history.beginGroup(label);
|
|
1258
|
+
try {
|
|
1259
|
+
for (const { rowIdx, colIdx, value } of cells) {
|
|
1260
|
+
if (this._stageOne(rowIdx, colIdx, value)) {
|
|
1261
|
+
changed++;
|
|
1262
|
+
rows.add(rowIdx);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
} finally {
|
|
1266
|
+
this.history.endGroup();
|
|
1267
|
+
}
|
|
1268
|
+
for (const rowIdx of rows) this.grid._redraw(rowIdx);
|
|
1269
|
+
if (changed) this._changed();
|
|
1270
|
+
return changed;
|
|
1271
|
+
}
|
|
1272
|
+
/** One cell's new state and its delta in the ring; whether it changed. */
|
|
1273
|
+
_stageOne(rowIdx, colIdx, value) {
|
|
1274
|
+
const k = key(rowIdx, colIdx);
|
|
1275
|
+
const before = this._side(k);
|
|
1276
|
+
const after = Object.is(value, this.original(rowIdx, colIdx)) ? { has: false, value: null } : { has: true, value };
|
|
1277
|
+
if (before.has === after.has && Object.is(before.value, after.value)) return false;
|
|
1278
|
+
this._write(rowIdx, colIdx, after);
|
|
1279
|
+
this.history.record({ action: "cell:stage", row: rowIdx, col: colIdx, before, after });
|
|
1280
|
+
return true;
|
|
1281
|
+
}
|
|
1282
|
+
_apply(entry, direction) {
|
|
1283
|
+
this._write(entry.row, entry.col, direction === "undo" ? entry.before : entry.after);
|
|
1284
|
+
this._touched?.add(entry.row);
|
|
1285
|
+
}
|
|
1286
|
+
_walk(step) {
|
|
1287
|
+
if (this.committing) return false;
|
|
1288
|
+
this._touched = /* @__PURE__ */ new Set();
|
|
1289
|
+
let moved = false;
|
|
1290
|
+
try {
|
|
1291
|
+
moved = step();
|
|
1292
|
+
} finally {
|
|
1293
|
+
const rows = this._touched;
|
|
1294
|
+
this._touched = null;
|
|
1295
|
+
for (const rowIdx of rows) this.grid._redraw(rowIdx);
|
|
1296
|
+
}
|
|
1297
|
+
if (moved) this._changed();
|
|
1298
|
+
return moved;
|
|
1299
|
+
}
|
|
1300
|
+
undo() {
|
|
1301
|
+
return this._walk(() => this.history.undo());
|
|
1302
|
+
}
|
|
1303
|
+
redo() {
|
|
1304
|
+
return this._walk(() => this.history.redo());
|
|
1305
|
+
}
|
|
1306
|
+
/** Drop every staged cell, its marks and the ring. */
|
|
1307
|
+
discard() {
|
|
1308
|
+
if (this.committing) return false;
|
|
1309
|
+
const rows = this._rows();
|
|
1310
|
+
const had = this.cells.size > 0 || this.errors.size > 0;
|
|
1311
|
+
this.cells.clear();
|
|
1312
|
+
this.errors.clear();
|
|
1313
|
+
this.history.reset();
|
|
1314
|
+
for (const rowIdx of rows) this.grid._redraw(rowIdx);
|
|
1315
|
+
if (had) this._changed();
|
|
1316
|
+
return had;
|
|
1317
|
+
}
|
|
1318
|
+
/** New rows (`setData({rows})`): the cells named rows that are gone. */
|
|
1319
|
+
clear() {
|
|
1320
|
+
this.generation++;
|
|
1321
|
+
this.committing = null;
|
|
1322
|
+
const had = this.cells.size > 0 || this.errors.size > 0;
|
|
1323
|
+
this.cells.clear();
|
|
1324
|
+
this.errors.clear();
|
|
1325
|
+
this.history.reset();
|
|
1326
|
+
if (had) this._changed();
|
|
1327
|
+
}
|
|
1328
|
+
_rows() {
|
|
1329
|
+
const rows = /* @__PURE__ */ new Set();
|
|
1330
|
+
for (const c of this.cells.values()) rows.add(c.rowIdx);
|
|
1331
|
+
for (const k of this.errors.keys()) rows.add(Number(k.split(":")[0]));
|
|
1332
|
+
return rows;
|
|
1333
|
+
}
|
|
1334
|
+
// ── the commit ───────────────────────────────────────────────────────
|
|
1335
|
+
/**
|
|
1336
|
+
* Hand every staged cell to `onCommit({edits})`, once. Resolves
|
|
1337
|
+
* `{ok: true, committed}` when it succeeded — the values are written into
|
|
1338
|
+
* the rows the table holds, and the staged cells and the ring are gone —
|
|
1339
|
+
* or `{ok: false, errors}` when it refused: per-cell errors mark their
|
|
1340
|
+
* cells, a refusal with none marks every staged cell, and every staged
|
|
1341
|
+
* value stays.
|
|
1342
|
+
*/
|
|
1343
|
+
commit() {
|
|
1344
|
+
if (this.committing) return this.committing;
|
|
1345
|
+
if (this.grid.isEditing()) this.grid.editor.commit(null, "commit");
|
|
1346
|
+
const fn = this.table.config.onCommit;
|
|
1347
|
+
const edits = this.edits();
|
|
1348
|
+
if (!edits.length) return Promise.resolve({ ok: true, committed: 0 });
|
|
1349
|
+
if (typeof fn !== "function") {
|
|
1350
|
+
console.warn("[DataTable] commit() needs an `onCommit`; nothing was committed.");
|
|
1351
|
+
return Promise.resolve({ ok: false, errors: [] });
|
|
1352
|
+
}
|
|
1353
|
+
const generation = this.generation;
|
|
1354
|
+
const refused = (err) => ({ ok: false, error: err?.message || String(err ?? "Not saved") });
|
|
1355
|
+
let result;
|
|
1356
|
+
try {
|
|
1357
|
+
result = fn({ edits: edits.map((e) => ({ ...e })) });
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
result = refused(err);
|
|
1360
|
+
}
|
|
1361
|
+
const out = Promise.resolve(result).then((r) => r, refused).then((r) => {
|
|
1362
|
+
if (generation !== this.generation) return { ok: false, errors: [] };
|
|
1363
|
+
this.committing = null;
|
|
1364
|
+
return this._settle(edits, r);
|
|
1365
|
+
});
|
|
1366
|
+
this.committing = out;
|
|
1367
|
+
this._changed();
|
|
1368
|
+
return out;
|
|
1369
|
+
}
|
|
1370
|
+
_settle(edits, result) {
|
|
1371
|
+
const listed = Array.isArray(result?.errors) ? result.errors : [];
|
|
1372
|
+
if (listed.length || result?.ok === false) {
|
|
1373
|
+
const errors = [];
|
|
1374
|
+
const mark = (rowIdx, colIdx, message) => {
|
|
1375
|
+
const k = key(rowIdx, colIdx);
|
|
1376
|
+
if (!this.cells.has(k)) return;
|
|
1377
|
+
const text = String(message ?? "Not saved");
|
|
1378
|
+
this.errors.set(k, text);
|
|
1379
|
+
errors.push({ rowIdx, colIdx, error: text });
|
|
1380
|
+
};
|
|
1381
|
+
if (listed.length) {
|
|
1382
|
+
for (const e of listed) mark(e?.rowIdx, e?.colIdx, e?.error ?? e?.message);
|
|
1383
|
+
} else {
|
|
1384
|
+
for (const e of edits) mark(e.rowIdx, e.colIdx, result.error);
|
|
1385
|
+
}
|
|
1386
|
+
for (const rowIdx of this._rows()) this.grid._redraw(rowIdx);
|
|
1387
|
+
this._changed();
|
|
1388
|
+
return { ok: false, errors };
|
|
1389
|
+
}
|
|
1390
|
+
const rows = this._rows();
|
|
1391
|
+
for (const e of edits) {
|
|
1392
|
+
const row = this.grid.currentRow(e.rowIdx);
|
|
1393
|
+
if (Array.isArray(row)) row[e.colIdx] = e.value;
|
|
1394
|
+
}
|
|
1395
|
+
this.cells.clear();
|
|
1396
|
+
this.errors.clear();
|
|
1397
|
+
this.history.reset();
|
|
1398
|
+
for (const rowIdx of rows) this.grid._redraw(rowIdx);
|
|
1399
|
+
this._changed();
|
|
1400
|
+
return { ok: true, committed: edits.length };
|
|
1401
|
+
}
|
|
1402
|
+
// ── the freeze ───────────────────────────────────────────────────────
|
|
1403
|
+
/** Why sort, filters and pages are held, or null when nothing is staged. */
|
|
1404
|
+
freezeReason() {
|
|
1405
|
+
const n = this.cells.size;
|
|
1406
|
+
if (!n) return null;
|
|
1407
|
+
const own = this.table.config.stagedRefusal;
|
|
1408
|
+
if (typeof own === "function") {
|
|
1409
|
+
try {
|
|
1410
|
+
return String(own(n));
|
|
1411
|
+
} catch (err) {
|
|
1412
|
+
console.error("[DataTable] stagedRefusal threw", err);
|
|
1413
|
+
}
|
|
1414
|
+
} else if (typeof own === "string" && own) return own;
|
|
1415
|
+
return `Sorting, filtering and paging are held while ${n} change${n === 1 ? " is" : "s are"} not committed. Commit or discard them first.`;
|
|
1416
|
+
}
|
|
1417
|
+
/** Disable every control the freeze holds with the reason on it, or put
|
|
1418
|
+
* back the ones it held. Run after every render and every change. */
|
|
1419
|
+
paintFreeze() {
|
|
1420
|
+
const root = this.table._wrapperEl;
|
|
1421
|
+
const reason = this.freezeReason();
|
|
1422
|
+
root?.classList.toggle("twm-dt--frozen", !!reason);
|
|
1423
|
+
if (!reason) {
|
|
1424
|
+
for (const [el, saved] of this._frozen) this._thaw(el, saved);
|
|
1425
|
+
this._frozen.clear();
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
for (const el of this._frozen.keys()) if (!el.isConnected) this._frozen.delete(el);
|
|
1429
|
+
if (!root) return;
|
|
1430
|
+
for (const el of root.querySelectorAll(FROZEN_SELECTORS)) {
|
|
1431
|
+
if (!this._frozen.has(el)) {
|
|
1432
|
+
const saved = {};
|
|
1433
|
+
for (const a of SAVED_ATTRS) saved[a] = el.getAttribute(a);
|
|
1434
|
+
saved.disabled = "disabled" in el ? el.disabled : null;
|
|
1435
|
+
this._frozen.set(el, saved);
|
|
1436
|
+
}
|
|
1437
|
+
el.setAttribute("data-refusal", reason);
|
|
1438
|
+
el.setAttribute("title", reason);
|
|
1439
|
+
if (el.hasAttribute("data-tooltip")) el.setAttribute("data-tooltip", reason);
|
|
1440
|
+
el.setAttribute("aria-disabled", "true");
|
|
1441
|
+
if ("disabled" in el) el.disabled = true;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
_thaw(el, saved) {
|
|
1445
|
+
for (const a of SAVED_ATTRS) {
|
|
1446
|
+
if (saved[a] == null) el.removeAttribute(a);
|
|
1447
|
+
else el.setAttribute(a, saved[a]);
|
|
1448
|
+
}
|
|
1449
|
+
if (saved.disabled !== null) el.disabled = saved.disabled;
|
|
1450
|
+
}
|
|
1451
|
+
/** After every change: the freeze, and whoever listens. */
|
|
1452
|
+
_changed() {
|
|
1453
|
+
this.paintFreeze();
|
|
1454
|
+
const fn = this.table.config.onStagedChange;
|
|
1455
|
+
if (typeof fn !== "function") return;
|
|
1456
|
+
try {
|
|
1457
|
+
fn({
|
|
1458
|
+
count: this.cells.size,
|
|
1459
|
+
canUndo: this.history.canUndo,
|
|
1460
|
+
canRedo: this.history.canRedo,
|
|
1461
|
+
committing: !!this.committing
|
|
1462
|
+
});
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
console.error("[DataTable] onStagedChange threw", err);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
// src/ui/components/fill_handle.js
|
|
1470
|
+
var key2 = (rowIdx, colIdx) => `${rowIdx}:${colIdx}`;
|
|
1471
|
+
var FillHandle = class {
|
|
1472
|
+
/** @param {import('./cell_grid.js').CellGrid} grid */
|
|
1473
|
+
constructor(grid) {
|
|
1474
|
+
this.grid = grid;
|
|
1475
|
+
this.el = null;
|
|
1476
|
+
this.corner = null;
|
|
1477
|
+
this.skipped = /* @__PURE__ */ new Set();
|
|
1478
|
+
this.preview = /* @__PURE__ */ new Set();
|
|
1479
|
+
this.drag = null;
|
|
1480
|
+
this._unguard = null;
|
|
1481
|
+
}
|
|
1482
|
+
// ── painting (called by the grid for every cell it paints) ───────────
|
|
1483
|
+
/** One cell: its preview and skipped marks, and the handle when it is
|
|
1484
|
+
* the range's corner. */
|
|
1485
|
+
paintCell(td, pos, col) {
|
|
1486
|
+
const rowIdx = td.parentNode?.__rowIndex;
|
|
1487
|
+
td.classList.toggle("twm-dt-cell--fill-preview", this.preview.has(`${pos}:${col}`));
|
|
1488
|
+
td.classList.toggle("twm-dt-cell--fill-skipped", this.skipped.has(key2(rowIdx, col)));
|
|
1489
|
+
const r = this.grid.sel.range();
|
|
1490
|
+
if (r && r.bottom === pos && r.right === col) this._place(td);
|
|
1491
|
+
else if (td === this.corner) this._unplace();
|
|
1492
|
+
}
|
|
1493
|
+
_handle() {
|
|
1494
|
+
if (this.el) return this.el;
|
|
1495
|
+
const el = document.createElement("div");
|
|
1496
|
+
el.className = "twm-dt-fill-handle";
|
|
1497
|
+
el.setAttribute("aria-hidden", "true");
|
|
1498
|
+
el.title = "Drag to fill; hold Ctrl to repeat instead of continuing a series";
|
|
1499
|
+
el.addEventListener("pointerdown", (ev) => this._begin(ev));
|
|
1500
|
+
this.el = el;
|
|
1501
|
+
return el;
|
|
1502
|
+
}
|
|
1503
|
+
_place(td) {
|
|
1504
|
+
if (this.grid.isEditing() || this.grid.staged.committing) {
|
|
1505
|
+
this._unplace();
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
if (this.corner && this.corner !== td) this.corner.classList.remove("twm-dt-cell--fill-corner");
|
|
1509
|
+
this.corner = td;
|
|
1510
|
+
td.classList.add("twm-dt-cell--fill-corner");
|
|
1511
|
+
const el = this._handle();
|
|
1512
|
+
if (el.parentNode !== td) td.appendChild(el);
|
|
1513
|
+
}
|
|
1514
|
+
_unplace() {
|
|
1515
|
+
this.corner?.classList.remove("twm-dt-cell--fill-corner");
|
|
1516
|
+
this.corner = null;
|
|
1517
|
+
this.el?.remove();
|
|
1518
|
+
}
|
|
1519
|
+
// ── what a fill covers ───────────────────────────────────────────────
|
|
1520
|
+
/**
|
|
1521
|
+
* The fill that reaching cell `to` (`{row, col}`, position and data
|
|
1522
|
+
* column) from `range` makes: `{direction, count}`, or null when `to` is
|
|
1523
|
+
* inside the range's rows and columns. The direction is the one the cell
|
|
1524
|
+
* lies furthest out in; up and down win a tie. Clamped to the page drawn.
|
|
1525
|
+
*/
|
|
1526
|
+
plan(range, to) {
|
|
1527
|
+
if (!range || !to) return null;
|
|
1528
|
+
const cols = this.grid.columns();
|
|
1529
|
+
const [first, last] = this.grid.span();
|
|
1530
|
+
const row = Math.max(first, Math.min(last, Number(to.row)));
|
|
1531
|
+
const ordL = cols.indexOf(range.left);
|
|
1532
|
+
const ordR = cols.indexOf(range.right);
|
|
1533
|
+
const ordTo = cols.indexOf(to.col);
|
|
1534
|
+
const out = [
|
|
1535
|
+
["down", row - range.bottom],
|
|
1536
|
+
["up", range.top - row],
|
|
1537
|
+
["right", ordTo < 0 ? 0 : ordTo - ordR],
|
|
1538
|
+
["left", ordTo < 0 ? 0 : ordL - ordTo]
|
|
1539
|
+
];
|
|
1540
|
+
let best = null;
|
|
1541
|
+
for (const [direction, count] of out) {
|
|
1542
|
+
if (count > 0 && (!best || count > best.count)) best = { direction, count };
|
|
1543
|
+
}
|
|
1544
|
+
return best;
|
|
1545
|
+
}
|
|
1546
|
+
/** The cells a plan covers, as `{pos, col}`, line by line in fill order:
|
|
1547
|
+
* `[{seed: [{pos, col}], targets: [{pos, col}]}]`. */
|
|
1548
|
+
_lines(range, { direction, count }) {
|
|
1549
|
+
const cols = this.grid.columns();
|
|
1550
|
+
const ordL = cols.indexOf(range.left);
|
|
1551
|
+
const ordR = cols.indexOf(range.right);
|
|
1552
|
+
const lines = [];
|
|
1553
|
+
if (direction === "down" || direction === "up") {
|
|
1554
|
+
const down = direction === "down";
|
|
1555
|
+
for (let o = ordL; o <= ordR; o++) {
|
|
1556
|
+
const col = cols[o];
|
|
1557
|
+
const seed = [];
|
|
1558
|
+
for (let p = range.top; p <= range.bottom; p++) seed.push({ pos: p, col });
|
|
1559
|
+
if (!down) seed.reverse();
|
|
1560
|
+
const targets = [];
|
|
1561
|
+
for (let i = 1; i <= count; i++) {
|
|
1562
|
+
targets.push({ pos: down ? range.bottom + i : range.top - i, col });
|
|
1563
|
+
}
|
|
1564
|
+
lines.push({ seed, targets });
|
|
1565
|
+
}
|
|
1566
|
+
} else {
|
|
1567
|
+
const right = direction === "right";
|
|
1568
|
+
for (let p = range.top; p <= range.bottom; p++) {
|
|
1569
|
+
const seed = [];
|
|
1570
|
+
for (let o = ordL; o <= ordR; o++) seed.push({ pos: p, col: cols[o] });
|
|
1571
|
+
if (!right) seed.reverse();
|
|
1572
|
+
const targets = [];
|
|
1573
|
+
for (let i = 1; i <= count; i++) {
|
|
1574
|
+
const o = right ? ordR + i : ordL - i;
|
|
1575
|
+
if (o >= 0 && o < cols.length) targets.push({ pos: p, col: cols[o] });
|
|
1576
|
+
}
|
|
1577
|
+
lines.push({ seed, targets });
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return lines;
|
|
1581
|
+
}
|
|
1582
|
+
// ── the fill ─────────────────────────────────────────────────────────
|
|
1583
|
+
/**
|
|
1584
|
+
* Fill from the range to cell `to`, staging the values as ONE undo
|
|
1585
|
+
* entry. Returns `{direction, count, staged, skipped}` (`skipped` the
|
|
1586
|
+
* cells `editable` refused, `{row, col, rowIdx}`), or null when nothing
|
|
1587
|
+
* was filled: no range, `to` inside it, an editor open or a commit out.
|
|
1588
|
+
*/
|
|
1589
|
+
fill(to, { forceCopy = false } = {}) {
|
|
1590
|
+
const g = this.grid;
|
|
1591
|
+
g._sync();
|
|
1592
|
+
const range = g.sel.range();
|
|
1593
|
+
const plan = this.plan(range, to);
|
|
1594
|
+
if (!plan || g.staged.committing || g.isEditing()) return null;
|
|
1595
|
+
const cells = [];
|
|
1596
|
+
const skipped = [];
|
|
1597
|
+
for (const { seed, targets } of this._lines(range, plan)) {
|
|
1598
|
+
const values = projectSeries(
|
|
1599
|
+
seed.map(({ pos, col }) => g.shownValue(g.rowIndexAt(pos), col)),
|
|
1600
|
+
targets.length,
|
|
1601
|
+
{ forceCopy }
|
|
1602
|
+
);
|
|
1603
|
+
targets.forEach(({ pos, col }, i) => {
|
|
1604
|
+
const rowIdx = g.rowIndexAt(pos);
|
|
1605
|
+
if (g.canEdit(pos, col)) cells.push({ rowIdx, colIdx: col, value: values[i] });
|
|
1606
|
+
else skipped.push({ row: pos, col, rowIdx });
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
const staged = g.staged.stageMany(cells, `fill ${cells.length} cell(s)`);
|
|
1610
|
+
const cols = g.columns();
|
|
1611
|
+
const ordL = cols.indexOf(range.left);
|
|
1612
|
+
const ordR = cols.indexOf(range.right);
|
|
1613
|
+
const { direction, count } = plan;
|
|
1614
|
+
if (direction === "down") g.set({ row: range.bottom + count, col: range.right }, { row: range.top, col: range.left });
|
|
1615
|
+
else if (direction === "up") g.set({ row: range.top - count, col: range.left }, { row: range.bottom, col: range.right });
|
|
1616
|
+
else if (direction === "right") g.set({ row: range.bottom, col: cols[Math.min(cols.length - 1, ordR + count)] }, { row: range.top, col: range.left });
|
|
1617
|
+
else g.set({ row: range.top, col: cols[Math.max(0, ordL - count)] }, { row: range.bottom, col: range.right });
|
|
1618
|
+
for (const s of skipped) this.skipped.add(key2(s.rowIdx, s.col));
|
|
1619
|
+
if (skipped.length) g.paint();
|
|
1620
|
+
return { direction, count, staged, skipped };
|
|
1621
|
+
}
|
|
1622
|
+
/** The selection moved, or new rows came: the skipped marks are over. */
|
|
1623
|
+
clearMarks() {
|
|
1624
|
+
this.skipped.clear();
|
|
1625
|
+
}
|
|
1626
|
+
// ── the drag ─────────────────────────────────────────────────────────
|
|
1627
|
+
_begin(ev) {
|
|
1628
|
+
if (ev.button != null && ev.button !== 0) return;
|
|
1629
|
+
ev.preventDefault();
|
|
1630
|
+
ev.stopPropagation();
|
|
1631
|
+
const g = this.grid;
|
|
1632
|
+
g._sync();
|
|
1633
|
+
const range = g.sel.range();
|
|
1634
|
+
if (!range || this.drag || g.isEditing() || g.staged.committing) return;
|
|
1635
|
+
const doc = this.el.ownerDocument;
|
|
1636
|
+
this._guardClick(doc);
|
|
1637
|
+
const onMove = (move) => this._move(move);
|
|
1638
|
+
const onUp = (up) => {
|
|
1639
|
+
const plan = this.drag?.plan;
|
|
1640
|
+
this._end();
|
|
1641
|
+
if (plan) this.fill(this._reach(range, plan), { forceCopy: !!(up.ctrlKey || up.metaKey) });
|
|
1642
|
+
try {
|
|
1643
|
+
g.table._tableEl?.focus({ preventScroll: true });
|
|
1644
|
+
} catch (_) {
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
const onCancel = () => this._end();
|
|
1648
|
+
const onKey = (k) => {
|
|
1649
|
+
if (k.key === "Escape") {
|
|
1650
|
+
k.preventDefault();
|
|
1651
|
+
k.stopPropagation();
|
|
1652
|
+
this._end();
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
doc.addEventListener("pointermove", onMove);
|
|
1656
|
+
doc.addEventListener("pointerup", onUp);
|
|
1657
|
+
doc.addEventListener("pointercancel", onCancel);
|
|
1658
|
+
doc.addEventListener("keydown", onKey, true);
|
|
1659
|
+
this.drag = {
|
|
1660
|
+
range,
|
|
1661
|
+
plan: null,
|
|
1662
|
+
unbind: () => {
|
|
1663
|
+
doc.removeEventListener("pointermove", onMove);
|
|
1664
|
+
doc.removeEventListener("pointerup", onUp);
|
|
1665
|
+
doc.removeEventListener("pointercancel", onCancel);
|
|
1666
|
+
doc.removeEventListener("keydown", onKey, true);
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* A press on the handle and its release make, in a browser, a click (and
|
|
1672
|
+
* on a second press a dblclick) of their own, on the element under both:
|
|
1673
|
+
* the corner cell, the row, the body. Filled, cancelled by Escape or
|
|
1674
|
+
* released where it began, a drag is no click on the table: from the
|
|
1675
|
+
* press, the next click in the document is swallowed in the capture
|
|
1676
|
+
* phase, and so is every dblclick until the task the release is handled
|
|
1677
|
+
* in is over. A cancelled pointer makes no click; the guard ends with it.
|
|
1678
|
+
*/
|
|
1679
|
+
_guardClick(doc) {
|
|
1680
|
+
this._unguard?.();
|
|
1681
|
+
const view = doc.defaultView;
|
|
1682
|
+
let timer = null;
|
|
1683
|
+
const swallow = (c) => {
|
|
1684
|
+
c.stopPropagation();
|
|
1685
|
+
c.preventDefault();
|
|
1686
|
+
};
|
|
1687
|
+
const swallowOnce = (c) => {
|
|
1688
|
+
swallow(c);
|
|
1689
|
+
doc.removeEventListener("click", swallowOnce, true);
|
|
1690
|
+
};
|
|
1691
|
+
const released = () => {
|
|
1692
|
+
doc.removeEventListener("pointerup", released, true);
|
|
1693
|
+
doc.removeEventListener("pointercancel", unguard, true);
|
|
1694
|
+
timer = view.setTimeout(unguard, 0);
|
|
1695
|
+
};
|
|
1696
|
+
const unguard = () => {
|
|
1697
|
+
if (timer !== null) view.clearTimeout(timer);
|
|
1698
|
+
doc.removeEventListener("click", swallowOnce, true);
|
|
1699
|
+
doc.removeEventListener("dblclick", swallow, true);
|
|
1700
|
+
doc.removeEventListener("pointerup", released, true);
|
|
1701
|
+
doc.removeEventListener("pointercancel", unguard, true);
|
|
1702
|
+
if (this._unguard === unguard) this._unguard = null;
|
|
1703
|
+
};
|
|
1704
|
+
doc.addEventListener("click", swallowOnce, true);
|
|
1705
|
+
doc.addEventListener("dblclick", swallow, true);
|
|
1706
|
+
doc.addEventListener("pointerup", released, true);
|
|
1707
|
+
doc.addEventListener("pointercancel", unguard, true);
|
|
1708
|
+
this._unguard = unguard;
|
|
1709
|
+
}
|
|
1710
|
+
/** The cell a plan reaches, as `fill` takes it. */
|
|
1711
|
+
_reach(range, { direction, count }) {
|
|
1712
|
+
const cols = this.grid.columns();
|
|
1713
|
+
if (direction === "down") return { row: range.bottom + count, col: range.right };
|
|
1714
|
+
if (direction === "up") return { row: range.top - count, col: range.left };
|
|
1715
|
+
if (direction === "right") return { row: range.bottom, col: cols[cols.indexOf(range.right) + count] };
|
|
1716
|
+
return { row: range.top, col: cols[cols.indexOf(range.left) - count] };
|
|
1717
|
+
}
|
|
1718
|
+
/** The data cell under the pointer: the event's target, or (a touch,
|
|
1719
|
+
* whose events stay on the handle) the element at its point. */
|
|
1720
|
+
_cellUnder(ev) {
|
|
1721
|
+
const tbody = this.grid.table._tbodyEl;
|
|
1722
|
+
const hit = (el) => {
|
|
1723
|
+
const td = el?.closest?.("td");
|
|
1724
|
+
if (!td || td.__colIdx === void 0 || !tbody?.contains(td)) return null;
|
|
1725
|
+
const pos = td.parentNode?.__cellPos;
|
|
1726
|
+
return pos === void 0 ? null : { row: pos, col: td.__colIdx };
|
|
1727
|
+
};
|
|
1728
|
+
let cell = ev.target === this.el ? null : hit(ev.target);
|
|
1729
|
+
const doc = this.el.ownerDocument;
|
|
1730
|
+
if (!cell && typeof doc.elementFromPoint === "function") {
|
|
1731
|
+
cell = hit(doc.elementFromPoint(ev.clientX, ev.clientY));
|
|
1732
|
+
}
|
|
1733
|
+
return cell;
|
|
1734
|
+
}
|
|
1735
|
+
_move(ev) {
|
|
1736
|
+
const d = this.drag;
|
|
1737
|
+
if (!d) return;
|
|
1738
|
+
const cell = this._cellUnder(ev);
|
|
1739
|
+
if (!cell) return;
|
|
1740
|
+
d.plan = this.plan(d.range, cell);
|
|
1741
|
+
this.preview.clear();
|
|
1742
|
+
if (d.plan) {
|
|
1743
|
+
for (const { targets } of this._lines(d.range, d.plan)) {
|
|
1744
|
+
for (const t of targets) this.preview.add(`${t.pos}:${t.col}`);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
this.grid.paint();
|
|
1748
|
+
}
|
|
1749
|
+
_end() {
|
|
1750
|
+
const d = this.drag;
|
|
1751
|
+
if (!d) return;
|
|
1752
|
+
this.drag = null;
|
|
1753
|
+
d.unbind();
|
|
1754
|
+
this.preview.clear();
|
|
1755
|
+
this.grid.paint();
|
|
1756
|
+
}
|
|
1757
|
+
dispose() {
|
|
1758
|
+
this._end();
|
|
1759
|
+
this._unguard?.();
|
|
1760
|
+
this._unplace();
|
|
1761
|
+
this.skipped.clear();
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
// src/ui/components/cell_grid.js
|
|
1766
|
+
function isFilledValue(value) {
|
|
1767
|
+
if (value == null) return false;
|
|
1768
|
+
if (typeof value === "string") return value !== "";
|
|
1769
|
+
if (typeof value === "number") return !Number.isNaN(value);
|
|
1770
|
+
return true;
|
|
1771
|
+
}
|
|
1772
|
+
function isPrintable(ev) {
|
|
1773
|
+
return typeof ev.key === "string" && [...ev.key].length === 1 && !ev.ctrlKey && !ev.metaKey && !ev.altKey;
|
|
1774
|
+
}
|
|
1775
|
+
var ARROWS = {
|
|
1776
|
+
ArrowUp: [-1, 0],
|
|
1777
|
+
ArrowDown: [1, 0],
|
|
1778
|
+
ArrowLeft: [0, -1],
|
|
1779
|
+
ArrowRight: [0, 1]
|
|
1780
|
+
};
|
|
1781
|
+
var CellGrid = class {
|
|
1782
|
+
/** @param {import('./data_table.js').DataTable} table */
|
|
1783
|
+
constructor(table) {
|
|
1784
|
+
this.table = table;
|
|
1785
|
+
this.sel = new CellSelection();
|
|
1786
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
1787
|
+
this.errors = /* @__PURE__ */ new Map();
|
|
1788
|
+
this.renderWaiting = false;
|
|
1789
|
+
this.rowsWaiting = /* @__PURE__ */ new Set();
|
|
1790
|
+
this.latest = /* @__PURE__ */ new Map();
|
|
1791
|
+
this.generation = 0;
|
|
1792
|
+
this.editor = new CellEditorHost({
|
|
1793
|
+
onCommit: (s, value, move, reason) => this._committed(s, value, move, reason),
|
|
1794
|
+
onCancel: (s, reason) => this._closed(s, reason)
|
|
1795
|
+
});
|
|
1796
|
+
this.staged = null;
|
|
1797
|
+
if (this._cfg().staged) {
|
|
1798
|
+
if (this._cfg().editable) this.staged = new StagedEdits(this);
|
|
1799
|
+
else console.warn("[DataTable] `staged` needs `editable`; ignored.");
|
|
1800
|
+
}
|
|
1801
|
+
this.fill = null;
|
|
1802
|
+
if (this._cfg().fillHandle) {
|
|
1803
|
+
if (this.staged) this.fill = new FillHandle(this);
|
|
1804
|
+
else console.warn("[DataTable] `fillHandle` needs `staged`; ignored.");
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
// ── what the table is, read through its own helpers ──────────────────
|
|
1808
|
+
_cfg() {
|
|
1809
|
+
return this.table.config;
|
|
1810
|
+
}
|
|
1811
|
+
/** The data columns shown, in the order shown. */
|
|
1812
|
+
columns() {
|
|
1813
|
+
return this.table._shownColumns();
|
|
1814
|
+
}
|
|
1815
|
+
/** The positions of the rows on the page drawn, `[first, last]`. */
|
|
1816
|
+
span() {
|
|
1817
|
+
const s = this.table._pageSpan || { start: 0, count: 0 };
|
|
1818
|
+
return [s.start, s.start + s.count - 1];
|
|
1819
|
+
}
|
|
1820
|
+
_sync() {
|
|
1821
|
+
const [first, last] = this.span();
|
|
1822
|
+
this.sel.setBounds(first, last, this.columns());
|
|
1823
|
+
}
|
|
1824
|
+
/** A position's original row index (the `__rowIndex` space). */
|
|
1825
|
+
rowIndexAt(pos) {
|
|
1826
|
+
const t = this.table;
|
|
1827
|
+
if (t._isServerSide()) return (t.config.dataSource ? 0 : t.config.offset || 0) + pos;
|
|
1828
|
+
t._getProcessedRows();
|
|
1829
|
+
return t._processedIndexMap?.[pos] ?? pos;
|
|
1830
|
+
}
|
|
1831
|
+
rowAt(pos) {
|
|
1832
|
+
return this.table._getProcessedRows()[pos] ?? null;
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* The row the table holds NOW for an original row index — the window's
|
|
1836
|
+
* row list, else `config.rows` at the place the table draws it from —
|
|
1837
|
+
* the one `updateRow` writes. Never a row captured earlier.
|
|
1838
|
+
*/
|
|
1839
|
+
currentRow(rowIdx) {
|
|
1840
|
+
const t = this.table;
|
|
1841
|
+
const local = t._virtLocalOf(rowIdx);
|
|
1842
|
+
if (local >= 0) return t._virt.rows[local] ?? null;
|
|
1843
|
+
const rows = t.config.rows;
|
|
1844
|
+
if (!Array.isArray(rows)) return null;
|
|
1845
|
+
const at = t._isServerSide() ? rowIdx - (t.config.dataSource ? 0 : t.config.offset || 0) : rowIdx;
|
|
1846
|
+
return rows[at] ?? null;
|
|
1847
|
+
}
|
|
1848
|
+
/** What a cell shows: a refused or pending commit's value, else the row's. */
|
|
1849
|
+
shownValue(rowIdx, col) {
|
|
1850
|
+
const key3 = `${rowIdx}:${col}`;
|
|
1851
|
+
const err = this.errors.get(key3);
|
|
1852
|
+
if (err) return err.value;
|
|
1853
|
+
if (this.pending.has(key3)) return this.pending.get(key3);
|
|
1854
|
+
if (this.staged?.has(key3)) return this.staged.value(key3);
|
|
1855
|
+
const row = this.currentRow(rowIdx);
|
|
1856
|
+
return row ? row[col] : void 0;
|
|
1857
|
+
}
|
|
1858
|
+
/** Whether the open editor sits in this row element. */
|
|
1859
|
+
_holds(tr) {
|
|
1860
|
+
const s = this.editor.session;
|
|
1861
|
+
return !!s && !!tr && tr.contains(s.td);
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* `updateRow` on a drawn row: the table has stored the data already. When
|
|
1865
|
+
* the row holds the open editor the rebuild — which would take the editor
|
|
1866
|
+
* out of the document — waits for that editor to close, the editor's cell
|
|
1867
|
+
* left as it is. Returns whether it waits.
|
|
1868
|
+
*/
|
|
1869
|
+
holdRow(tr, rowIdx) {
|
|
1870
|
+
if (!this._holds(tr)) return false;
|
|
1871
|
+
this.rowsWaiting.add(rowIdx);
|
|
1872
|
+
return true;
|
|
1873
|
+
}
|
|
1874
|
+
valueAt(pos, col) {
|
|
1875
|
+
const row = this.rowAt(pos);
|
|
1876
|
+
return row ? row[col] : void 0;
|
|
1877
|
+
}
|
|
1878
|
+
/** The `<td>` drawing a cell, or null when it is not drawn. */
|
|
1879
|
+
cellEl(pos, col) {
|
|
1880
|
+
const tr = this.table.getRowElement(this.rowIndexAt(pos));
|
|
1881
|
+
if (!tr) return null;
|
|
1882
|
+
for (const td of tr.children) if (td.__colIdx === col) return td;
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
// ── the public face (DataTable delegates) ────────────────────────────
|
|
1886
|
+
/** `{anchor, focus, range}`; each cell is `{row, col, rowIdx}`, and the
|
|
1887
|
+
* range also lists its `rows` (original indices) and `columns`. */
|
|
1888
|
+
get() {
|
|
1889
|
+
this._sync();
|
|
1890
|
+
const cell = (c) => c ? { row: c.row, col: c.col, rowIdx: this.rowIndexAt(c.row) } : null;
|
|
1891
|
+
const r = this.sel.range();
|
|
1892
|
+
let range = null;
|
|
1893
|
+
if (r) {
|
|
1894
|
+
const rows = [];
|
|
1895
|
+
for (let p = r.top; p <= r.bottom; p++) rows.push(this.rowIndexAt(p));
|
|
1896
|
+
range = { ...r, rows };
|
|
1897
|
+
}
|
|
1898
|
+
return { anchor: cell(this.sel.anchor), focus: cell(this.sel.focus), range };
|
|
1899
|
+
}
|
|
1900
|
+
/** Rows (original indices) the range touches, ascending. */
|
|
1901
|
+
rowIndices() {
|
|
1902
|
+
const r = this.sel.range();
|
|
1903
|
+
if (!r) return [];
|
|
1904
|
+
const out = [];
|
|
1905
|
+
for (let p = r.top; p <= r.bottom; p++) out.push(this.rowIndexAt(p));
|
|
1906
|
+
return out.sort((a, b) => a - b);
|
|
1907
|
+
}
|
|
1908
|
+
/** Select from `anchor` to `focus` (`{row, col}`, positions and data
|
|
1909
|
+
* indices), or put the cursor on `focus` alone. */
|
|
1910
|
+
set(focus, anchor = null) {
|
|
1911
|
+
this._sync();
|
|
1912
|
+
if (!focus) {
|
|
1913
|
+
this.sel.clear();
|
|
1914
|
+
this._changed();
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
this.sel.setCursor(anchor ? anchor.row : focus.row, anchor ? anchor.col : focus.col);
|
|
1918
|
+
if (anchor) this.sel.extendTo(focus.row, focus.col);
|
|
1919
|
+
this._changed({ reveal: true });
|
|
1920
|
+
}
|
|
1921
|
+
/** `setSelection(indices)` in cell mode: whole rows, the first to the last
|
|
1922
|
+
* of them as shown. */
|
|
1923
|
+
selectRows(indices) {
|
|
1924
|
+
this._sync();
|
|
1925
|
+
const t = this.table;
|
|
1926
|
+
t._getProcessedRows();
|
|
1927
|
+
const map = t._processedIndexMap || [];
|
|
1928
|
+
const positions = indices.map((i) => t._isServerSide() ? i - (t.config.dataSource ? 0 : t.config.offset || 0) : map.indexOf(i)).filter((p) => p >= 0);
|
|
1929
|
+
const cols = this.sel.cols;
|
|
1930
|
+
if (!positions.length || !cols.length) {
|
|
1931
|
+
this.sel.clear();
|
|
1932
|
+
this._changed();
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
this.sel.setCursor(Math.min(...positions), cols[0]);
|
|
1936
|
+
this.sel.extendTo(Math.max(...positions), cols[cols.length - 1]);
|
|
1937
|
+
this._changed();
|
|
1938
|
+
}
|
|
1939
|
+
clear() {
|
|
1940
|
+
this.sel.clear();
|
|
1941
|
+
this._changed();
|
|
1942
|
+
}
|
|
1943
|
+
/** New rows (`setData({rows})`): the marks were about the old ones. */
|
|
1944
|
+
newRows() {
|
|
1945
|
+
this.generation++;
|
|
1946
|
+
this.pending.clear();
|
|
1947
|
+
this.errors.clear();
|
|
1948
|
+
this.latest.clear();
|
|
1949
|
+
this.rowsWaiting.clear();
|
|
1950
|
+
this.staged?.clear();
|
|
1951
|
+
this.fill?.clearMarks();
|
|
1952
|
+
}
|
|
1953
|
+
isEditing() {
|
|
1954
|
+
return !!this.editor.session;
|
|
1955
|
+
}
|
|
1956
|
+
// ── painting ─────────────────────────────────────────────────────────
|
|
1957
|
+
/** Called for every row drawn — on render, on a scroll, on `updateRow` —
|
|
1958
|
+
* so a row that arrives later is painted like the rest. */
|
|
1959
|
+
decorateRow(tr) {
|
|
1960
|
+
const pos = tr.__cellPos;
|
|
1961
|
+
if (pos === void 0) return;
|
|
1962
|
+
for (const td of tr.children) {
|
|
1963
|
+
if (td.__colIdx !== void 0) this._decorateCell(td, pos, tr.__rowIndex, td.__colIdx);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
/** One cell: the selection, its commit's pending or refused mark, and
|
|
1967
|
+
* (STORY-0088) its staged value and a refused batch's mark. */
|
|
1968
|
+
_decorateCell(td, pos, rowIdx, col) {
|
|
1969
|
+
this._paintCell(td, pos, col);
|
|
1970
|
+
const key3 = `${rowIdx}:${col}`;
|
|
1971
|
+
const err = this.errors.get(key3);
|
|
1972
|
+
const waiting = this.pending.has(key3);
|
|
1973
|
+
const staged = !!this.staged?.has(key3);
|
|
1974
|
+
if (err || waiting) {
|
|
1975
|
+
td.textContent = this.table._formatValue(err ? err.value : this.pending.get(key3), col);
|
|
1976
|
+
} else if (staged) {
|
|
1977
|
+
td.textContent = this.table._formatValue(this.staged.value(key3), col);
|
|
1978
|
+
}
|
|
1979
|
+
const message = err ? err.message : this.staged?.errors.get(key3);
|
|
1980
|
+
td.classList.toggle("twm-dt-cell--pending", waiting);
|
|
1981
|
+
td.classList.toggle("twm-dt-cell--staged", staged);
|
|
1982
|
+
td.classList.toggle("twm-dt-cell--error", message != null);
|
|
1983
|
+
if (message != null) {
|
|
1984
|
+
td.title = message;
|
|
1985
|
+
td.setAttribute("data-error", message);
|
|
1986
|
+
} else td.removeAttribute("data-error");
|
|
1987
|
+
this.fill?.paintCell(td, pos, col);
|
|
1988
|
+
}
|
|
1989
|
+
_paintCell(td, pos, col) {
|
|
1990
|
+
td.classList.toggle("twm-dt-cell--selected", this.sel.contains(pos, col));
|
|
1991
|
+
const cursor = this.sel.isCursor(pos, col);
|
|
1992
|
+
td.classList.toggle("twm-dt-cell--cursor", cursor);
|
|
1993
|
+
if (cursor) td.setAttribute("aria-selected", "true");
|
|
1994
|
+
else td.removeAttribute("aria-selected");
|
|
1995
|
+
this.fill?.paintCell(td, pos, col);
|
|
1996
|
+
}
|
|
1997
|
+
paint() {
|
|
1998
|
+
const tbody = this.table._tbodyEl;
|
|
1999
|
+
if (!tbody) return;
|
|
2000
|
+
for (const tr of tbody.children) {
|
|
2001
|
+
const pos = tr.__cellPos;
|
|
2002
|
+
if (pos === void 0) continue;
|
|
2003
|
+
for (const td of tr.children) {
|
|
2004
|
+
if (td.__colIdx !== void 0) this._paintCell(td, pos, td.__colIdx);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/** The selection moved: the cells, the rows `getSelection()` answers, and
|
|
2009
|
+
* whoever listens. */
|
|
2010
|
+
_changed({ reveal = false } = {}) {
|
|
2011
|
+
const t = this.table;
|
|
2012
|
+
this.fill?.clearMarks();
|
|
2013
|
+
if (reveal && this.sel.focus) t.scrollToRow(this.rowIndexAt(this.sel.focus.row));
|
|
2014
|
+
this.paint();
|
|
2015
|
+
const before = t.getSelection().join();
|
|
2016
|
+
const rows = this.rowIndices();
|
|
2017
|
+
t._state.selected.clear();
|
|
2018
|
+
for (const i of rows) t._state.selected.add(i);
|
|
2019
|
+
t._state.anchorIndex = this.sel.anchor ? this.rowIndexAt(this.sel.anchor.row) : null;
|
|
2020
|
+
if (rows.join() !== before) t._notifySelectionChange();
|
|
2021
|
+
try {
|
|
2022
|
+
t.config.onCellSelectionChange?.(this.get());
|
|
2023
|
+
} catch (err) {
|
|
2024
|
+
console.error("[DataTable] onCellSelectionChange threw", err);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
// ── the pointer and the keys ─────────────────────────────────────────
|
|
2028
|
+
/** Bound by `_installInteractions` on every render, BEFORE the table's own
|
|
2029
|
+
* activation, detail and copy listeners, so a key the cell mode takes
|
|
2030
|
+
* goes no further. Listeners go on the table's disposers. */
|
|
2031
|
+
install(tbody, table) {
|
|
2032
|
+
const t = this.table;
|
|
2033
|
+
const on = (el, type, fn) => {
|
|
2034
|
+
el.addEventListener(type, fn);
|
|
2035
|
+
t._disposers.push(() => el.removeEventListener(type, fn));
|
|
2036
|
+
};
|
|
2037
|
+
this._sync();
|
|
2038
|
+
const cellOf = (target) => {
|
|
2039
|
+
const td = target?.closest?.("td");
|
|
2040
|
+
if (!td || td.__colIdx === void 0 || !tbody.contains(td)) return null;
|
|
2041
|
+
const tr = td.parentNode;
|
|
2042
|
+
if (tr?.__cellPos === void 0) return null;
|
|
2043
|
+
return { td, row: tr.__cellPos, col: td.__colIdx };
|
|
2044
|
+
};
|
|
2045
|
+
const inEditor = (target) => !!this.editor.session && this.editor.session.td.contains(target);
|
|
2046
|
+
on(tbody, "click", (ev) => {
|
|
2047
|
+
if (ev.button != null && ev.button !== 0) return;
|
|
2048
|
+
if (inEditor(ev.target)) return;
|
|
2049
|
+
const hit = cellOf(ev.target);
|
|
2050
|
+
if (!hit) return;
|
|
2051
|
+
this._sync();
|
|
2052
|
+
if (ev.shiftKey && this.sel.anchor) this.sel.extendTo(hit.row, hit.col);
|
|
2053
|
+
else this.sel.setCursor(hit.row, hit.col);
|
|
2054
|
+
this._changed();
|
|
2055
|
+
try {
|
|
2056
|
+
table.focus({ preventScroll: true });
|
|
2057
|
+
} catch (_) {
|
|
2058
|
+
}
|
|
2059
|
+
});
|
|
2060
|
+
on(tbody, "dblclick", (ev) => {
|
|
2061
|
+
if (inEditor(ev.target)) return;
|
|
2062
|
+
const hit = cellOf(ev.target);
|
|
2063
|
+
if (!hit || !this.canEdit(hit.row, hit.col)) return;
|
|
2064
|
+
this._sync();
|
|
2065
|
+
this.sel.setCursor(hit.row, hit.col);
|
|
2066
|
+
this._changed();
|
|
2067
|
+
if (this.open(hit.row, hit.col)) ev.stopImmediatePropagation();
|
|
2068
|
+
});
|
|
2069
|
+
on(tbody, "contextmenu", (ev) => {
|
|
2070
|
+
if (inEditor(ev.target)) return;
|
|
2071
|
+
const hit = cellOf(ev.target);
|
|
2072
|
+
if (!hit || this.sel.contains(hit.row, hit.col)) return;
|
|
2073
|
+
this._sync();
|
|
2074
|
+
this.sel.setCursor(hit.row, hit.col);
|
|
2075
|
+
this._changed();
|
|
2076
|
+
});
|
|
2077
|
+
on(table, "keydown", (ev) => {
|
|
2078
|
+
if (ev.isComposing || inEditor(ev.target)) return;
|
|
2079
|
+
if (ev.target !== table && ev.target?.closest?.('input, select, textarea, button, [contenteditable="true"]')) return;
|
|
2080
|
+
if (this._onKey(ev)) {
|
|
2081
|
+
ev.preventDefault();
|
|
2082
|
+
ev.stopImmediatePropagation();
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
this.paint();
|
|
2086
|
+
this.staged?.paintFreeze();
|
|
2087
|
+
}
|
|
2088
|
+
/** One key on the focused table. Returns whether the cell mode took it. */
|
|
2089
|
+
_onKey(ev) {
|
|
2090
|
+
this._sync();
|
|
2091
|
+
const ctrl = ev.ctrlKey || ev.metaKey;
|
|
2092
|
+
const k = ev.key;
|
|
2093
|
+
if (this.staged && ctrl && !ev.altKey) {
|
|
2094
|
+
const lower = String(k).toLowerCase();
|
|
2095
|
+
if (lower === "z" && !ev.shiftKey) {
|
|
2096
|
+
this.staged.undo();
|
|
2097
|
+
return true;
|
|
2098
|
+
}
|
|
2099
|
+
if (lower === "y" || lower === "z" && ev.shiftKey) {
|
|
2100
|
+
this.staged.redo();
|
|
2101
|
+
return true;
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
if (ARROWS[k] && !ev.altKey) {
|
|
2105
|
+
const [dr, dc] = ARROWS[k];
|
|
2106
|
+
if (ctrl) {
|
|
2107
|
+
this.sel.jump(dr, dc, (r, c) => isFilledValue(this.valueAt(r, c)), { extend: ev.shiftKey });
|
|
2108
|
+
} else this.sel.move(dr, dc, { extend: ev.shiftKey });
|
|
2109
|
+
this._changed({ reveal: true });
|
|
2110
|
+
return true;
|
|
2111
|
+
}
|
|
2112
|
+
if ((k === "Home" || k === "End") && !ev.altKey) {
|
|
2113
|
+
this.sel.lineEnd(k === "End", { extend: ev.shiftKey, corner: ctrl });
|
|
2114
|
+
this._changed({ reveal: true });
|
|
2115
|
+
return true;
|
|
2116
|
+
}
|
|
2117
|
+
if (k === "Tab" && !ctrl && !ev.altKey) {
|
|
2118
|
+
if (this.sel.tabEnds(ev.shiftKey)) return false;
|
|
2119
|
+
this.sel.tab(ev.shiftKey);
|
|
2120
|
+
this._changed({ reveal: true });
|
|
2121
|
+
return true;
|
|
2122
|
+
}
|
|
2123
|
+
if (ctrl && !ev.altKey && String(k).toLowerCase() === "a") {
|
|
2124
|
+
this.sel.selectAll();
|
|
2125
|
+
this._changed();
|
|
2126
|
+
return true;
|
|
2127
|
+
}
|
|
2128
|
+
if (k === "Escape") {
|
|
2129
|
+
this.sel.collapse();
|
|
2130
|
+
this._changed();
|
|
2131
|
+
this.table._hideContextMenu();
|
|
2132
|
+
return true;
|
|
2133
|
+
}
|
|
2134
|
+
const f = this.sel.focus;
|
|
2135
|
+
if (!f) return false;
|
|
2136
|
+
if ((k === "F2" || k === "Enter" && !ctrl && !ev.altKey && !ev.shiftKey) && this.canEdit(f.row, f.col)) {
|
|
2137
|
+
return this.open(f.row, f.col);
|
|
2138
|
+
}
|
|
2139
|
+
if (isPrintable(ev) && this.canEdit(f.row, f.col)) {
|
|
2140
|
+
return this.open(f.row, f.col, { initialText: ev.key });
|
|
2141
|
+
}
|
|
2142
|
+
return false;
|
|
2143
|
+
}
|
|
2144
|
+
// ── editing ──────────────────────────────────────────────────────────
|
|
2145
|
+
/** Whether a cell opens an editor: `editable` says so for its column. */
|
|
2146
|
+
canEdit(pos, col) {
|
|
2147
|
+
const spec = this._cfg().editable;
|
|
2148
|
+
if (!spec) return false;
|
|
2149
|
+
if (this.staged?.committing) return false;
|
|
2150
|
+
if (spec === true) return true;
|
|
2151
|
+
if (Array.isArray(spec)) return spec.includes(col);
|
|
2152
|
+
if (typeof spec === "function") {
|
|
2153
|
+
try {
|
|
2154
|
+
return !!spec(col, this.rowAt(pos), this.rowIndexAt(pos));
|
|
2155
|
+
} catch (err) {
|
|
2156
|
+
console.error("[DataTable] editable threw", err);
|
|
2157
|
+
return false;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return false;
|
|
2161
|
+
}
|
|
2162
|
+
/** The column's type, as `editors` is keyed by: the declared one, else the
|
|
2163
|
+
* detected one. */
|
|
2164
|
+
_typeOf(col) {
|
|
2165
|
+
const declared = this._cfg().columnTypes?.[col];
|
|
2166
|
+
return declared || this.table._columnTypes?.[col] || "text";
|
|
2167
|
+
}
|
|
2168
|
+
_factoryFor(col) {
|
|
2169
|
+
const editors = this._cfg().editors;
|
|
2170
|
+
if (editors && typeof editors === "object") {
|
|
2171
|
+
if (typeof editors[col] === "function") return editors[col];
|
|
2172
|
+
const type = this._typeOf(col);
|
|
2173
|
+
if (typeof editors[type] === "function") return editors[type];
|
|
2174
|
+
}
|
|
2175
|
+
return createTextEditor;
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Open the editor on a cell (position, data column). Brings the row into
|
|
2179
|
+
* view first. Returns whether an editor opened.
|
|
2180
|
+
*/
|
|
2181
|
+
open(pos, col, { initialText = null } = {}) {
|
|
2182
|
+
if (this.editor.session || !this.canEdit(pos, col)) return false;
|
|
2183
|
+
this.table.scrollToRow(this.rowIndexAt(pos));
|
|
2184
|
+
const td = this.cellEl(pos, col);
|
|
2185
|
+
if (!td) return false;
|
|
2186
|
+
const rowIdx = this.rowIndexAt(pos);
|
|
2187
|
+
const row = this.currentRow(rowIdx);
|
|
2188
|
+
const headers = this._cfg().headers || [];
|
|
2189
|
+
return this.editor.open({
|
|
2190
|
+
td,
|
|
2191
|
+
factory: this._factoryFor(col),
|
|
2192
|
+
pos,
|
|
2193
|
+
colIdx: col,
|
|
2194
|
+
rowIdx,
|
|
2195
|
+
row,
|
|
2196
|
+
generation: this.generation,
|
|
2197
|
+
value: this.shownValue(rowIdx, col),
|
|
2198
|
+
column: { index: col, header: headers[col], type: this._typeOf(col) },
|
|
2199
|
+
initialText
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
/** The virtual window is taking a row out: an editor in it commits, as a
|
|
2203
|
+
* click elsewhere would — what was typed is not thrown away. An editor
|
|
2204
|
+
* with no `getValue` has nothing to commit, and is cancelled. */
|
|
2205
|
+
rowLeaving(tr) {
|
|
2206
|
+
const s = this.editor.session;
|
|
2207
|
+
if (s && tr.contains(s.td)) this.editor.commit(null, "scroll");
|
|
2208
|
+
}
|
|
2209
|
+
dispose() {
|
|
2210
|
+
this.fill?.dispose();
|
|
2211
|
+
this.editor.cancel("dispose");
|
|
2212
|
+
this.renderWaiting = false;
|
|
2213
|
+
this.rowsWaiting.clear();
|
|
2214
|
+
}
|
|
2215
|
+
/** After every close: focus back on the table when the keyboard closed it,
|
|
2216
|
+
* and the render that waited for the editor. */
|
|
2217
|
+
_closed(s, reason) {
|
|
2218
|
+
const t = this.table;
|
|
2219
|
+
if (reason === "dispose") return;
|
|
2220
|
+
if (reason === "key" || reason === "editor") {
|
|
2221
|
+
try {
|
|
2222
|
+
t._tableEl?.focus({ preventScroll: true });
|
|
2223
|
+
} catch (_) {
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (this.renderWaiting) {
|
|
2227
|
+
this.renderWaiting = false;
|
|
2228
|
+
this.rowsWaiting.clear();
|
|
2229
|
+
if (reason === "scroll") queueMicrotask(() => {
|
|
2230
|
+
if (t._wrapperEl) t.render();
|
|
2231
|
+
});
|
|
2232
|
+
else t.render();
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
if (this.rowsWaiting.size) {
|
|
2236
|
+
const flush = () => {
|
|
2237
|
+
const waiting = [...this.rowsWaiting];
|
|
2238
|
+
this.rowsWaiting.clear();
|
|
2239
|
+
for (const rowIdx of waiting) this._redraw(rowIdx);
|
|
2240
|
+
};
|
|
2241
|
+
if (reason === "scroll") queueMicrotask(() => {
|
|
2242
|
+
if (t._wrapperEl) flush();
|
|
2243
|
+
});
|
|
2244
|
+
else flush();
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
/**
|
|
2248
|
+
* Draw a row again after a commit, from the row the table holds now
|
|
2249
|
+
* (`currentRow`). `updateRow` rebuilds EVERY cell of the row, so while an
|
|
2250
|
+
* editor is open in it (save, Tab, type — the commit settles under the
|
|
2251
|
+
* next editor) the row waits for that editor to close (`_closed`), and
|
|
2252
|
+
* only the committed cell's marks are painted now.
|
|
2253
|
+
*/
|
|
2254
|
+
_redraw(rowIdx, col = null) {
|
|
2255
|
+
const t = this.table;
|
|
2256
|
+
const tr = t.getRowElement(rowIdx);
|
|
2257
|
+
const row = this.currentRow(rowIdx);
|
|
2258
|
+
if (!tr || !row) return;
|
|
2259
|
+
if (this._holds(tr)) {
|
|
2260
|
+
this.rowsWaiting.add(rowIdx);
|
|
2261
|
+
if (col === null || tr.__cellPos === void 0) return;
|
|
2262
|
+
const s = this.editor.session;
|
|
2263
|
+
for (const td of tr.children) {
|
|
2264
|
+
if (td.__colIdx === col && td !== s.td) this._decorateCell(td, tr.__cellPos, rowIdx, col);
|
|
2265
|
+
}
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
this.rowsWaiting.delete(rowIdx);
|
|
2269
|
+
t.updateRow(rowIdx, row);
|
|
2270
|
+
}
|
|
2271
|
+
_committed(s, value, move, reason) {
|
|
2272
|
+
if (move) {
|
|
2273
|
+
this._sync();
|
|
2274
|
+
if (move === "down" || move === "up") this.sel.move(move === "down" ? 1 : -1, 0);
|
|
2275
|
+
else this.sel.tab(move === "left");
|
|
2276
|
+
this._changed({ reveal: true });
|
|
2277
|
+
}
|
|
2278
|
+
const live = s.generation === this.generation;
|
|
2279
|
+
const key3 = `${s.rowIdx}:${s.colIdx}`;
|
|
2280
|
+
if (Object.is(value, s.previous)) {
|
|
2281
|
+
this._closed(s, reason);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
if (this.staged) {
|
|
2285
|
+
if (live) this.staged.stage(s.rowIdx, s.colIdx, value);
|
|
2286
|
+
this._closed(s, reason);
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
const held = live ? this.currentRow(s.rowIdx) : s.row;
|
|
2290
|
+
const stored = held ? held[s.colIdx] : void 0;
|
|
2291
|
+
if (live && this.errors.has(key3) && !this.pending.has(key3) && Object.is(value, stored)) {
|
|
2292
|
+
this.errors.delete(key3);
|
|
2293
|
+
this.latest.delete(key3);
|
|
2294
|
+
this._redraw(s.rowIdx, s.colIdx);
|
|
2295
|
+
this._closed(s, reason);
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
const fn = this._cfg().onCellCommit;
|
|
2299
|
+
const token = {};
|
|
2300
|
+
if (live) this.latest.set(key3, token);
|
|
2301
|
+
const finish = (result2) => {
|
|
2302
|
+
if (!live || s.generation !== this.generation) return;
|
|
2303
|
+
const newest = this.latest.get(key3) === token;
|
|
2304
|
+
const good = !(result2 && result2.ok === false);
|
|
2305
|
+
if (good) {
|
|
2306
|
+
const row = this.currentRow(s.rowIdx);
|
|
2307
|
+
if (Array.isArray(row)) row[s.colIdx] = value;
|
|
2308
|
+
}
|
|
2309
|
+
if (newest) {
|
|
2310
|
+
this.latest.delete(key3);
|
|
2311
|
+
this.pending.delete(key3);
|
|
2312
|
+
if (good) this.errors.delete(key3);
|
|
2313
|
+
else this.errors.set(key3, { value, message: String(result2.error ?? "Not saved") });
|
|
2314
|
+
}
|
|
2315
|
+
this._redraw(s.rowIdx, s.colIdx);
|
|
2316
|
+
};
|
|
2317
|
+
const refused = (err) => ({ ok: false, error: err?.message || String(err ?? "Not saved") });
|
|
2318
|
+
let result;
|
|
2319
|
+
if (typeof fn === "function") {
|
|
2320
|
+
try {
|
|
2321
|
+
result = fn({ rowIdx: s.rowIdx, colIdx: s.colIdx, value, previous: stored });
|
|
2322
|
+
} catch (err) {
|
|
2323
|
+
result = refused(err);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
if (result && typeof result.then === "function") {
|
|
2327
|
+
if (live) {
|
|
2328
|
+
this.pending.set(key3, value);
|
|
2329
|
+
this.errors.delete(key3);
|
|
2330
|
+
this._redraw(s.rowIdx, s.colIdx);
|
|
2331
|
+
}
|
|
2332
|
+
this._closed(s, reason);
|
|
2333
|
+
result.then(finish, (err) => finish(refused(err)));
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
finish(result);
|
|
2337
|
+
this._closed(s, reason);
|
|
2338
|
+
}
|
|
2339
|
+
};
|
|
2340
|
+
|
|
2341
|
+
// src/ui/components/column_order.js
|
|
2342
|
+
function isColumnOrder(order, n) {
|
|
2343
|
+
if (!Array.isArray(order) || order.length !== n) return false;
|
|
2344
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2345
|
+
for (const c of order) {
|
|
2346
|
+
if (!Number.isInteger(c) || c < 0 || c >= n || seen.has(c)) return false;
|
|
2347
|
+
seen.add(c);
|
|
2348
|
+
}
|
|
2349
|
+
return true;
|
|
2350
|
+
}
|
|
2351
|
+
function naturalOrder(n) {
|
|
2352
|
+
return Array.from({ length: Math.max(0, n) }, (_, i) => i);
|
|
2353
|
+
}
|
|
2354
|
+
function cellOrder(order, count) {
|
|
2355
|
+
if (!order) return naturalOrder(count);
|
|
2356
|
+
const out = order.filter((c) => c < count);
|
|
2357
|
+
for (let c = order.length; c < count; c++) out.push(c);
|
|
2358
|
+
return out;
|
|
2359
|
+
}
|
|
2360
|
+
function moveInOrder(order, from, to) {
|
|
2361
|
+
const next = order.slice();
|
|
2362
|
+
const [moved] = next.splice(from, 1);
|
|
2363
|
+
next.splice(to, 0, moved);
|
|
2364
|
+
return next;
|
|
2365
|
+
}
|
|
2366
|
+
var ColumnReorder = class {
|
|
2367
|
+
/** @param {import('./data_table.js').DataTable} table */
|
|
2368
|
+
constructor(table) {
|
|
2369
|
+
this.table = table;
|
|
2370
|
+
this.dragging = null;
|
|
2371
|
+
this.onGrip = false;
|
|
2372
|
+
}
|
|
2373
|
+
/** Wire the header row of a fresh render. Its cells are new each render,
|
|
2374
|
+
* so nothing needs taking off. */
|
|
2375
|
+
install(headRow) {
|
|
2376
|
+
if (!headRow) return;
|
|
2377
|
+
[...headRow.children].forEach((th, domIdx) => {
|
|
2378
|
+
const col = this.table._dataIdxOfDom(domIdx);
|
|
2379
|
+
if (col === null) return;
|
|
2380
|
+
th.draggable = true;
|
|
2381
|
+
th.classList.add("twm-dt-col--movable");
|
|
2382
|
+
th.addEventListener("mousedown", (ev) => {
|
|
2383
|
+
this.onGrip = !!ev.target?.closest?.(".twm-dt-col-resizer");
|
|
2384
|
+
}, true);
|
|
2385
|
+
th.addEventListener("dragstart", (ev) => {
|
|
2386
|
+
if (this.onGrip || ev.target?.closest?.(".twm-dt-col-resizer")) {
|
|
2387
|
+
ev.preventDefault();
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
this.dragging = col;
|
|
2391
|
+
try {
|
|
2392
|
+
ev.dataTransfer?.setData("text/plain", String(col));
|
|
2393
|
+
if (ev.dataTransfer) ev.dataTransfer.effectAllowed = "move";
|
|
2394
|
+
} catch (_) {
|
|
2395
|
+
}
|
|
2396
|
+
th.classList.add("twm-dt-col--dragging");
|
|
2397
|
+
});
|
|
2398
|
+
th.addEventListener("dragover", (ev) => {
|
|
2399
|
+
if (this.dragging === null || this.dragging === col) return;
|
|
2400
|
+
ev.preventDefault();
|
|
2401
|
+
if (ev.dataTransfer) ev.dataTransfer.dropEffect = "move";
|
|
2402
|
+
const order = this.table.getColumnOrder();
|
|
2403
|
+
th.classList.add("twm-dt-col--drop");
|
|
2404
|
+
th.classList.toggle("twm-dt-col--drop-after", order.indexOf(col) > order.indexOf(this.dragging));
|
|
2405
|
+
});
|
|
2406
|
+
th.addEventListener("dragleave", () => th.classList.remove("twm-dt-col--drop", "twm-dt-col--drop-after"));
|
|
2407
|
+
th.addEventListener("drop", (ev) => {
|
|
2408
|
+
th.classList.remove("twm-dt-col--drop", "twm-dt-col--drop-after");
|
|
2409
|
+
const from = this.dragging;
|
|
2410
|
+
this.dragging = null;
|
|
2411
|
+
if (from === null || from === col) return;
|
|
2412
|
+
ev.preventDefault();
|
|
2413
|
+
const order = this.table.getColumnOrder();
|
|
2414
|
+
this.table.moveColumn(order.indexOf(from), order.indexOf(col));
|
|
2415
|
+
});
|
|
2416
|
+
th.addEventListener("dragend", () => {
|
|
2417
|
+
th.classList.remove("twm-dt-col--dragging");
|
|
2418
|
+
this.dragging = null;
|
|
2419
|
+
});
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
/** "Move left" / "Move right" for the header menu of data column `col`:
|
|
2423
|
+
* past the shown column next to it. */
|
|
2424
|
+
menuItems(col) {
|
|
2425
|
+
const shown = this.table._shownColumns();
|
|
2426
|
+
const at = shown.indexOf(col);
|
|
2427
|
+
if (at < 0) return [];
|
|
2428
|
+
const order = this.table.getColumnOrder();
|
|
2429
|
+
const step = (to) => () => this.table.moveColumn(order.indexOf(col), order.indexOf(to));
|
|
2430
|
+
return [
|
|
2431
|
+
{
|
|
2432
|
+
label: "Move left",
|
|
2433
|
+
icon: "arrow_back",
|
|
2434
|
+
action: "move-left",
|
|
2435
|
+
disabled: at === 0,
|
|
2436
|
+
onChoose: at === 0 ? void 0 : step(shown[at - 1])
|
|
2437
|
+
},
|
|
2438
|
+
{
|
|
2439
|
+
label: "Move right",
|
|
2440
|
+
icon: "arrow_forward",
|
|
2441
|
+
action: "move-right",
|
|
2442
|
+
disabled: at === shown.length - 1,
|
|
2443
|
+
onChoose: at === shown.length - 1 ? void 0 : step(shown[at + 1])
|
|
2444
|
+
}
|
|
2445
|
+
];
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
// src/ui/components/header_actions.js
|
|
2450
|
+
function callOption(config, name, ...args) {
|
|
2451
|
+
const fn = config?.[name];
|
|
2452
|
+
if (typeof fn !== "function") return void 0;
|
|
2453
|
+
try {
|
|
2454
|
+
return fn.apply(config, args);
|
|
2455
|
+
} catch (err) {
|
|
2456
|
+
console.error(`[DataTable] ${name} threw`, err);
|
|
2457
|
+
return void 0;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
function headerActionButton(className, { icon, label, title, disabled } = {}, onClick) {
|
|
2461
|
+
const b = document.createElement("button");
|
|
2462
|
+
b.type = "button";
|
|
2463
|
+
b.className = className;
|
|
2464
|
+
if (icon) {
|
|
2465
|
+
const span = document.createElement("span");
|
|
2466
|
+
span.className = "material-symbols-outlined";
|
|
2467
|
+
span.textContent = String(icon);
|
|
2468
|
+
b.appendChild(span);
|
|
2469
|
+
} else if (label != null) {
|
|
2470
|
+
b.appendChild(document.createTextNode(String(label)));
|
|
2471
|
+
}
|
|
2472
|
+
const name = title ?? label;
|
|
2473
|
+
if (name != null) {
|
|
2474
|
+
b.title = String(name);
|
|
2475
|
+
b.setAttribute("aria-label", String(name));
|
|
2476
|
+
}
|
|
2477
|
+
if (disabled) b.disabled = true;
|
|
2478
|
+
b.addEventListener("mousedown", (ev) => ev.stopPropagation());
|
|
2479
|
+
b.addEventListener("click", (ev) => {
|
|
2480
|
+
ev.preventDefault();
|
|
2481
|
+
ev.stopPropagation();
|
|
2482
|
+
onClick(ev);
|
|
2483
|
+
});
|
|
2484
|
+
return b;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// src/ui/components/column_groups.js
|
|
2488
|
+
function normalizeSpans(spec, n, { requireKey = false, defaultLabel = () => "" } = {}) {
|
|
2489
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2490
|
+
const out = [];
|
|
2491
|
+
(Array.isArray(spec) ? spec : []).forEach((g, index) => {
|
|
2492
|
+
if (!g || requireKey && g.key == null) return;
|
|
2493
|
+
const columns = [];
|
|
2494
|
+
for (const c of Array.isArray(g.columns) ? g.columns : []) {
|
|
2495
|
+
const i = Number(c);
|
|
2496
|
+
if (!Number.isInteger(i) || i < 0 || i >= n || taken.has(i)) continue;
|
|
2497
|
+
taken.add(i);
|
|
2498
|
+
columns.push(i);
|
|
2499
|
+
}
|
|
2500
|
+
out.push({ key: g.key, label: g.label == null ? defaultLabel(g) : String(g.label), columns, index });
|
|
2501
|
+
});
|
|
2502
|
+
return out;
|
|
2503
|
+
}
|
|
2504
|
+
function normalize(spec, n) {
|
|
2505
|
+
return normalizeSpans(spec, n, { requireKey: true, defaultLabel: (g) => String(g.key) });
|
|
2506
|
+
}
|
|
2507
|
+
function blankSpanCell(span) {
|
|
2508
|
+
const th = document.createElement("th");
|
|
2509
|
+
th.className = "twm-dt-group twm-dt-group--none";
|
|
2510
|
+
th.colSpan = span;
|
|
2511
|
+
return th;
|
|
2512
|
+
}
|
|
2513
|
+
function labelSpanCell(span, label, title = label) {
|
|
2514
|
+
const th = document.createElement("th");
|
|
2515
|
+
th.className = "twm-dt-group";
|
|
2516
|
+
th.colSpan = span;
|
|
2517
|
+
th.title = title;
|
|
2518
|
+
const el = document.createElement("span");
|
|
2519
|
+
el.className = "twm-dt-group__label";
|
|
2520
|
+
el.textContent = label;
|
|
2521
|
+
th.appendChild(el);
|
|
2522
|
+
return th;
|
|
2523
|
+
}
|
|
2524
|
+
var ColumnGroups = class {
|
|
2525
|
+
/** @param {import('./data_table.js').DataTable} table */
|
|
2526
|
+
constructor(table) {
|
|
2527
|
+
this.table = table;
|
|
2528
|
+
this._memoKey = null;
|
|
2529
|
+
this._memo = null;
|
|
2530
|
+
this._el = null;
|
|
2531
|
+
}
|
|
2532
|
+
_cfg() {
|
|
2533
|
+
return this.table.config;
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* The drawn columns after the lead, in order: `{col, group}` for a shown
|
|
2537
|
+
* data column (`group` the index in `groups`, or null) and
|
|
2538
|
+
* `{placeholder: true, group}` for an empty group's column. The data
|
|
2539
|
+
* columns come in the order the table draws them (STORY-0086's
|
|
2540
|
+
* `columnOrder`, hidden ones left out). Memoised on the groups' content,
|
|
2541
|
+
* the column count and the drawn columns.
|
|
2542
|
+
*/
|
|
2543
|
+
layout() {
|
|
2544
|
+
const cfg = this._cfg();
|
|
2545
|
+
const n = cfg.headers?.length || 0;
|
|
2546
|
+
const shown = this.table._shownColumns();
|
|
2547
|
+
const spec = Array.isArray(cfg.columnGroups) ? cfg.columnGroups : [];
|
|
2548
|
+
const key3 = `${n}|${shown.join(",")}|${spec.map((g) => g ? `${g.key}${g.label}${(g.columns || []).join(",")}` : "").join("")}`;
|
|
2549
|
+
if (key3 === this._memoKey) return this._memo;
|
|
2550
|
+
const groups = normalize(spec, n);
|
|
2551
|
+
const groupOf = new Array(n).fill(null);
|
|
2552
|
+
groups.forEach((g, gi) => {
|
|
2553
|
+
for (const c of g.columns) groupOf[c] = gi;
|
|
2554
|
+
});
|
|
2555
|
+
const empty = groups.map((g) => g.columns.length === 0);
|
|
2556
|
+
const placed = /* @__PURE__ */ new Set();
|
|
2557
|
+
const slots = [];
|
|
2558
|
+
const placeEmptyBefore = (gi) => {
|
|
2559
|
+
for (let e = 0; e < gi; e++) {
|
|
2560
|
+
if (empty[e] && !placed.has(e)) {
|
|
2561
|
+
placed.add(e);
|
|
2562
|
+
slots.push({ placeholder: true, group: e });
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
for (const c of shown) {
|
|
2567
|
+
const gi = groupOf[c];
|
|
2568
|
+
if (gi !== null) placeEmptyBefore(gi);
|
|
2569
|
+
slots.push({ col: c, group: gi });
|
|
2570
|
+
}
|
|
2571
|
+
placeEmptyBefore(groups.length);
|
|
2572
|
+
const slotOfCol = /* @__PURE__ */ new Map();
|
|
2573
|
+
slots.forEach((s, i) => {
|
|
2574
|
+
if (!s.placeholder) slotOfCol.set(s.col, i);
|
|
2575
|
+
});
|
|
2576
|
+
const placeholders = slots.filter((s) => s.placeholder).length;
|
|
2577
|
+
this._memoKey = key3;
|
|
2578
|
+
this._memo = { groups, slots, slotOfCol, placeholders };
|
|
2579
|
+
return this._memo;
|
|
2580
|
+
}
|
|
2581
|
+
/** How many placeholder columns are drawn. */
|
|
2582
|
+
placeholderCount() {
|
|
2583
|
+
return this.layout().placeholders;
|
|
2584
|
+
}
|
|
2585
|
+
/** A shown data column's place after the lead, or -1 when the layout
|
|
2586
|
+
* has no placeholder (the table's own arithmetic then holds). */
|
|
2587
|
+
slotOf(colIdx) {
|
|
2588
|
+
const l = this.layout();
|
|
2589
|
+
if (!l.placeholders) return -1;
|
|
2590
|
+
return l.slotOfCol.has(colIdx) ? l.slotOfCol.get(colIdx) : -1;
|
|
2591
|
+
}
|
|
2592
|
+
/** What the drawn column `i` after the lead is: the data column, null for
|
|
2593
|
+
* a placeholder or past the end, `undefined` when the layout has no
|
|
2594
|
+
* placeholder (the table's own arithmetic then holds). */
|
|
2595
|
+
slotAt(i) {
|
|
2596
|
+
const l = this.layout();
|
|
2597
|
+
if (!l.placeholders) return void 0;
|
|
2598
|
+
const s = l.slots[i];
|
|
2599
|
+
return s && !s.placeholder ? s.col : null;
|
|
2600
|
+
}
|
|
2601
|
+
/** Part of the table's column signature: where the placeholders stand. */
|
|
2602
|
+
sig() {
|
|
2603
|
+
const l = this.layout();
|
|
2604
|
+
if (!l.placeholders) return "";
|
|
2605
|
+
return `|g${l.slots.map((s, i) => s.placeholder ? i : "").filter((x) => x !== "").join(",")}`;
|
|
2606
|
+
}
|
|
2607
|
+
/** The drawn positions (after the lead) of the placeholders, ascending,
|
|
2608
|
+
* with their groups. */
|
|
2609
|
+
_placeholderSlots() {
|
|
2610
|
+
const out = [];
|
|
2611
|
+
this.layout().slots.forEach((s, i) => {
|
|
2612
|
+
if (s.placeholder) out.push({ at: i, group: s.group });
|
|
2613
|
+
});
|
|
2614
|
+
return out;
|
|
2615
|
+
}
|
|
2616
|
+
/** Put the placeholders' cells into a drawn row whose cells are the lead
|
|
2617
|
+
* and the shown data columns, in order. */
|
|
2618
|
+
_insert(tr, make) {
|
|
2619
|
+
const lead = this.table._lead().all;
|
|
2620
|
+
for (const { at, group } of this._placeholderSlots()) {
|
|
2621
|
+
const ref = tr.children[lead + at] || null;
|
|
2622
|
+
tr.insertBefore(make(this.layout().groups[group]), ref);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
/** STORY-0066. A body row gets an empty cell under each placeholder. */
|
|
2626
|
+
fillRow(tr) {
|
|
2627
|
+
if (!this.placeholderCount()) return;
|
|
2628
|
+
this._insert(tr, () => {
|
|
2629
|
+
const td = document.createElement("td");
|
|
2630
|
+
td.className = "twm-dt-cell--placeholder";
|
|
2631
|
+
return td;
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
/**
|
|
2635
|
+
* After the header is built and in the header table: the placeholders'
|
|
2636
|
+
* header and filter cells, and the group table above the header table.
|
|
2637
|
+
*/
|
|
2638
|
+
decorate(headerWrap, headerTable) {
|
|
2639
|
+
this._el = null;
|
|
2640
|
+
if (!headerWrap || !headerTable) return;
|
|
2641
|
+
const thead = headerTable.querySelector("thead");
|
|
2642
|
+
const labelRow = thead?.querySelector("tr");
|
|
2643
|
+
if (!labelRow) return;
|
|
2644
|
+
if (this.placeholderCount()) {
|
|
2645
|
+
this._insert(labelRow, (g) => this._placeholderHead(g));
|
|
2646
|
+
for (const tr of thead.querySelectorAll("tr.twm-data-table__filter-row")) {
|
|
2647
|
+
this._insert(tr, () => {
|
|
2648
|
+
const th = document.createElement("th");
|
|
2649
|
+
th.className = "data-table__filter-cell data-table__filter-cell--empty twm-dt-col--placeholder";
|
|
2650
|
+
return th;
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
const groupTable = this._buildGroupTable(headerTable);
|
|
2655
|
+
headerWrap.insertBefore(groupTable, headerTable);
|
|
2656
|
+
this._el = groupTable;
|
|
2657
|
+
this.align();
|
|
2658
|
+
}
|
|
2659
|
+
_placeholderHead(group) {
|
|
2660
|
+
const th = document.createElement("th");
|
|
2661
|
+
th.className = "text twm-dt-col--placeholder";
|
|
2662
|
+
if (typeof this._cfg().onColumnAdd === "function") {
|
|
2663
|
+
th.appendChild(this._addButton(group));
|
|
2664
|
+
}
|
|
2665
|
+
return th;
|
|
2666
|
+
}
|
|
2667
|
+
_addButton(group) {
|
|
2668
|
+
return headerActionButton(
|
|
2669
|
+
"twm-dt-group__add",
|
|
2670
|
+
{ icon: "add", title: `Add column to ${group.label}` },
|
|
2671
|
+
() => callOption(this._cfg(), "onColumnAdd", group.key)
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
_buildGroupTable(headerTable) {
|
|
2675
|
+
const { groups, slots } = this.layout();
|
|
2676
|
+
const lead = this.table._lead().all;
|
|
2677
|
+
const table = document.createElement("table");
|
|
2678
|
+
table.className = `${headerTable.className} twm-dt-groups`;
|
|
2679
|
+
const colgroup = document.createElement("colgroup");
|
|
2680
|
+
for (let i2 = 0; i2 < lead + slots.length; i2++) colgroup.appendChild(document.createElement("col"));
|
|
2681
|
+
table.appendChild(colgroup);
|
|
2682
|
+
const thead = document.createElement("thead");
|
|
2683
|
+
const tr = document.createElement("tr");
|
|
2684
|
+
tr.className = "twm-dt-group-row";
|
|
2685
|
+
if (lead) {
|
|
2686
|
+
const th = document.createElement("th");
|
|
2687
|
+
th.className = "twm-dt-group twm-dt-group--lead";
|
|
2688
|
+
th.colSpan = lead;
|
|
2689
|
+
tr.appendChild(th);
|
|
2690
|
+
}
|
|
2691
|
+
let i = 0;
|
|
2692
|
+
while (i < slots.length) {
|
|
2693
|
+
const gi = slots[i].group;
|
|
2694
|
+
let j = i + 1;
|
|
2695
|
+
while (j < slots.length && slots[j].group === gi) j++;
|
|
2696
|
+
tr.appendChild(gi === null ? blankSpanCell(j - i) : this._groupCell(groups[gi], j - i, slots[i].placeholder));
|
|
2697
|
+
i = j;
|
|
2698
|
+
}
|
|
2699
|
+
thead.appendChild(tr);
|
|
2700
|
+
table.appendChild(thead);
|
|
2701
|
+
return table;
|
|
2702
|
+
}
|
|
2703
|
+
_groupCell(group, span, empty) {
|
|
2704
|
+
const th = labelSpanCell(span, group.label);
|
|
2705
|
+
if (empty) th.classList.add("twm-dt-group--empty");
|
|
2706
|
+
const actions = document.createElement("span");
|
|
2707
|
+
actions.className = "twm-dt-group__actions";
|
|
2708
|
+
const cfg = this._cfg();
|
|
2709
|
+
const items = callOption(cfg, "groupActions", cfg.columnGroups[group.index], { key: group.key, columns: group.columns.slice(), table: this.table });
|
|
2710
|
+
for (const item of Array.isArray(items) ? items.filter(Boolean) : []) {
|
|
2711
|
+
actions.appendChild(headerActionButton(
|
|
2712
|
+
"twm-dt-group__action",
|
|
2713
|
+
item,
|
|
2714
|
+
() => callOption(cfg, "onGroupAction", item.action, group.key)
|
|
2715
|
+
));
|
|
2716
|
+
}
|
|
2717
|
+
if (typeof cfg.onColumnAdd === "function") actions.appendChild(this._addButton(group));
|
|
2718
|
+
if (actions.childNodes.length) th.appendChild(actions);
|
|
2719
|
+
return th;
|
|
2720
|
+
}
|
|
2721
|
+
/** The group table follows the header table: the same column widths,
|
|
2722
|
+
* table width, layout and sideways offset. */
|
|
2723
|
+
align() {
|
|
2724
|
+
const groupTable = this._el;
|
|
2725
|
+
const headerTable = this.table._headerTableEl;
|
|
2726
|
+
if (!groupTable || !headerTable) return;
|
|
2727
|
+
const labelRow = headerTable.querySelector("thead > tr");
|
|
2728
|
+
const cols = groupTable.querySelector("colgroup")?.children || [];
|
|
2729
|
+
for (let i = 0; i < cols.length; i++) {
|
|
2730
|
+
const th = labelRow?.children[i];
|
|
2731
|
+
const w = th ? parseFloat(th.style.width) : NaN;
|
|
2732
|
+
cols[i].style.width = Number.isFinite(w) ? `${w}px` : "";
|
|
2733
|
+
}
|
|
2734
|
+
groupTable.style.width = headerTable.style.width;
|
|
2735
|
+
groupTable.style.tableLayout = headerTable.style.tableLayout;
|
|
2736
|
+
groupTable.style.transform = headerTable.style.transform;
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
360
2739
|
|
|
361
|
-
// src/ui/components/
|
|
362
|
-
var
|
|
363
|
-
function
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
2740
|
+
// src/ui/components/crosstab.js
|
|
2741
|
+
var DEFAULT_EMPTY_LABEL = "(none)";
|
|
2742
|
+
function unzoom(el) {
|
|
2743
|
+
return 1 / (el?.currentCSSZoom || 1);
|
|
2744
|
+
}
|
|
2745
|
+
var Crosstab = class {
|
|
2746
|
+
/** @param {import('./data_table.js').DataTable} table */
|
|
2747
|
+
constructor(table) {
|
|
2748
|
+
this.table = table;
|
|
2749
|
+
this._lefts = [];
|
|
2750
|
+
this._leafRow = null;
|
|
2751
|
+
}
|
|
2752
|
+
_spec() {
|
|
2753
|
+
const s = this.table.config.crosstab;
|
|
2754
|
+
return s && typeof s === "object" ? s : {};
|
|
2755
|
+
}
|
|
2756
|
+
/** How many of the drawn data columns are row labels: the leading ones. */
|
|
2757
|
+
labelCount() {
|
|
2758
|
+
const k = Math.floor(Number(this._spec().labelColumns) || 0);
|
|
2759
|
+
return Math.max(0, Math.min(k, this.table._shownColumns().length));
|
|
2760
|
+
}
|
|
2761
|
+
/** The drawn columns that pin to the left: the select and row-number
|
|
2762
|
+
* columns, and the label columns. */
|
|
2763
|
+
pinnedCount() {
|
|
2764
|
+
return this.table._lead().all + this.labelCount();
|
|
2765
|
+
}
|
|
2766
|
+
/** The drawn data columns after the label columns, in order. */
|
|
2767
|
+
bodyColumns() {
|
|
2768
|
+
return this.table._shownColumns().slice(this.labelCount());
|
|
2769
|
+
}
|
|
2770
|
+
/** Columns drawn that are no data column: the empty axis's one. */
|
|
2771
|
+
extraColumns() {
|
|
2772
|
+
return this.bodyColumns().length ? 0 : 1;
|
|
2773
|
+
}
|
|
2774
|
+
/** The header rows as drawn: per row, `{cell, span}` runs over the body
|
|
2775
|
+
* columns (`cell` null for a run in no cell of that row). None for an
|
|
2776
|
+
* empty axis. */
|
|
2777
|
+
headerRuns() {
|
|
2778
|
+
const body = this.bodyColumns();
|
|
2779
|
+
if (!body.length) return [];
|
|
2780
|
+
const spec = this._spec().headerRows;
|
|
2781
|
+
const n = this.table.config.headers?.length || 0;
|
|
2782
|
+
return (Array.isArray(spec) ? spec : []).map((rowSpec) => {
|
|
2783
|
+
const cells = normalizeSpans(rowSpec, n);
|
|
2784
|
+
const cellOf = /* @__PURE__ */ new Map();
|
|
2785
|
+
cells.forEach((c) => {
|
|
2786
|
+
for (const col of c.columns) cellOf.set(col, c);
|
|
2787
|
+
});
|
|
2788
|
+
const runs = [];
|
|
2789
|
+
for (const col of body) {
|
|
2790
|
+
const cell = cellOf.get(col) || null;
|
|
2791
|
+
const last = runs[runs.length - 1];
|
|
2792
|
+
if (last && last.cell === cell) last.span++;
|
|
2793
|
+
else runs.push({ cell, span: 1, rowSpec });
|
|
2794
|
+
}
|
|
2795
|
+
return runs;
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
/** STORY-0091. A body row: its pinned cells marked and put at the measured
|
|
2799
|
+
* offsets (a row the virtual window draws later lines up at once), and
|
|
2800
|
+
* the empty axis's cell. */
|
|
2801
|
+
fillRow(tr) {
|
|
2802
|
+
if (this.extraColumns()) {
|
|
2803
|
+
const td = document.createElement("td");
|
|
2804
|
+
td.className = "twm-dt-crosstab__none";
|
|
2805
|
+
tr.appendChild(td);
|
|
380
2806
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
btn.dataset.value = key;
|
|
392
|
-
if (opt.title) btn.title = String(opt.title);
|
|
393
|
-
if (opt.icon) {
|
|
394
|
-
const icon = document.createElement("span");
|
|
395
|
-
icon.className = "material-symbols-outlined twm-dt-filter-chip__icon";
|
|
396
|
-
icon.setAttribute("aria-hidden", "true");
|
|
397
|
-
icon.textContent = String(opt.icon);
|
|
398
|
-
btn.appendChild(icon);
|
|
2807
|
+
this._pinRowCells(tr);
|
|
2808
|
+
}
|
|
2809
|
+
_pinRowCells(tr) {
|
|
2810
|
+
const p = this.pinnedCount();
|
|
2811
|
+
for (let j = 0; j < p && j < tr.children.length; j++) {
|
|
2812
|
+
const cell = tr.children[j];
|
|
2813
|
+
cell.classList.add("twm-dt-pin");
|
|
2814
|
+
if (j === p - 1) cell.classList.add("twm-dt-pin--last");
|
|
2815
|
+
cell.__pinCol = j;
|
|
2816
|
+
if (this._lefts[j] != null) cell.style.left = `${this._lefts[j]}px`;
|
|
399
2817
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* After `_createTable`: the header rows above the column headers, the
|
|
2821
|
+
* corner, the pinned header cells and the empty axis's header — all in the
|
|
2822
|
+
* body table's own `<thead>`.
|
|
2823
|
+
*/
|
|
2824
|
+
decorate(tableEl) {
|
|
2825
|
+
this._leafRow = null;
|
|
2826
|
+
const thead = tableEl?.querySelector("thead");
|
|
2827
|
+
const leaf = thead?.querySelector("tr");
|
|
2828
|
+
if (!leaf) return;
|
|
2829
|
+
tableEl.classList.add("twm-dt-crosstab");
|
|
2830
|
+
this._leafRow = leaf;
|
|
2831
|
+
leaf.classList.add("twm-dt-crosstab__leaf");
|
|
2832
|
+
const filters = [...thead.querySelectorAll("tr.twm-data-table__filter-row")];
|
|
2833
|
+
if (this.extraColumns()) {
|
|
2834
|
+
const th = document.createElement("th");
|
|
2835
|
+
th.className = "text twm-dt-crosstab__none";
|
|
2836
|
+
const label = this._spec().emptyLabel;
|
|
2837
|
+
th.textContent = label == null ? DEFAULT_EMPTY_LABEL : String(label);
|
|
2838
|
+
leaf.appendChild(th);
|
|
2839
|
+
for (const tr of filters) {
|
|
2840
|
+
const cell = document.createElement("th");
|
|
2841
|
+
cell.className = "data-table__filter-cell data-table__filter-cell--empty twm-dt-crosstab__none";
|
|
2842
|
+
tr.appendChild(cell);
|
|
2843
|
+
}
|
|
410
2844
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
2845
|
+
for (const tr of [leaf, ...filters]) this._pinRowCells(tr);
|
|
2846
|
+
const levels = this.headerRuns();
|
|
2847
|
+
const p = this.pinnedCount();
|
|
2848
|
+
levels.forEach((runs, r) => {
|
|
2849
|
+
const tr = document.createElement("tr");
|
|
2850
|
+
tr.className = "twm-dt-crosstab__band";
|
|
2851
|
+
if (r === 0) {
|
|
2852
|
+
for (let j = 0; j < p; j++) {
|
|
2853
|
+
const th = document.createElement("th");
|
|
2854
|
+
th.className = "twm-dt-crosstab__corner twm-dt-pin";
|
|
2855
|
+
if (j === p - 1) th.classList.add("twm-dt-pin--last");
|
|
2856
|
+
th.rowSpan = levels.length;
|
|
2857
|
+
th.__pinCol = j;
|
|
2858
|
+
tr.appendChild(th);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
for (const run of runs) tr.appendChild(this._bandCell(run));
|
|
2862
|
+
thead.insertBefore(tr, leaf);
|
|
414
2863
|
});
|
|
415
|
-
chips.set(key, { btn, count });
|
|
416
|
-
el.appendChild(btn);
|
|
417
2864
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
2865
|
+
/** A header row's cell: STORY-0066's spanning cells, the consumer's
|
|
2866
|
+
* `className` and `title` on a labelled one. */
|
|
2867
|
+
_bandCell({ cell, span, rowSpec }) {
|
|
2868
|
+
if (!cell) return blankSpanCell(span);
|
|
2869
|
+
const own = rowSpec[cell.index];
|
|
2870
|
+
const th = labelSpanCell(span, cell.label, own?.title == null ? cell.label : String(own.title));
|
|
2871
|
+
if (own?.className) this.table._addClasses(th, own.className);
|
|
2872
|
+
return th;
|
|
421
2873
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
2874
|
+
/**
|
|
2875
|
+
* Once the table is in the document: pin now (a layout already there is
|
|
2876
|
+
* used at once), on the next frame (when it has settled), and whenever a
|
|
2877
|
+
* header row, a pinned header cell or the table's box changes size. The
|
|
2878
|
+
* observer goes with the render (`_disposers`).
|
|
2879
|
+
*
|
|
2880
|
+
* The split layout's own resize observer and settle frame hang off its
|
|
2881
|
+
* header table, which a crosstab has none of; so the virtual window's
|
|
2882
|
+
* re-measure and resize (`_virtRemeasure`, `_virtUpdate`) run from here.
|
|
2883
|
+
*/
|
|
2884
|
+
install() {
|
|
2885
|
+
const table = this.table;
|
|
2886
|
+
const tableEl = table._tableEl;
|
|
2887
|
+
if (!tableEl || !this._leafRow) return;
|
|
2888
|
+
this.pin();
|
|
2889
|
+
requestAnimationFrame(() => {
|
|
2890
|
+
if (table._tableEl !== tableEl) return;
|
|
2891
|
+
this.pin();
|
|
2892
|
+
table._virtRemeasure();
|
|
2893
|
+
});
|
|
2894
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
2895
|
+
const observer = createRafResizeObserver(() => {
|
|
2896
|
+
if (table._tableEl !== tableEl) return;
|
|
2897
|
+
this.pin();
|
|
2898
|
+
table._virtUpdate();
|
|
2899
|
+
});
|
|
2900
|
+
if (table._wrapperEl) observer.observe(table._wrapperEl);
|
|
2901
|
+
const thead = tableEl.querySelector("thead");
|
|
2902
|
+
for (const tr of thead.querySelectorAll("tr")) observer.observe(tr);
|
|
2903
|
+
for (let j = 0; j < this.pinnedCount(); j++) {
|
|
2904
|
+
const cell = this._leafRow.children[j];
|
|
2905
|
+
if (cell) observer.observe(cell);
|
|
2906
|
+
}
|
|
2907
|
+
table._disposers.push(() => {
|
|
2908
|
+
try {
|
|
2909
|
+
observer.disconnect();
|
|
2910
|
+
} catch (_) {
|
|
2911
|
+
}
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
/** Measure and write every offset: each header row's `top`, each pinned
|
|
2915
|
+
* column's `left`. */
|
|
2916
|
+
pin() {
|
|
2917
|
+
const tableEl = this.table._tableEl;
|
|
2918
|
+
const thead = tableEl?.querySelector("thead");
|
|
2919
|
+
if (!thead || !this._leafRow) return;
|
|
2920
|
+
const rows = [...thead.rows];
|
|
2921
|
+
const scale = unzoom(thead);
|
|
2922
|
+
const origin = rows[0].getBoundingClientRect().top;
|
|
2923
|
+
for (const tr of rows) {
|
|
2924
|
+
const top = Math.max(0, (tr.getBoundingClientRect().top - origin) * scale);
|
|
2925
|
+
const css = `${+top.toFixed(2)}px`;
|
|
2926
|
+
for (const th of tr.cells) th.style.top = css;
|
|
2927
|
+
}
|
|
2928
|
+
const p = this.pinnedCount();
|
|
2929
|
+
const lefts = [0];
|
|
2930
|
+
for (let j = 1; j < p; j++) {
|
|
2931
|
+
const cell = this._leafRow.children[j - 1];
|
|
2932
|
+
const w = cell ? cell.getBoundingClientRect().width * scale : 0;
|
|
2933
|
+
lefts[j] = +(lefts[j - 1] + w).toFixed(2);
|
|
2934
|
+
}
|
|
2935
|
+
this._lefts = p ? lefts : [];
|
|
2936
|
+
for (const cell of tableEl.querySelectorAll(".twm-dt-pin")) {
|
|
2937
|
+
const left = this._lefts[cell.__pinCol];
|
|
2938
|
+
if (left != null) cell.style.left = `${left}px`;
|
|
2939
|
+
}
|
|
2940
|
+
this._lockColumns(scale);
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Under `virtualize` the rows drawn change with every scroll, and with
|
|
2944
|
+
* them the widths an automatic layout gives the columns. The first pass
|
|
2945
|
+
* that sees a layout writes the widths it measured on the column headers
|
|
2946
|
+
* into the `<colgroup>` and fixes the layout, so the columns hold still.
|
|
2947
|
+
* No layout (every width 0) is no measurement, and nothing is written.
|
|
2948
|
+
*/
|
|
2949
|
+
_lockColumns(scale) {
|
|
2950
|
+
const cols = this.table._cols();
|
|
2951
|
+
const tableEl = this.table._tableEl;
|
|
2952
|
+
if (!cols || !tableEl || tableEl.__crosstabLocked) return;
|
|
2953
|
+
const cells = this._leafRow.children;
|
|
2954
|
+
if (cells.length !== cols.length) return;
|
|
2955
|
+
const widths = [...cells].map((c) => c.getBoundingClientRect().width * scale);
|
|
2956
|
+
if (!widths.every((w) => w > 0)) return;
|
|
2957
|
+
widths.forEach((w, i) => {
|
|
2958
|
+
cols[i].style.width = `${+w.toFixed(2)}px`;
|
|
2959
|
+
});
|
|
2960
|
+
tableEl.style.tableLayout = "fixed";
|
|
2961
|
+
tableEl.style.width = `${+widths.reduce((a, b) => a + b, 0).toFixed(2)}px`;
|
|
2962
|
+
tableEl.__crosstabLocked = true;
|
|
2963
|
+
}
|
|
2964
|
+
};
|
|
2965
|
+
|
|
2966
|
+
// src/ui/components/table_structure.js
|
|
2967
|
+
var STRUCTURE_OPTIONS = ["columnActions", "onColumnRename", "onColumnDelete", "onRowAdd", "onRowDelete"];
|
|
2968
|
+
var TableStructure = class {
|
|
2969
|
+
/** @param {import('./data_table.js').DataTable} table */
|
|
2970
|
+
constructor(table) {
|
|
2971
|
+
this.table = table;
|
|
2972
|
+
this._renamer = null;
|
|
2973
|
+
}
|
|
2974
|
+
_cfg() {
|
|
2975
|
+
return this.table.config;
|
|
2976
|
+
}
|
|
2977
|
+
_has(name) {
|
|
2978
|
+
return typeof this._cfg()[name] === "function";
|
|
2979
|
+
}
|
|
2980
|
+
_call(name, ...args) {
|
|
2981
|
+
return callOption(this._cfg(), name, ...args);
|
|
2982
|
+
}
|
|
2983
|
+
/** Whether the header's right-click has items of this module's. */
|
|
2984
|
+
hasHeaderItems() {
|
|
2985
|
+
return this._has("onColumnRename") || this._has("onColumnDelete") || this._has("onRowAdd");
|
|
2986
|
+
}
|
|
2987
|
+
/** Whether a row's right-click has items of this module's. */
|
|
2988
|
+
hasRowItems() {
|
|
2989
|
+
return this._has("onRowAdd") || this._has("onRowDelete");
|
|
2990
|
+
}
|
|
2991
|
+
/** The header menu's items for the data column `colIdx` (null off one). */
|
|
2992
|
+
headerItems(colIdx) {
|
|
2993
|
+
const items = [];
|
|
2994
|
+
if (colIdx !== null && this._has("onColumnRename")) {
|
|
2995
|
+
items.push({ label: "Rename column", icon: "edit", onChoose: () => this.rename(colIdx) });
|
|
2996
|
+
}
|
|
2997
|
+
if (colIdx !== null && this._has("onColumnDelete")) {
|
|
2998
|
+
items.push({
|
|
2999
|
+
label: "Delete column",
|
|
3000
|
+
icon: "delete",
|
|
3001
|
+
danger: true,
|
|
3002
|
+
onChoose: () => this._call("onColumnDelete", colIdx)
|
|
3003
|
+
});
|
|
428
3004
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (typeof ctx.setValue === "function") ctx.setValue(out);
|
|
432
|
-
try {
|
|
433
|
-
spec.onChange?.(out);
|
|
434
|
-
} catch (err) {
|
|
435
|
-
console.error("[chips filter] onChange threw", err);
|
|
3005
|
+
if (this._has("onRowAdd")) {
|
|
3006
|
+
items.push({ label: "Add row", icon: "add", onChoose: () => this._call("onRowAdd", null) });
|
|
436
3007
|
}
|
|
3008
|
+
return items;
|
|
437
3009
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
3010
|
+
/** The body menu's items for the row `rowIdx` (its original index). */
|
|
3011
|
+
rowItems(rowIdx) {
|
|
3012
|
+
const items = [];
|
|
3013
|
+
if (this._has("onRowAdd")) {
|
|
3014
|
+
items.push({ label: "Insert row below", icon: "add", onChoose: () => this._call("onRowAdd", rowIdx) });
|
|
443
3015
|
}
|
|
3016
|
+
if (this._has("onRowDelete")) {
|
|
3017
|
+
items.push({
|
|
3018
|
+
label: "Delete row",
|
|
3019
|
+
icon: "delete",
|
|
3020
|
+
danger: true,
|
|
3021
|
+
onChoose: () => this._call("onRowDelete", rowIdx)
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
return items;
|
|
444
3025
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
3026
|
+
/**
|
|
3027
|
+
* After the header is built: each data column's label in a span the
|
|
3028
|
+
* renamer can take over, and the consumer's action buttons.
|
|
3029
|
+
*/
|
|
3030
|
+
decorate(headerTable) {
|
|
3031
|
+
this._renamer?.destroy();
|
|
3032
|
+
this._renamer = null;
|
|
3033
|
+
const labelRow = headerTable?.querySelector("thead > tr");
|
|
3034
|
+
if (!labelRow) return;
|
|
3035
|
+
const headers = this._cfg().headers || [];
|
|
3036
|
+
const rename = this._has("onColumnRename");
|
|
3037
|
+
const actions = this._has("columnActions");
|
|
3038
|
+
if (!rename && !actions) return;
|
|
3039
|
+
for (let colIdx = 0; colIdx < headers.length; colIdx++) {
|
|
3040
|
+
const th = labelRow.children[this.table._domIdxOf(colIdx)];
|
|
3041
|
+
if (!th) continue;
|
|
3042
|
+
if (rename) this._wrapLabel(th, colIdx);
|
|
3043
|
+
if (actions) this._addActions(th, colIdx, headers[colIdx]);
|
|
3044
|
+
}
|
|
448
3045
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
3046
|
+
_wrapLabel(th, colIdx) {
|
|
3047
|
+
const text = [...th.childNodes].find((n) => n.nodeType === 3);
|
|
3048
|
+
if (!text) return;
|
|
3049
|
+
const label = document.createElement("span");
|
|
3050
|
+
label.className = "twm-dt-col-label";
|
|
3051
|
+
th.insertBefore(label, text);
|
|
3052
|
+
label.appendChild(text);
|
|
3053
|
+
label.addEventListener("keydown", (ev) => ev.stopPropagation());
|
|
3054
|
+
if (!this._cfg().sortable) {
|
|
3055
|
+
label.addEventListener("dblclick", (ev) => {
|
|
3056
|
+
ev.preventDefault();
|
|
3057
|
+
ev.stopPropagation();
|
|
3058
|
+
this.rename(colIdx);
|
|
3059
|
+
});
|
|
455
3060
|
}
|
|
456
3061
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
3062
|
+
_addActions(th, colIdx, header) {
|
|
3063
|
+
const items = this._call("columnActions", colIdx, { colIdx, header, table: this.table });
|
|
3064
|
+
const list = Array.isArray(items) ? items.filter(Boolean) : [];
|
|
3065
|
+
if (!list.length) return;
|
|
3066
|
+
const box = document.createElement("span");
|
|
3067
|
+
box.className = "twm-dt-col-actions";
|
|
3068
|
+
for (const item of list) {
|
|
3069
|
+
box.appendChild(headerActionButton(
|
|
3070
|
+
"twm-dt-col-action",
|
|
3071
|
+
item,
|
|
3072
|
+
() => this._call("onColumnAction", item.action, colIdx)
|
|
3073
|
+
));
|
|
465
3074
|
}
|
|
466
|
-
|
|
467
|
-
}
|
|
3075
|
+
th.appendChild(box);
|
|
3076
|
+
}
|
|
3077
|
+
/**
|
|
3078
|
+
* Open the renamer on the data column's header. Enter (or the focus
|
|
3079
|
+
* leaving) with a different, non-empty name calls
|
|
3080
|
+
* `onColumnRename(colIdx, name)`; Escape, or the name left as it was,
|
|
3081
|
+
* restores the header and calls nothing. Returns whether it opened.
|
|
3082
|
+
*/
|
|
3083
|
+
rename(colIdx) {
|
|
3084
|
+
if (!this._has("onColumnRename")) return false;
|
|
3085
|
+
const labelRow = this.table._headerTableEl?.querySelector("thead > tr");
|
|
3086
|
+
const dom = this.table._domIdxOf(colIdx);
|
|
3087
|
+
const label = dom < 0 ? null : labelRow?.children[dom]?.querySelector(".twm-dt-col-label");
|
|
3088
|
+
if (!label) return false;
|
|
3089
|
+
this._renamer?.cancel();
|
|
3090
|
+
const before = label.textContent || "";
|
|
3091
|
+
const done = () => {
|
|
3092
|
+
if (this._renamer === renamer) this._renamer = null;
|
|
3093
|
+
};
|
|
3094
|
+
const renamer = attachInlineRenamer({
|
|
3095
|
+
label,
|
|
3096
|
+
inputClass: "twm-dt-col-rename",
|
|
3097
|
+
onCommit: async (raw) => {
|
|
3098
|
+
const name = String(raw ?? "").trim();
|
|
3099
|
+
if (!name || name === before) return { success: false };
|
|
3100
|
+
const answer = await this._call("onColumnRename", colIdx, name);
|
|
3101
|
+
if (answer === false || answer && answer.ok === false) return { success: false };
|
|
3102
|
+
return { newValue: name };
|
|
3103
|
+
},
|
|
3104
|
+
onCancel: done,
|
|
3105
|
+
onFinish: done
|
|
3106
|
+
});
|
|
3107
|
+
this._renamer = renamer;
|
|
3108
|
+
renamer.start();
|
|
3109
|
+
return true;
|
|
3110
|
+
}
|
|
3111
|
+
dispose() {
|
|
3112
|
+
this._renamer?.destroy();
|
|
3113
|
+
this._renamer = null;
|
|
3114
|
+
}
|
|
3115
|
+
};
|
|
468
3116
|
|
|
469
3117
|
// src/ui/components/data_table.js
|
|
470
3118
|
var DEFAULT_PAGE_SIZE = 100;
|
|
@@ -764,12 +3412,67 @@ var DataTable = class {
|
|
|
764
3412
|
hiddenColumns: null,
|
|
765
3413
|
columnChooser: false,
|
|
766
3414
|
onColumnsChange: null,
|
|
3415
|
+
// STORY-0086. Off — columns in their natural order, as in 0.16.
|
|
3416
|
+
reorderable: false,
|
|
3417
|
+
columnOrder: null,
|
|
767
3418
|
// STORY-0016. Off — no select column, as in 0.11 — until set.
|
|
768
3419
|
selectToggle: false,
|
|
769
3420
|
selectMode: false,
|
|
770
3421
|
onSelectModeChange: null,
|
|
3422
|
+
// STORY-0003. Off — rows selected as in 0.16, no editing — until
|
|
3423
|
+
// set. The typedef says what each does.
|
|
3424
|
+
selectionMode: "rows",
|
|
3425
|
+
editable: false,
|
|
3426
|
+
editors: null,
|
|
3427
|
+
onCellCommit: null,
|
|
3428
|
+
onCellSelectionChange: null,
|
|
3429
|
+
headerMenu: null,
|
|
3430
|
+
onHeaderMenuAction: null,
|
|
3431
|
+
// STORY-0088. Off — commits go to onCellCommit — until set.
|
|
3432
|
+
staged: false,
|
|
3433
|
+
historyLimit: 100,
|
|
3434
|
+
onCommit: null,
|
|
3435
|
+
onStagedChange: null,
|
|
3436
|
+
stagedRefusal: null,
|
|
3437
|
+
// STORY-0066. Off — one header row, no structural edits — until
|
|
3438
|
+
// set. The typedef says what each does.
|
|
3439
|
+
columnGroups: null,
|
|
3440
|
+
groupActions: null,
|
|
3441
|
+
onGroupAction: null,
|
|
3442
|
+
onColumnAdd: null,
|
|
3443
|
+
columnActions: null,
|
|
3444
|
+
onColumnAction: null,
|
|
3445
|
+
onColumnRename: null,
|
|
3446
|
+
onColumnDelete: null,
|
|
3447
|
+
onRowAdd: null,
|
|
3448
|
+
onRowDelete: null,
|
|
3449
|
+
// STORY-0089. Off — no fill handle — until set.
|
|
3450
|
+
fillHandle: false,
|
|
771
3451
|
...config
|
|
772
3452
|
};
|
|
3453
|
+
this._cellGrid = null;
|
|
3454
|
+
this._columnGroups = null;
|
|
3455
|
+
this._tableStructure = null;
|
|
3456
|
+
this._crosstabLayout = null;
|
|
3457
|
+
if (this.config.selectionMode === "cells") {
|
|
3458
|
+
const clash = ["selectToggle", "selectMode", "keyboardNav"].filter((k) => !!this.config[k]);
|
|
3459
|
+
if (clash.length) {
|
|
3460
|
+
console.warn(`[DataTable] ${clash.join("/")} cannot be combined with selectionMode: 'cells'; ignored.`);
|
|
3461
|
+
for (const k of clash) this.config[k] = false;
|
|
3462
|
+
}
|
|
3463
|
+
this._cellGrid = new CellGrid(this);
|
|
3464
|
+
} else if (this.config.editable) {
|
|
3465
|
+
console.warn("[DataTable] `editable` needs selectionMode: 'cells'; ignored.");
|
|
3466
|
+
}
|
|
3467
|
+
if (this.config.staged && !this._cellGrid) {
|
|
3468
|
+
console.warn("[DataTable] `staged` needs `editable`; ignored.");
|
|
3469
|
+
}
|
|
3470
|
+
if (this.config.fillHandle && !this._cellGrid) {
|
|
3471
|
+
console.warn("[DataTable] `fillHandle` needs `staged`; ignored.");
|
|
3472
|
+
}
|
|
3473
|
+
if (Array.isArray(this.config.columnGroups) && this.config.columnGroups.length && this._crosstab()) {
|
|
3474
|
+
console.warn("[DataTable] `columnGroups` cannot be combined with `crosstab` (use crosstab.headerRows); ignored.");
|
|
3475
|
+
}
|
|
773
3476
|
{
|
|
774
3477
|
const sel = this.config.selectable;
|
|
775
3478
|
const asked = !!this.config.selectToggle || !!this.config.selectMode;
|
|
@@ -790,6 +3493,13 @@ var DataTable = class {
|
|
|
790
3493
|
this._pendingFilter = null;
|
|
791
3494
|
this._hidden = /* @__PURE__ */ new Set();
|
|
792
3495
|
this._setHidden(this.config.hiddenColumns);
|
|
3496
|
+
this._order = null;
|
|
3497
|
+
this._askedOrder = null;
|
|
3498
|
+
this._askedUnchecked = false;
|
|
3499
|
+
if (!this._setOrder(this.config.columnOrder, true)) {
|
|
3500
|
+
console.warn("[DataTable] columnOrder is not an order of the columns; ignored.");
|
|
3501
|
+
}
|
|
3502
|
+
this._reorder = this.config.reorderable ? new ColumnReorder(this) : null;
|
|
793
3503
|
this._activeKey = this.config.activeRow == null ? null : String(this.config.activeRow);
|
|
794
3504
|
this._error = null;
|
|
795
3505
|
this._virt = null;
|
|
@@ -891,6 +3601,9 @@ var DataTable = class {
|
|
|
891
3601
|
this._setHidden(blob.hidden);
|
|
892
3602
|
applied = true;
|
|
893
3603
|
}
|
|
3604
|
+
if (Array.isArray(blob.order) && this._setOrder(blob.order)) {
|
|
3605
|
+
applied = true;
|
|
3606
|
+
}
|
|
894
3607
|
if (blob.colWidths && typeof blob.colWidths === "object") {
|
|
895
3608
|
this._colWidths = {};
|
|
896
3609
|
const shift = this._lead().select - (Number(blob.colWidthsLead) || 0);
|
|
@@ -911,7 +3624,7 @@ var DataTable = class {
|
|
|
911
3624
|
/** Identity of the current column set — restored widths/sort are
|
|
912
3625
|
* keyed by position, so a schema change invalidates them. */
|
|
913
3626
|
_colSig() {
|
|
914
|
-
return (this.config.headers || []).join("") + (this.config.showRowNumbers ? "|#" : "") + (this._hidden?.size ? `|-${this.getHiddenColumns().join(",")}` : "");
|
|
3627
|
+
return (this.config.headers || []).join("") + (this.config.showRowNumbers ? "|#" : "") + (this._hidden?.size ? `|-${this.getHiddenColumns().join(",")}` : "") + (this._columnOrder() ? `|>${this._columnOrder().join(",")}` : "") + (this._colGroups()?.sig() || "");
|
|
915
3628
|
}
|
|
916
3629
|
/** Snapshot the persistable slice of state to the project store.
|
|
917
3630
|
* No-op unless `persistKey` is set. Debounced inside the store. */
|
|
@@ -930,7 +3643,9 @@ var DataTable = class {
|
|
|
930
3643
|
...this.config.persistPage ? { offset: this._state.offset } : {},
|
|
931
3644
|
// STORY-0012. Only for a table that hides columns or lets them be
|
|
932
3645
|
// hidden.
|
|
933
|
-
...this._columnsOn() ? { hidden: this.getHiddenColumns() } : {}
|
|
3646
|
+
...this._columnsOn() ? { hidden: this.getHiddenColumns() } : {},
|
|
3647
|
+
// STORY-0086. Only for a table that orders its columns.
|
|
3648
|
+
...this._orderOn() ? { order: this.getColumnOrder() } : {}
|
|
934
3649
|
});
|
|
935
3650
|
}
|
|
936
3651
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -954,7 +3669,28 @@ var DataTable = class {
|
|
|
954
3669
|
/** The cells a drawn row has: the shown columns, and the row number. */
|
|
955
3670
|
_domColCount() {
|
|
956
3671
|
const n = Math.max(0, (this.config.headers?.length || 0) - this._hidden.size);
|
|
957
|
-
return n + this._lead().all;
|
|
3672
|
+
return n + this._lead().all + (this._colGroups()?.placeholderCount() || 0) + (this._crosstab()?.extraColumns() || 0);
|
|
3673
|
+
}
|
|
3674
|
+
/** STORY-0066. The column groups (column_groups.js) while `columnGroups`
|
|
3675
|
+
* holds any; null otherwise. */
|
|
3676
|
+
_colGroups() {
|
|
3677
|
+
const spec = this.config.columnGroups;
|
|
3678
|
+
if (!Array.isArray(spec) || !spec.length) return null;
|
|
3679
|
+
if (this._crosstab()) return null;
|
|
3680
|
+
return this._columnGroups ||= new ColumnGroups(this);
|
|
3681
|
+
}
|
|
3682
|
+
/** STORY-0091. The crosstab layout (crosstab.js) while `crosstab` is an
|
|
3683
|
+
* object; null otherwise. */
|
|
3684
|
+
_crosstab() {
|
|
3685
|
+
const spec = this.config.crosstab;
|
|
3686
|
+
if (!spec || typeof spec !== "object") return null;
|
|
3687
|
+
return this._crosstabLayout ||= new Crosstab(this);
|
|
3688
|
+
}
|
|
3689
|
+
/** STORY-0066. The structural edits (table_structure.js) while any of
|
|
3690
|
+
* their options is set; null otherwise. */
|
|
3691
|
+
_structure() {
|
|
3692
|
+
if (!STRUCTURE_OPTIONS.some((k) => typeof this.config[k] === "function")) return null;
|
|
3693
|
+
return this._tableStructure ||= new TableStructure(this);
|
|
958
3694
|
}
|
|
959
3695
|
/** STORY-0016. The drawn columns in front of the data: the select column
|
|
960
3696
|
* (`select`, 0 or 1, always first) and the row number. */
|
|
@@ -962,24 +3698,30 @@ var DataTable = class {
|
|
|
962
3698
|
const select = this._gutter ? 1 : 0;
|
|
963
3699
|
return { select, all: select + (this.config.showRowNumbers ? 1 : 0) };
|
|
964
3700
|
}
|
|
3701
|
+
/** The data columns drawn, in the order drawn (STORY-0086): the order
|
|
3702
|
+
* without the hidden ones. */
|
|
3703
|
+
_shownColumns() {
|
|
3704
|
+
return this._rowColumns(this.config.headers?.length || 0);
|
|
3705
|
+
}
|
|
3706
|
+
/** The data indices of a row of `count` cells to draw, in order: the
|
|
3707
|
+
* shown columns, and any cell past the headers after them. */
|
|
3708
|
+
_rowColumns(count) {
|
|
3709
|
+
return cellOrder(this._columnOrder(), count).filter((c) => !this._isHidden(c));
|
|
3710
|
+
}
|
|
965
3711
|
/** A data column's drawn index (row number included), or -1 when hidden. */
|
|
966
3712
|
_domIdxOf(colIdx) {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
return
|
|
3713
|
+
const slot = this._colGroups()?.slotOf(colIdx) ?? -1;
|
|
3714
|
+
if (slot >= 0) return this._lead().all + slot;
|
|
3715
|
+
const at = this._shownColumns().indexOf(colIdx);
|
|
3716
|
+
return at < 0 ? -1 : this._lead().all + at;
|
|
971
3717
|
}
|
|
972
3718
|
/** A drawn cell's data column, or null for the row number. */
|
|
973
3719
|
_dataIdxOfDom(domIdx) {
|
|
974
|
-
|
|
3720
|
+
const i = domIdx - this._lead().all;
|
|
975
3721
|
if (i < 0) return null;
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
if (i === 0) return c;
|
|
980
|
-
i--;
|
|
981
|
-
}
|
|
982
|
-
return null;
|
|
3722
|
+
const slot = this._colGroups()?.slotAt(i);
|
|
3723
|
+
if (slot !== void 0) return slot;
|
|
3724
|
+
return this._shownColumns()[i] ?? null;
|
|
983
3725
|
}
|
|
984
3726
|
/** The columns not drawn, by data index, ascending. */
|
|
985
3727
|
getHiddenColumns() {
|
|
@@ -1010,12 +3752,23 @@ var DataTable = class {
|
|
|
1010
3752
|
* that stay drawn to their new places, and tell whoever listens. */
|
|
1011
3753
|
_applyHidden(next) {
|
|
1012
3754
|
const before = this.getHiddenColumns();
|
|
3755
|
+
this._carryWidths(() => this._setHidden([...next]));
|
|
3756
|
+
const after = this.getHiddenColumns();
|
|
3757
|
+
if (before.join() === after.join()) return;
|
|
3758
|
+
this._savePersisted();
|
|
3759
|
+
if (this._wrapperEl) this.render();
|
|
3760
|
+
this._columnsChanged();
|
|
3761
|
+
}
|
|
3762
|
+
/** STORY-0012/0086. Run `change` (a new hidden set or order), carrying
|
|
3763
|
+
* the drawn widths of the columns drawn before and after to their new
|
|
3764
|
+
* places. */
|
|
3765
|
+
_carryWidths(change) {
|
|
1013
3766
|
const n = this.config.headers?.length || 0;
|
|
1014
3767
|
const old = this._colWidths;
|
|
1015
3768
|
const kept = this._colWidthsSig === this._colSig();
|
|
1016
3769
|
const domBefore = [];
|
|
1017
3770
|
for (let c = 0; c < n; c++) domBefore[c] = this._domIdxOf(c);
|
|
1018
|
-
|
|
3771
|
+
change();
|
|
1019
3772
|
if (kept) {
|
|
1020
3773
|
const widths = {};
|
|
1021
3774
|
for (let i = 0; i < this._lead().all; i++) if (old[i] != null) widths[i] = old[i];
|
|
@@ -1027,22 +3780,101 @@ var DataTable = class {
|
|
|
1027
3780
|
this._colWidths = widths;
|
|
1028
3781
|
this._colWidthsSig = this._colSig();
|
|
1029
3782
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
3783
|
+
}
|
|
3784
|
+
/** `onColumnsChange(hidden, {hidden, order})`, after a change of either. */
|
|
3785
|
+
_columnsChanged() {
|
|
3786
|
+
const hidden = this.getHiddenColumns();
|
|
1034
3787
|
try {
|
|
1035
|
-
this.config.onColumnsChange?.(
|
|
3788
|
+
this.config.onColumnsChange?.(hidden, { hidden, order: this.getColumnOrder() });
|
|
1036
3789
|
} catch (err) {
|
|
1037
3790
|
console.error("[DataTable] onColumnsChange threw", err);
|
|
1038
3791
|
}
|
|
1039
3792
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
3793
|
+
// ─────────────────────────────────────────────────────────────────
|
|
3794
|
+
// STORY-0086 — the order the columns are drawn in (column_order.js)
|
|
3795
|
+
// ─────────────────────────────────────────────────────────────────
|
|
3796
|
+
/** Whether this table orders its columns at all (and so saves the order). */
|
|
3797
|
+
_orderOn() {
|
|
3798
|
+
return !!this.config.reorderable || Array.isArray(this.config.columnOrder) || !!this._orderTouched;
|
|
3799
|
+
}
|
|
3800
|
+
/** Take `list` as the order (`asked`: it is `columnOrder`, kept apart as
|
|
3801
|
+
* the fallback). Refused (false, nothing changed) when it is no
|
|
3802
|
+
* permutation of the columns; a table with no headers yet keeps it for
|
|
3803
|
+
* the headers `setData` brings, and `_checkAskedOrder` checks it then. */
|
|
3804
|
+
_setOrder(list, asked = false) {
|
|
3805
|
+
const n = this.config.headers?.length || 0;
|
|
3806
|
+
if (list != null && (!Array.isArray(list) || n && !isColumnOrder(list, n))) return false;
|
|
3807
|
+
this._order = list == null ? null : list.map(Number);
|
|
3808
|
+
if (asked) {
|
|
3809
|
+
this._askedOrder = this._order;
|
|
3810
|
+
this._askedUnchecked = !!this._order && !n;
|
|
3811
|
+
}
|
|
3812
|
+
return true;
|
|
3813
|
+
}
|
|
3814
|
+
/** The first headers have come: a `columnOrder` taken before them, unchecked,
|
|
3815
|
+
* that is no order of them is dropped with the same warning a table with
|
|
3816
|
+
* headers gives at once — once, as the check is made once. */
|
|
3817
|
+
_checkAskedOrder() {
|
|
3818
|
+
const n = this.config.headers?.length || 0;
|
|
3819
|
+
if (!this._askedUnchecked || !n) return;
|
|
3820
|
+
this._askedUnchecked = false;
|
|
3821
|
+
if (isColumnOrder(this._askedOrder, n)) return;
|
|
3822
|
+
console.warn("[DataTable] columnOrder is not an order of the columns; ignored.");
|
|
3823
|
+
if (this._order === this._askedOrder) this._order = null;
|
|
3824
|
+
this._askedOrder = null;
|
|
3825
|
+
}
|
|
3826
|
+
/** The order in force, or null for the natural one. One rule, whether the
|
|
3827
|
+
* headers were known when an order came or not: the first of the live
|
|
3828
|
+
* order (dragged, moved or restored) and `columnOrder` that is an order
|
|
3829
|
+
* of today's columns — so a saved order of the wrong length never
|
|
3830
|
+
* displaces `columnOrder`. */
|
|
3831
|
+
_columnOrder() {
|
|
3832
|
+
const n = this.config.headers?.length || 0;
|
|
3833
|
+
for (const o of [this._order, this._askedOrder]) {
|
|
3834
|
+
if (o && isColumnOrder(o, n)) return o;
|
|
3835
|
+
}
|
|
3836
|
+
return null;
|
|
3837
|
+
}
|
|
3838
|
+
/**
|
|
3839
|
+
* Every data column's index, hidden ones included, in the order drawn.
|
|
3840
|
+
* @returns {number[]}
|
|
3841
|
+
*/
|
|
3842
|
+
getColumnOrder() {
|
|
3843
|
+
return (this._columnOrder() || naturalOrder(this.config.headers?.length || 0)).slice();
|
|
3844
|
+
}
|
|
3845
|
+
/**
|
|
3846
|
+
* Move the column at position `from` of `getColumnOrder()` to position
|
|
3847
|
+
* `to`, the others closing up behind it: from the natural order,
|
|
3848
|
+
* `moveColumn(3, 1)` draws 0, 3, 1, 2. The widths stay with their columns.
|
|
3849
|
+
* Saved under `persistKey`, and `onColumnsChange` is told.
|
|
3850
|
+
*
|
|
3851
|
+
* @param {number} from
|
|
3852
|
+
* @param {number} to
|
|
3853
|
+
* @returns {boolean} whether the order changed
|
|
3854
|
+
*/
|
|
3855
|
+
moveColumn(from, to) {
|
|
3856
|
+
const order = this.getColumnOrder();
|
|
3857
|
+
const n = order.length;
|
|
3858
|
+
if (![from, to].every((i) => Number.isInteger(i) && i >= 0 && i < n) || from === to) return false;
|
|
3859
|
+
this._applyOrder(moveInOrder(order, from, to));
|
|
3860
|
+
return true;
|
|
3861
|
+
}
|
|
3862
|
+
/** Swap in a new order (a permutation, or null), widths carried, and
|
|
3863
|
+
* tell whoever listens. */
|
|
3864
|
+
_applyOrder(next) {
|
|
3865
|
+
const before = this.getColumnOrder().join();
|
|
3866
|
+
this._orderTouched = true;
|
|
3867
|
+
this._carryWidths(() => this._setOrder(next));
|
|
3868
|
+
if (this.getColumnOrder().join() === before) return;
|
|
3869
|
+
this._savePersisted();
|
|
3870
|
+
if (this._wrapperEl) this.render();
|
|
3871
|
+
this._columnsChanged();
|
|
3872
|
+
}
|
|
3873
|
+
/** `columnChooser`'s items: one checkable item a column. */
|
|
3874
|
+
_columnChooserItems() {
|
|
1043
3875
|
const headers = this.config.headers || [];
|
|
1044
3876
|
const lastShown = headers.length - this._hidden.size <= 1;
|
|
1045
|
-
|
|
3877
|
+
return headers.map((h, c) => {
|
|
1046
3878
|
const shown = !this._isHidden(c);
|
|
1047
3879
|
return {
|
|
1048
3880
|
label: String(h ?? ""),
|
|
@@ -1053,7 +3885,70 @@ var DataTable = class {
|
|
|
1053
3885
|
onChoose: () => this.setColumnVisible(c, !shown)
|
|
1054
3886
|
};
|
|
1055
3887
|
});
|
|
1056
|
-
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* STORY-0003. The header's right-click menu for a press on `target`: the
|
|
3891
|
+
* consumer's `headerMenu` items for the data column under it, then — with
|
|
3892
|
+
* `columnChooser` — the chooser's items behind a separator. Empty when
|
|
3893
|
+
* there is nothing to show.
|
|
3894
|
+
*/
|
|
3895
|
+
_headerMenuItems(target) {
|
|
3896
|
+
const items = [];
|
|
3897
|
+
const hook = this.config.headerMenu;
|
|
3898
|
+
const colIdx = this._headerColumnAt(target);
|
|
3899
|
+
if (typeof hook === "function" && colIdx !== null) {
|
|
3900
|
+
let asked = null;
|
|
3901
|
+
try {
|
|
3902
|
+
asked = hook(colIdx, {
|
|
3903
|
+
colIdx,
|
|
3904
|
+
header: this.config.headers?.[colIdx],
|
|
3905
|
+
hiddenColumns: this.getHiddenColumns(),
|
|
3906
|
+
table: this
|
|
3907
|
+
});
|
|
3908
|
+
} catch (err) {
|
|
3909
|
+
console.error("[DataTable] headerMenu threw", err);
|
|
3910
|
+
}
|
|
3911
|
+
for (const item of Array.isArray(asked) ? asked.filter(Boolean) : []) {
|
|
3912
|
+
if (item.separator) {
|
|
3913
|
+
items.push({ separator: true });
|
|
3914
|
+
continue;
|
|
3915
|
+
}
|
|
3916
|
+
items.push({
|
|
3917
|
+
...item,
|
|
3918
|
+
onChoose: () => {
|
|
3919
|
+
try {
|
|
3920
|
+
this.config.onHeaderMenuAction?.(item.action, colIdx);
|
|
3921
|
+
} catch (err) {
|
|
3922
|
+
console.error("[DataTable] onHeaderMenuAction threw", err);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
});
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
if (this._reorder && colIdx !== null) {
|
|
3929
|
+
const moves = this._reorder.menuItems(colIdx);
|
|
3930
|
+
if (moves.length && items.length) items.push({ separator: true });
|
|
3931
|
+
items.push(...moves);
|
|
3932
|
+
}
|
|
3933
|
+
const structural = this._structure()?.headerItems(colIdx) || [];
|
|
3934
|
+
if (structural.length) {
|
|
3935
|
+
if (items.length) items.push({ separator: true });
|
|
3936
|
+
items.push(...structural);
|
|
3937
|
+
}
|
|
3938
|
+
if (this.config.columnChooser) {
|
|
3939
|
+
if (items.length) items.push({ separator: true });
|
|
3940
|
+
items.push(...this._columnChooserItems());
|
|
3941
|
+
}
|
|
3942
|
+
return items;
|
|
3943
|
+
}
|
|
3944
|
+
/** The data column of a header cell (the header row, or the filter row
|
|
3945
|
+
* under it), or null off one — the row number, the select column. */
|
|
3946
|
+
_headerColumnAt(target) {
|
|
3947
|
+
const cell = target?.closest?.("th, td");
|
|
3948
|
+
const tr = cell?.parentNode;
|
|
3949
|
+
if (!tr || tr.parentNode?.tagName !== "THEAD") return null;
|
|
3950
|
+
if (tr.children.length !== this._domColCount()) return null;
|
|
3951
|
+
return this._dataIdxOfDom(Array.prototype.indexOf.call(tr.children, cell));
|
|
1057
3952
|
}
|
|
1058
3953
|
/** STORY-0010 `persistPage`: the client page changed, so save it. */
|
|
1059
3954
|
_savePage() {
|
|
@@ -1089,18 +3984,26 @@ var DataTable = class {
|
|
|
1089
3984
|
this._setHidden(updates.hiddenColumns);
|
|
1090
3985
|
if (this.getHiddenColumns().join() !== before) {
|
|
1091
3986
|
this._savePersisted();
|
|
1092
|
-
|
|
1093
|
-
this.config.onColumnsChange?.(this.getHiddenColumns());
|
|
1094
|
-
} catch (err) {
|
|
1095
|
-
console.error("[DataTable] onColumnsChange threw", err);
|
|
1096
|
-
}
|
|
3987
|
+
this._columnsChanged();
|
|
1097
3988
|
}
|
|
1098
3989
|
} else if (updates.headers && this._hidden.size) {
|
|
1099
3990
|
this._setHidden([...this._hidden]);
|
|
1100
3991
|
}
|
|
3992
|
+
if (Object.prototype.hasOwnProperty.call(updates, "columnOrder")) {
|
|
3993
|
+
const before = this.getColumnOrder().join();
|
|
3994
|
+
if (!this._setOrder(updates.columnOrder, true)) {
|
|
3995
|
+
console.warn("[DataTable] columnOrder is not an order of the columns; ignored.");
|
|
3996
|
+
} else if (this.getColumnOrder().join() !== before) {
|
|
3997
|
+
this._savePersisted();
|
|
3998
|
+
this._columnsChanged();
|
|
3999
|
+
}
|
|
4000
|
+
} else if (updates.headers) {
|
|
4001
|
+
this._checkAskedOrder();
|
|
4002
|
+
}
|
|
1101
4003
|
if (updates.rows) {
|
|
1102
4004
|
this._state.selected.clear();
|
|
1103
4005
|
this._state.anchorIndex = null;
|
|
4006
|
+
this._cellGrid?.newRows();
|
|
1104
4007
|
if (this._state.offset >= updates.rows.length) {
|
|
1105
4008
|
this._state.offset = 0;
|
|
1106
4009
|
}
|
|
@@ -1167,6 +4070,10 @@ var DataTable = class {
|
|
|
1167
4070
|
* @param {number[]} indices - Row indices to select
|
|
1168
4071
|
*/
|
|
1169
4072
|
setSelection(indices) {
|
|
4073
|
+
if (this._cellGrid) {
|
|
4074
|
+
this._cellGrid.selectRows(indices);
|
|
4075
|
+
return;
|
|
4076
|
+
}
|
|
1170
4077
|
this._state.selected.clear();
|
|
1171
4078
|
for (const idx of indices) {
|
|
1172
4079
|
if (idx >= 0 && idx < this.config.rows.length) {
|
|
@@ -1194,9 +4101,9 @@ var DataTable = class {
|
|
|
1194
4101
|
*
|
|
1195
4102
|
* @param {*} key
|
|
1196
4103
|
*/
|
|
1197
|
-
setActiveRow(
|
|
1198
|
-
this._activeKey =
|
|
1199
|
-
this.config.activeRow =
|
|
4104
|
+
setActiveRow(key3) {
|
|
4105
|
+
this._activeKey = key3 == null ? null : String(key3);
|
|
4106
|
+
this.config.activeRow = key3 ?? null;
|
|
1200
4107
|
const tbody = this._tbodyEl;
|
|
1201
4108
|
if (!tbody) return;
|
|
1202
4109
|
for (const tr of tbody.children) {
|
|
@@ -1247,16 +4154,112 @@ var DataTable = class {
|
|
|
1247
4154
|
* Clear selection
|
|
1248
4155
|
*/
|
|
1249
4156
|
clearSelection() {
|
|
4157
|
+
if (this._cellGrid) {
|
|
4158
|
+
this._cellGrid.clear();
|
|
4159
|
+
return;
|
|
4160
|
+
}
|
|
1250
4161
|
this._state.selected.clear();
|
|
1251
4162
|
this._state.anchorIndex = null;
|
|
1252
4163
|
this._updateRowSelection();
|
|
1253
4164
|
this._notifySelectionChange();
|
|
1254
4165
|
}
|
|
4166
|
+
// ─────────────────────────────────────────────────────────────────
|
|
4167
|
+
// STORY-0003 — cell mode (`selectionMode: 'cells'`), see cell_grid.js
|
|
4168
|
+
// ─────────────────────────────────────────────────────────────────
|
|
4169
|
+
/**
|
|
4170
|
+
* The cell selection: `{anchor, focus, range}`. `anchor` and `focus` (the
|
|
4171
|
+
* cursor) are `{row, col, rowIdx}` — `row` the position in the order
|
|
4172
|
+
* shown, `col` the data column, `rowIdx` the original row index — and
|
|
4173
|
+
* `range` is `{top, bottom, left, right, rows, columns}`: positions,
|
|
4174
|
+
* data columns at its edges, and the original row indices and data
|
|
4175
|
+
* columns it covers. All null with nothing selected. Null outside cell
|
|
4176
|
+
* mode.
|
|
4177
|
+
*/
|
|
4178
|
+
getCellSelection() {
|
|
4179
|
+
return this._cellGrid ? this._cellGrid.get() : null;
|
|
4180
|
+
}
|
|
4181
|
+
/**
|
|
4182
|
+
* Put the cursor on a cell (`row` a position in the order shown, `col` a
|
|
4183
|
+
* data column) — with `anchor`, select the range from there. `null`
|
|
4184
|
+
* clears. The row is brought into view. A no-op outside cell mode.
|
|
4185
|
+
*
|
|
4186
|
+
* @param {{row:number, col:number}|null} focus
|
|
4187
|
+
* @param {{row:number, col:number}} [anchor]
|
|
4188
|
+
*/
|
|
4189
|
+
setCellSelection(focus, anchor = null) {
|
|
4190
|
+
this._cellGrid?.set(focus, anchor);
|
|
4191
|
+
}
|
|
4192
|
+
/** Open the editor on a cell (position, data column), as F2 would.
|
|
4193
|
+
* Returns whether one opened. */
|
|
4194
|
+
editCell(row, col) {
|
|
4195
|
+
return !!this._cellGrid?.open(row, col);
|
|
4196
|
+
}
|
|
4197
|
+
/** Whether a cell editor is open. */
|
|
4198
|
+
isEditing() {
|
|
4199
|
+
return !!this._cellGrid?.isEditing();
|
|
4200
|
+
}
|
|
4201
|
+
// ── STORY-0088 `staged` ──────────────────────────────────────────────
|
|
4202
|
+
/**
|
|
4203
|
+
* Send every staged cell to `onCommit({edits})`, in one call. Resolves
|
|
4204
|
+
* `{ok: true, committed}` (the staged cells and the undo ring are
|
|
4205
|
+
* cleared), or `{ok: false, errors}` (the cells refused are marked and
|
|
4206
|
+
* every staged value stays). An open editor's value is staged first.
|
|
4207
|
+
* Without `staged`, resolves `{ok: false, errors: []}`.
|
|
4208
|
+
*
|
|
4209
|
+
* @returns {Promise<{ok: boolean, committed?: number, errors?: Array<{rowIdx:number, colIdx:number, error:string}>}>}
|
|
4210
|
+
*/
|
|
4211
|
+
commit() {
|
|
4212
|
+
const st = this._cellGrid?.staged;
|
|
4213
|
+
return st ? st.commit() : Promise.resolve({ ok: false, errors: [] });
|
|
4214
|
+
}
|
|
4215
|
+
/** Drop every staged cell, its marks and the undo ring. Returns whether
|
|
4216
|
+
* there was anything to drop. */
|
|
4217
|
+
discard() {
|
|
4218
|
+
return !!this._cellGrid?.staged?.discard();
|
|
4219
|
+
}
|
|
4220
|
+
/** Take the last staged edit back (Ctrl+Z). Returns whether it moved. */
|
|
4221
|
+
undo() {
|
|
4222
|
+
return !!this._cellGrid?.staged?.undo();
|
|
4223
|
+
}
|
|
4224
|
+
/** Put the last edit taken back back again (Ctrl+Y). */
|
|
4225
|
+
redo() {
|
|
4226
|
+
return !!this._cellGrid?.staged?.redo();
|
|
4227
|
+
}
|
|
4228
|
+
/** Every staged cell, `{rowIdx, colIdx, value, previous}`, by row then
|
|
4229
|
+
* column. Empty without `staged`. */
|
|
4230
|
+
getStagedEdits() {
|
|
4231
|
+
return this._cellGrid?.staged ? this._cellGrid.staged.edits() : [];
|
|
4232
|
+
}
|
|
4233
|
+
/**
|
|
4234
|
+
* STORY-0089. Fill from the cell range to the cell `to` (`{row, col}`:
|
|
4235
|
+
* position in the order shown, data column), as dragging the fill handle
|
|
4236
|
+
* there does: staged as ONE undo entry, Ctrl's repeat as `forceCopy`.
|
|
4237
|
+
* Returns `{direction, count, staged, skipped}` —
|
|
4238
|
+
* `staged` the NUMBER of cells that changed, `skipped` the cells `editable` refused
|
|
4239
|
+
* (`{row, col, rowIdx}`) — or null when nothing was filled (no
|
|
4240
|
+
* `fillHandle`, no range, `to` inside it, an editor open, a commit out).
|
|
4241
|
+
*/
|
|
4242
|
+
fill(to, { forceCopy = false } = {}) {
|
|
4243
|
+
return this._cellGrid?.fill ? this._cellGrid.fill.fill(to, { forceCopy }) : null;
|
|
4244
|
+
}
|
|
4245
|
+
/** Why sort, filters and pages are held — a staged edit — or null; with
|
|
4246
|
+
* a reason, `what` is refused out loud. */
|
|
4247
|
+
_stagedRefusal(what) {
|
|
4248
|
+
const why = this._cellGrid?.staged?.freezeReason() || null;
|
|
4249
|
+
if (why) console.warn(`[DataTable] ${what} refused: ${why}`);
|
|
4250
|
+
return why;
|
|
4251
|
+
}
|
|
4252
|
+
/** STORY-0066. Open the inline renamer on a data column's header (needs
|
|
4253
|
+
* `onColumnRename`). Returns whether it opened. */
|
|
4254
|
+
renameColumn(colIdx) {
|
|
4255
|
+
return !!this._structure()?.rename(colIdx);
|
|
4256
|
+
}
|
|
1255
4257
|
/**
|
|
1256
4258
|
* Go to specific page
|
|
1257
4259
|
* @param {number} page - Page number (0-indexed)
|
|
1258
4260
|
*/
|
|
1259
4261
|
goToPage(page) {
|
|
4262
|
+
if (this._stagedRefusal("goToPage")) return;
|
|
1260
4263
|
if (this.config.dataSource) {
|
|
1261
4264
|
const size = this._sourcePageSize();
|
|
1262
4265
|
let p = Math.max(0, Math.floor(Number(page)) || 0);
|
|
@@ -1275,6 +4278,7 @@ var DataTable = class {
|
|
|
1275
4278
|
* @param {number} rowIndex - Row index
|
|
1276
4279
|
*/
|
|
1277
4280
|
goToRow(rowIndex) {
|
|
4281
|
+
if (this._stagedRefusal("goToRow")) return;
|
|
1278
4282
|
if (this.config.dataSource) {
|
|
1279
4283
|
const size = this._sourcePageSize();
|
|
1280
4284
|
return this.goToPage(Math.floor(Math.max(0, Math.floor(Number(rowIndex)) || 0) / size));
|
|
@@ -1291,6 +4295,7 @@ var DataTable = class {
|
|
|
1291
4295
|
* @param {boolean} [ascending] - Sort direction (toggles if same column)
|
|
1292
4296
|
*/
|
|
1293
4297
|
sortBy(colIndex, ascending) {
|
|
4298
|
+
if (this._stagedRefusal("sortBy")) return;
|
|
1294
4299
|
if (this._state.sortColumn === colIndex && ascending === void 0) {
|
|
1295
4300
|
this._state.sortAscending = !this._state.sortAscending;
|
|
1296
4301
|
} else {
|
|
@@ -1317,6 +4322,7 @@ var DataTable = class {
|
|
|
1317
4322
|
* Clear all column filters
|
|
1318
4323
|
*/
|
|
1319
4324
|
clearFilters() {
|
|
4325
|
+
if (this._stagedRefusal("clearFilters")) return;
|
|
1320
4326
|
this._state.filters.clear();
|
|
1321
4327
|
this._processedRows = null;
|
|
1322
4328
|
this._processedIndexMap = null;
|
|
@@ -1384,6 +4390,10 @@ var DataTable = class {
|
|
|
1384
4390
|
* Render the table
|
|
1385
4391
|
*/
|
|
1386
4392
|
render() {
|
|
4393
|
+
if (this._cellGrid?.isEditing()) {
|
|
4394
|
+
this._cellGrid.renderWaiting = true;
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
1387
4397
|
const activeFilterColIdx = this._getActiveFilterColIdx();
|
|
1388
4398
|
const doc = this.container.ownerDocument;
|
|
1389
4399
|
const hadTableFocus = !!this._tableEl && doc?.activeElement === this._tableEl;
|
|
@@ -1436,7 +4446,7 @@ var DataTable = class {
|
|
|
1436
4446
|
if (this._error && rows.length > 0) {
|
|
1437
4447
|
this._wrapperEl.appendChild(this._createErrorBanner());
|
|
1438
4448
|
}
|
|
1439
|
-
const keepFilterRow = rows.length === 0 && !!this.config.dataSource && this.config.filterable;
|
|
4449
|
+
const keepFilterRow = rows.length === 0 && (!!this.config.dataSource && this.config.filterable || !!this._crosstab());
|
|
1440
4450
|
if (rows.length === 0 && !keepFilterRow) {
|
|
1441
4451
|
const { headers, showRowNumbers } = this.config;
|
|
1442
4452
|
const headerWrap = document.createElement("div");
|
|
@@ -1453,13 +4463,12 @@ var DataTable = class {
|
|
|
1453
4463
|
th.textContent = "#";
|
|
1454
4464
|
tr.appendChild(th);
|
|
1455
4465
|
}
|
|
1456
|
-
|
|
1457
|
-
if (this._isHidden(colIdx)) return;
|
|
4466
|
+
for (const colIdx of this._shownColumns()) {
|
|
1458
4467
|
const th = document.createElement("th");
|
|
1459
4468
|
th.className = this._columnTypes[colIdx] || "text";
|
|
1460
|
-
th.textContent =
|
|
4469
|
+
th.textContent = headers[colIdx];
|
|
1461
4470
|
tr.appendChild(th);
|
|
1462
|
-
}
|
|
4471
|
+
}
|
|
1463
4472
|
thead.appendChild(tr);
|
|
1464
4473
|
headerTable.appendChild(thead);
|
|
1465
4474
|
headerWrap.appendChild(headerTable);
|
|
@@ -1478,7 +4487,13 @@ var DataTable = class {
|
|
|
1478
4487
|
} else {
|
|
1479
4488
|
const fullTable = this._createTable();
|
|
1480
4489
|
this._tableEl = fullTable;
|
|
1481
|
-
const
|
|
4490
|
+
const crosstab = this._crosstab();
|
|
4491
|
+
const thead = crosstab ? null : fullTable.querySelector("thead");
|
|
4492
|
+
if (crosstab) {
|
|
4493
|
+
crosstab.decorate(fullTable);
|
|
4494
|
+
this._headerTableEl = null;
|
|
4495
|
+
this._headerWrapEl = null;
|
|
4496
|
+
}
|
|
1482
4497
|
if (thead) {
|
|
1483
4498
|
const headerWrap = document.createElement("div");
|
|
1484
4499
|
headerWrap.className = "twm-preview-table-header-wrap";
|
|
@@ -1500,16 +4515,27 @@ var DataTable = class {
|
|
|
1500
4515
|
this._wrapperEl.appendChild(tableWrap);
|
|
1501
4516
|
}
|
|
1502
4517
|
this._tableWrapEl = tableWrap;
|
|
1503
|
-
|
|
4518
|
+
this._colGroups()?.decorate(this._headerWrapEl, this._headerTableEl);
|
|
4519
|
+
this._structure()?.decorate(this._headerTableEl);
|
|
4520
|
+
if ((this.config.columnChooser || typeof this.config.headerMenu === "function" || this._reorder || this._structure()?.hasHeaderItems()) && this._headerTableEl) {
|
|
1504
4521
|
const head = this._headerTableEl;
|
|
1505
4522
|
const onHeadMenu = (ev) => {
|
|
4523
|
+
const items = this._headerMenuItems(ev.target);
|
|
4524
|
+
if (!items.length) return;
|
|
1506
4525
|
ev.preventDefault();
|
|
1507
4526
|
ev.stopPropagation();
|
|
1508
|
-
setTimeout(() => this.
|
|
4527
|
+
setTimeout(() => this._showContextMenu(
|
|
4528
|
+
ev.clientX,
|
|
4529
|
+
ev.clientY,
|
|
4530
|
+
{ ctx: null, items, copy: false }
|
|
4531
|
+
), 0);
|
|
1509
4532
|
};
|
|
1510
4533
|
head.addEventListener("contextmenu", onHeadMenu);
|
|
1511
4534
|
this._disposers.push(() => head.removeEventListener("contextmenu", onHeadMenu));
|
|
1512
4535
|
}
|
|
4536
|
+
if (this._reorder && this._headerTableEl) {
|
|
4537
|
+
this._reorder.install(this._headerTableEl.querySelector("thead > tr"));
|
|
4538
|
+
}
|
|
1513
4539
|
if (this._paginationEl && this.config.pagerPosition === "bottom") {
|
|
1514
4540
|
this._paginationEl.classList.add("twm-pagination-controls--bottom");
|
|
1515
4541
|
this._paginationEl.style.borderBottom = "0";
|
|
@@ -1522,6 +4548,7 @@ var DataTable = class {
|
|
|
1522
4548
|
if (this._pagingEl) this._wrapperEl.appendChild(this._pagingEl);
|
|
1523
4549
|
}
|
|
1524
4550
|
this.container.appendChild(this._wrapperEl);
|
|
4551
|
+
this._crosstab()?.install();
|
|
1525
4552
|
if (this.config.scrollLock && typeof this.config.scrollLock.attach === "function") {
|
|
1526
4553
|
this._disposers.push(this.config.scrollLock.attach(tableWrap));
|
|
1527
4554
|
}
|
|
@@ -1567,7 +4594,7 @@ var DataTable = class {
|
|
|
1567
4594
|
this._installHeaderScrollSync();
|
|
1568
4595
|
if (rows.length > 0) this._installColumnResizers();
|
|
1569
4596
|
}
|
|
1570
|
-
if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly || typeof this.config.onRowActivate === "function" || typeof this.config.rowDetail === "function" || typeof this.config.onRowKeyDown === "function")) {
|
|
4597
|
+
if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly || typeof this.config.onRowActivate === "function" || typeof this.config.rowDetail === "function" || typeof this.config.onRowKeyDown === "function" || this._cellGrid)) {
|
|
1571
4598
|
this._installInteractions();
|
|
1572
4599
|
}
|
|
1573
4600
|
if (activeFilterColIdx !== null) {
|
|
@@ -1609,12 +4636,11 @@ var DataTable = class {
|
|
|
1609
4636
|
return;
|
|
1610
4637
|
}
|
|
1611
4638
|
const { headers, rows } = this.config;
|
|
1612
|
-
const
|
|
1613
|
-
const matrix = [headers.filter(shown)];
|
|
4639
|
+
const matrix = [this._rowColumns(headers.length).map((c) => headers[c])];
|
|
1614
4640
|
for (const idx of indices) {
|
|
1615
4641
|
const row = rows[idx];
|
|
1616
4642
|
if (row) {
|
|
1617
|
-
matrix.push(row.map((
|
|
4643
|
+
matrix.push(this._rowColumns(row.length).map((c) => this._formatValue(row[c], c)));
|
|
1618
4644
|
}
|
|
1619
4645
|
}
|
|
1620
4646
|
const separator = format === "csv" ? "," : " ";
|
|
@@ -1645,12 +4671,11 @@ var DataTable = class {
|
|
|
1645
4671
|
this._notify("Download", "No data to download.", "warn");
|
|
1646
4672
|
return;
|
|
1647
4673
|
}
|
|
1648
|
-
const
|
|
1649
|
-
const lines = [headers.filter(shown).join(",")];
|
|
4674
|
+
const lines = [this._rowColumns(headers.length).map((c) => headers[c]).join(",")];
|
|
1650
4675
|
for (const row of rows) {
|
|
1651
|
-
lines.push(row.map(
|
|
1652
|
-
(
|
|
1653
|
-
).
|
|
4676
|
+
lines.push(this._rowColumns(row.length).map(
|
|
4677
|
+
(c) => this._csvEscape(this._formatValue(row[c], c))
|
|
4678
|
+
).join(","));
|
|
1654
4679
|
}
|
|
1655
4680
|
const csv = lines.join("\n");
|
|
1656
4681
|
const dialogs = this.config.host?.dialogs;
|
|
@@ -1698,6 +4723,8 @@ var DataTable = class {
|
|
|
1698
4723
|
this._teardown(false);
|
|
1699
4724
|
}
|
|
1700
4725
|
_teardown(emptyHost) {
|
|
4726
|
+
this._cellGrid?.dispose();
|
|
4727
|
+
this._tableStructure?.dispose();
|
|
1701
4728
|
autoDisposeUnwatch(this);
|
|
1702
4729
|
this._autoSeen = false;
|
|
1703
4730
|
if (this._autoTimer) {
|
|
@@ -1965,8 +4992,8 @@ var DataTable = class {
|
|
|
1965
4992
|
th.className = "data-table__filter-cell data-table__filter-cell--empty";
|
|
1966
4993
|
tr.appendChild(th);
|
|
1967
4994
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
4995
|
+
this._shownColumns().forEach((colIdx) => {
|
|
4996
|
+
const header = headers[colIdx];
|
|
1970
4997
|
const th = document.createElement("th");
|
|
1971
4998
|
th.className = "data-table__filter-cell";
|
|
1972
4999
|
const control = this._filterControlFor(colIdx, header);
|
|
@@ -2255,6 +5282,7 @@ var DataTable = class {
|
|
|
2255
5282
|
}, 200);
|
|
2256
5283
|
}
|
|
2257
5284
|
_onFilterInput(colIdx, value) {
|
|
5285
|
+
if (this._stagedRefusal("A filter")) return;
|
|
2258
5286
|
const empty = typeof value === "string" ? !value : this._isEmptyFilterValue(value);
|
|
2259
5287
|
if (!empty) {
|
|
2260
5288
|
this._state.filters.set(colIdx, value);
|
|
@@ -2420,6 +5448,7 @@ var DataTable = class {
|
|
|
2420
5448
|
const spacer = document.createElement("div");
|
|
2421
5449
|
spacer.style.flex = "1";
|
|
2422
5450
|
const handlePageChange = (newOffset) => {
|
|
5451
|
+
if (this._stagedRefusal("Turning the page")) return;
|
|
2423
5452
|
if (isServerSide) {
|
|
2424
5453
|
this._awaitWithSpinner(onPageChange(newOffset, pageSize));
|
|
2425
5454
|
} else {
|
|
@@ -2479,12 +5508,14 @@ var DataTable = class {
|
|
|
2479
5508
|
_createExportBtn() {
|
|
2480
5509
|
const name = this.config.exportFilename;
|
|
2481
5510
|
const filename = (typeof name === "function" ? name(this) : name) || "data.csv";
|
|
2482
|
-
|
|
5511
|
+
const btn = this._createPaginationBtn(
|
|
2483
5512
|
"download",
|
|
2484
5513
|
"Download visible rows as CSV",
|
|
2485
5514
|
false,
|
|
2486
5515
|
() => this.downloadCSV(filename)
|
|
2487
5516
|
);
|
|
5517
|
+
btn.classList.add("twm-pagination-btn--export");
|
|
5518
|
+
return btn;
|
|
2488
5519
|
}
|
|
2489
5520
|
/**
|
|
2490
5521
|
* The strip on a table with one page: nothing, as always, unless the export
|
|
@@ -2606,6 +5637,7 @@ var DataTable = class {
|
|
|
2606
5637
|
this._wrapperEl.appendChild(next);
|
|
2607
5638
|
}
|
|
2608
5639
|
this._pagingEl = next;
|
|
5640
|
+
this._cellGrid?.staged?.paintFreeze();
|
|
2609
5641
|
}
|
|
2610
5642
|
/**
|
|
2611
5643
|
* Call one of the consumer's paging callbacks. A Promise holds every other
|
|
@@ -2615,6 +5647,7 @@ var DataTable = class {
|
|
|
2615
5647
|
*/
|
|
2616
5648
|
_pagingCall(kind, fn, ...args) {
|
|
2617
5649
|
if (typeof fn !== "function" || this._pagingBusy) return;
|
|
5650
|
+
if (this._stagedRefusal(`paging ${kind}`)) return;
|
|
2618
5651
|
let result;
|
|
2619
5652
|
try {
|
|
2620
5653
|
result = fn(...args);
|
|
@@ -2645,6 +5678,7 @@ var DataTable = class {
|
|
|
2645
5678
|
_pagingMaybeLoadMore() {
|
|
2646
5679
|
const p = this._pagingConfig();
|
|
2647
5680
|
if (!p || !p.hasMore || this._pagingBusy) return;
|
|
5681
|
+
if (this._cellGrid?.staged?.freezeReason()) return;
|
|
2648
5682
|
if (this.config.dataSource && this._source?.inFlight) return;
|
|
2649
5683
|
const state = this.pagingState;
|
|
2650
5684
|
if (state.mode !== "scroll" || p.held >= state.hardLimit) return;
|
|
@@ -2791,6 +5825,10 @@ var DataTable = class {
|
|
|
2791
5825
|
const processedRows = isServerSide ? rows : this._getProcessedRows();
|
|
2792
5826
|
const offset = isServerSide ? configOffset || 0 : this._state.offset;
|
|
2793
5827
|
const pageRows = isServerSide || !this.config.pagination || this._pagingConfig() ? processedRows : processedRows.slice(offset, offset + pageSize);
|
|
5828
|
+
this._pageSpan = {
|
|
5829
|
+
start: pageRows === processedRows ? 0 : offset,
|
|
5830
|
+
count: pageRows.length
|
|
5831
|
+
};
|
|
2794
5832
|
const table = document.createElement("table");
|
|
2795
5833
|
table.className = this._tableClassName();
|
|
2796
5834
|
table.tabIndex = 0;
|
|
@@ -2803,8 +5841,8 @@ var DataTable = class {
|
|
|
2803
5841
|
th.textContent = "#";
|
|
2804
5842
|
headerRow.appendChild(th);
|
|
2805
5843
|
}
|
|
2806
|
-
|
|
2807
|
-
|
|
5844
|
+
this._shownColumns().forEach((colIdx) => {
|
|
5845
|
+
const header = headers[colIdx];
|
|
2808
5846
|
const th = document.createElement("th");
|
|
2809
5847
|
th.className = this._columnTypes[colIdx] || "text";
|
|
2810
5848
|
if (sortable) {
|
|
@@ -2884,7 +5922,8 @@ var DataTable = class {
|
|
|
2884
5922
|
tr.__rowIndex = globalIdx;
|
|
2885
5923
|
tr.__position = (this._positionBase || 0) + localIdx;
|
|
2886
5924
|
tr.className = "data-preview-row";
|
|
2887
|
-
if (this.
|
|
5925
|
+
if (this._cellGrid) tr.__cellPos = (this._pageSpan?.start || 0) + localIdx;
|
|
5926
|
+
if (this._state.selected.has(globalIdx) && !this._cellGrid) {
|
|
2888
5927
|
tr.classList.add("selected");
|
|
2889
5928
|
}
|
|
2890
5929
|
this._decorateRow(tr, row, globalIdx);
|
|
@@ -3176,6 +6215,7 @@ var DataTable = class {
|
|
|
3176
6215
|
const wanted = new Set(want);
|
|
3177
6216
|
for (const [i, tr] of v.map) {
|
|
3178
6217
|
if (!wanted.has(i)) {
|
|
6218
|
+
this._cellGrid?.rowLeaving(tr);
|
|
3179
6219
|
tr.remove();
|
|
3180
6220
|
this._removeDetailPieces(tr);
|
|
3181
6221
|
v.map.delete(i);
|
|
@@ -3386,10 +6426,10 @@ var DataTable = class {
|
|
|
3386
6426
|
const diff = this._diffOf(row, globalIdx);
|
|
3387
6427
|
const gap = diff?.status === "gap";
|
|
3388
6428
|
const changedCells = diff?.cells ? diff.cells instanceof Set ? diff.cells : new Set(diff.cells) : null;
|
|
3389
|
-
for (
|
|
3390
|
-
if (this._isHidden(colIdx)) continue;
|
|
6429
|
+
for (const colIdx of this._rowColumns(row.length)) {
|
|
3391
6430
|
const td = document.createElement("td");
|
|
3392
6431
|
td.className = this._columnTypes[colIdx] || "text";
|
|
6432
|
+
if (this._cellGrid) td.__colIdx = colIdx;
|
|
3393
6433
|
const value = row[colIdx];
|
|
3394
6434
|
let handled = false;
|
|
3395
6435
|
if (gap) {
|
|
@@ -3430,6 +6470,9 @@ var DataTable = class {
|
|
|
3430
6470
|
}
|
|
3431
6471
|
tr.appendChild(td);
|
|
3432
6472
|
}
|
|
6473
|
+
this._colGroups()?.fillRow(tr);
|
|
6474
|
+
this._crosstab()?.fillRow(tr);
|
|
6475
|
+
this._cellGrid?.decorateRow(tr);
|
|
3433
6476
|
}
|
|
3434
6477
|
/** Re-render ONE row in place, preserving everything around it.
|
|
3435
6478
|
*
|
|
@@ -3451,6 +6494,9 @@ var DataTable = class {
|
|
|
3451
6494
|
* - keyboard focus, because the focused element's position is recorded
|
|
3452
6495
|
* before the replace and restored after.
|
|
3453
6496
|
*
|
|
6497
|
+
* STORY-0003: on the row an open cell editor is in, the data is stored
|
|
6498
|
+
* now and the row is drawn when the editor closes (true is returned).
|
|
6499
|
+
*
|
|
3454
6500
|
* @param {number} index row index as tracked by `tr.__rowIndex`
|
|
3455
6501
|
* @param {any[]} row the new cell values
|
|
3456
6502
|
* @returns {boolean} false when the row is not currently rendered
|
|
@@ -3476,6 +6522,7 @@ var DataTable = class {
|
|
|
3476
6522
|
this.config.rows[index] = row;
|
|
3477
6523
|
}
|
|
3478
6524
|
if (this._virt) this._virtSetRow(index, row);
|
|
6525
|
+
if (this._cellGrid?.holdRow(tr, index)) return true;
|
|
3479
6526
|
const active = document.activeElement;
|
|
3480
6527
|
let focusedCol = -1;
|
|
3481
6528
|
if (active && tr.contains(active)) {
|
|
@@ -3498,7 +6545,7 @@ var DataTable = class {
|
|
|
3498
6545
|
cell.style.maxWidth = saved.maxWidth;
|
|
3499
6546
|
});
|
|
3500
6547
|
}
|
|
3501
|
-
tr.classList.toggle("selected", this._state?.selected?.has(index) === true);
|
|
6548
|
+
tr.classList.toggle("selected", this._state?.selected?.has(index) === true && !this._cellGrid);
|
|
3502
6549
|
this._decorateRow(tr, row, index);
|
|
3503
6550
|
if (this._detailOn()) this._redrawDetail(tr);
|
|
3504
6551
|
if (focusedCol >= 0 && tr.children[focusedCol]) {
|
|
@@ -3544,8 +6591,8 @@ var DataTable = class {
|
|
|
3544
6591
|
td.insertBefore(gap, td.firstChild);
|
|
3545
6592
|
return;
|
|
3546
6593
|
}
|
|
3547
|
-
const
|
|
3548
|
-
const open =
|
|
6594
|
+
const key3 = this._rowKey(row, rowIdx);
|
|
6595
|
+
const open = key3 !== null && this._expanded.has(key3);
|
|
3549
6596
|
const btn = document.createElement("button");
|
|
3550
6597
|
btn.type = "button";
|
|
3551
6598
|
btn.className = "material-symbols-outlined twm-dt-detail-toggle";
|
|
@@ -3567,14 +6614,14 @@ var DataTable = class {
|
|
|
3567
6614
|
const row = tr.__row;
|
|
3568
6615
|
const idx = tr.__rowIndex;
|
|
3569
6616
|
if (!this._rowHasDetail(row, idx)) return;
|
|
3570
|
-
const
|
|
3571
|
-
if (
|
|
3572
|
-
const pieces = this._buildDetailPieces(tr,
|
|
6617
|
+
const key3 = this._rowKey(row, idx);
|
|
6618
|
+
if (key3 === null || !this._expanded.has(key3)) return;
|
|
6619
|
+
const pieces = this._buildDetailPieces(tr, key3);
|
|
3573
6620
|
const parent = tr.parentNode;
|
|
3574
6621
|
if (!parent) return;
|
|
3575
6622
|
for (const p of pieces) parent.insertBefore(p, ref);
|
|
3576
6623
|
}
|
|
3577
|
-
_buildDetailPieces(tr,
|
|
6624
|
+
_buildDetailPieces(tr, key3) {
|
|
3578
6625
|
const n = this._domColCount();
|
|
3579
6626
|
const detailTr = document.createElement("tr");
|
|
3580
6627
|
detailTr.className = "twm-dt-detail-row";
|
|
@@ -3593,16 +6640,16 @@ var DataTable = class {
|
|
|
3593
6640
|
filler.setAttribute("aria-hidden", "true");
|
|
3594
6641
|
detailTr.__detailOf = tr;
|
|
3595
6642
|
tr.__detail = [detailTr, filler];
|
|
3596
|
-
this._drawDetail(tr, host,
|
|
6643
|
+
this._drawDetail(tr, host, key3);
|
|
3597
6644
|
const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
|
|
3598
6645
|
if (btn) btn.setAttribute("aria-controls", host.id);
|
|
3599
6646
|
return tr.__detail;
|
|
3600
6647
|
}
|
|
3601
|
-
_drawDetail(tr, host,
|
|
6648
|
+
_drawDetail(tr, host, key3) {
|
|
3602
6649
|
host.replaceChildren();
|
|
3603
6650
|
let out;
|
|
3604
6651
|
try {
|
|
3605
|
-
out = this.config.rowDetail(tr.__row, tr.__rowIndex, { host, key, table: this });
|
|
6652
|
+
out = this.config.rowDetail(tr.__row, tr.__rowIndex, { host, key: key3, table: this });
|
|
3606
6653
|
} catch (err) {
|
|
3607
6654
|
console.error("[DataTable] rowDetail threw", err);
|
|
3608
6655
|
out = err?.message || String(err);
|
|
@@ -3621,8 +6668,8 @@ var DataTable = class {
|
|
|
3621
6668
|
}
|
|
3622
6669
|
/** `updateRow`'s half: the detail follows the row's new data. */
|
|
3623
6670
|
_redrawDetail(tr) {
|
|
3624
|
-
const
|
|
3625
|
-
const want = this._rowHasDetail(tr.__row, tr.__rowIndex) &&
|
|
6671
|
+
const key3 = this._rowKey(tr.__row, tr.__rowIndex);
|
|
6672
|
+
const want = this._rowHasDetail(tr.__row, tr.__rowIndex) && key3 !== null && this._expanded.has(key3);
|
|
3626
6673
|
const pieces = tr.__detail;
|
|
3627
6674
|
if (!want) {
|
|
3628
6675
|
if (pieces) this._removeDetailPieces(tr);
|
|
@@ -3631,7 +6678,7 @@ var DataTable = class {
|
|
|
3631
6678
|
if (pieces) {
|
|
3632
6679
|
const host = pieces[0].querySelector(".twm-dt-detail");
|
|
3633
6680
|
if (host) {
|
|
3634
|
-
this._drawDetail(tr, host,
|
|
6681
|
+
this._drawDetail(tr, host, key3);
|
|
3635
6682
|
const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
|
|
3636
6683
|
if (btn) btn.setAttribute("aria-controls", host.id);
|
|
3637
6684
|
}
|
|
@@ -3644,27 +6691,27 @@ var DataTable = class {
|
|
|
3644
6691
|
_setRowElExpanded(tr, open, ev = null) {
|
|
3645
6692
|
if (!this._detailOn() || tr.__rowIndex === void 0) return false;
|
|
3646
6693
|
if (!this._rowHasDetail(tr.__row, tr.__rowIndex)) return false;
|
|
3647
|
-
const
|
|
3648
|
-
if (
|
|
3649
|
-
if (open) this._expanded.add(
|
|
3650
|
-
else this._expanded.delete(
|
|
3651
|
-
this._syncDetailOnScreen(tr,
|
|
6694
|
+
const key3 = this._rowKey(tr.__row, tr.__rowIndex);
|
|
6695
|
+
if (key3 === null || this._expanded.has(key3) === !!open) return false;
|
|
6696
|
+
if (open) this._expanded.add(key3);
|
|
6697
|
+
else this._expanded.delete(key3);
|
|
6698
|
+
this._syncDetailOnScreen(tr, key3);
|
|
3652
6699
|
this._afterDetailChange();
|
|
3653
6700
|
try {
|
|
3654
|
-
this.config.onRowDetailToggle?.(tr.__rowIndex, tr.__row, !!open,
|
|
6701
|
+
this.config.onRowDetailToggle?.(tr.__rowIndex, tr.__row, !!open, key3, ev);
|
|
3655
6702
|
} catch (err) {
|
|
3656
6703
|
console.error("[DataTable] onRowDetailToggle threw", err);
|
|
3657
6704
|
}
|
|
3658
6705
|
return true;
|
|
3659
6706
|
}
|
|
3660
6707
|
/** Make the row on screen agree with `_expanded` for its key. */
|
|
3661
|
-
_syncDetailOnScreen(tr,
|
|
3662
|
-
const open = this._expanded.has(
|
|
6708
|
+
_syncDetailOnScreen(tr, key3) {
|
|
6709
|
+
const open = this._expanded.has(key3);
|
|
3663
6710
|
const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
|
|
3664
6711
|
if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false");
|
|
3665
6712
|
if (open && !tr.__detail && tr.parentNode) {
|
|
3666
6713
|
const ref = tr.nextSibling;
|
|
3667
|
-
for (const p of this._buildDetailPieces(tr,
|
|
6714
|
+
for (const p of this._buildDetailPieces(tr, key3)) tr.parentNode.insertBefore(p, ref);
|
|
3668
6715
|
} else if (!open && tr.__detail) {
|
|
3669
6716
|
this._removeDetailPieces(tr);
|
|
3670
6717
|
}
|
|
@@ -3678,9 +6725,9 @@ var DataTable = class {
|
|
|
3678
6725
|
* measured height open, three rows until it has been measured. */
|
|
3679
6726
|
_detailHeightOf(row, rowIdx, base) {
|
|
3680
6727
|
if (!this._rowHasDetail(row, rowIdx)) return 0;
|
|
3681
|
-
const
|
|
3682
|
-
if (
|
|
3683
|
-
const h = this._detailHeights.get(
|
|
6728
|
+
const key3 = this._rowKey(row, rowIdx);
|
|
6729
|
+
if (key3 === null || !this._expanded.has(key3)) return 0;
|
|
6730
|
+
const h = this._detailHeights.get(key3);
|
|
3684
6731
|
return h > 0 ? h : base * 3;
|
|
3685
6732
|
}
|
|
3686
6733
|
/** Measure the open details among `trs` (client pixels, the zoom divided
|
|
@@ -3703,9 +6750,9 @@ var DataTable = class {
|
|
|
3703
6750
|
if (!d || !d.isConnected) continue;
|
|
3704
6751
|
const h = d.getBoundingClientRect().height / zoom;
|
|
3705
6752
|
if (!(h > 0)) continue;
|
|
3706
|
-
const
|
|
3707
|
-
if (Math.abs((this._detailHeights.get(
|
|
3708
|
-
this._detailHeights.set(
|
|
6753
|
+
const key3 = this._rowKey(tr.__row, tr.__rowIndex);
|
|
6754
|
+
if (Math.abs((this._detailHeights.get(key3) || 0) - h) > 0.5) {
|
|
6755
|
+
this._detailHeights.set(key3, h);
|
|
3709
6756
|
changed = true;
|
|
3710
6757
|
}
|
|
3711
6758
|
}
|
|
@@ -3720,9 +6767,9 @@ var DataTable = class {
|
|
|
3720
6767
|
* @param {boolean} [open=true]
|
|
3721
6768
|
* @returns {boolean} whether anything changed
|
|
3722
6769
|
*/
|
|
3723
|
-
setRowExpanded(
|
|
3724
|
-
if (
|
|
3725
|
-
const k = String(
|
|
6770
|
+
setRowExpanded(key3, open = true) {
|
|
6771
|
+
if (key3 == null) return false;
|
|
6772
|
+
const k = String(key3);
|
|
3726
6773
|
if (this._expanded.has(k) === !!open) return false;
|
|
3727
6774
|
if (open) this._expanded.add(k);
|
|
3728
6775
|
else this._expanded.delete(k);
|
|
@@ -3743,8 +6790,8 @@ var DataTable = class {
|
|
|
3743
6790
|
return true;
|
|
3744
6791
|
}
|
|
3745
6792
|
/** Whether the row whose key is `key` has its detail open. */
|
|
3746
|
-
isRowExpanded(
|
|
3747
|
-
return
|
|
6793
|
+
isRowExpanded(key3) {
|
|
6794
|
+
return key3 != null && this._expanded.has(String(key3));
|
|
3748
6795
|
}
|
|
3749
6796
|
/** The keys of the rows whose detail is open, as text. */
|
|
3750
6797
|
getExpandedRows() {
|
|
@@ -3755,12 +6802,12 @@ var DataTable = class {
|
|
|
3755
6802
|
for (const k of [...this._expanded]) this.setRowExpanded(k, false);
|
|
3756
6803
|
}
|
|
3757
6804
|
/** The `<tr>` on screen for a key, or null. */
|
|
3758
|
-
_rowElByKey(
|
|
6805
|
+
_rowElByKey(key3) {
|
|
3759
6806
|
const tbody = this._tbodyEl;
|
|
3760
6807
|
if (!tbody) return null;
|
|
3761
6808
|
for (const tr of tbody.children) {
|
|
3762
6809
|
if (tr.__rowIndex === void 0) continue;
|
|
3763
|
-
if (this._rowKey(tr.__row, tr.__rowIndex) ===
|
|
6810
|
+
if (this._rowKey(tr.__row, tr.__rowIndex) === key3) return tr;
|
|
3764
6811
|
}
|
|
3765
6812
|
return null;
|
|
3766
6813
|
}
|
|
@@ -4027,6 +7074,7 @@ var DataTable = class {
|
|
|
4027
7074
|
}
|
|
4028
7075
|
/** Go to the page at `offset` (paging back stops following). */
|
|
4029
7076
|
_sourceGoTo(offset) {
|
|
7077
|
+
if (this._stagedRefusal("Turning the page")) return void 0;
|
|
4030
7078
|
const next = Math.max(0, offset | 0);
|
|
4031
7079
|
if (this._following && next > 0) this.setFollow(false);
|
|
4032
7080
|
return this._sourceLoad({ offset: next, reason: "page" });
|
|
@@ -4300,6 +7348,7 @@ var DataTable = class {
|
|
|
4300
7348
|
if (!known && !hasPrev && !hasNext) return this._createExportOnlyStrip();
|
|
4301
7349
|
const maxJump = Number.isFinite(c.maxJumpOffset) ? c.maxJumpOffset : null;
|
|
4302
7350
|
const go = (newOffset) => {
|
|
7351
|
+
if (this._stagedRefusal("Turning the page")) return;
|
|
4303
7352
|
if (source) {
|
|
4304
7353
|
this._sourceGoTo(newOffset);
|
|
4305
7354
|
return;
|
|
@@ -4504,6 +7553,7 @@ var DataTable = class {
|
|
|
4504
7553
|
* value is whatever the column's control works in — text for the 0.4 box.
|
|
4505
7554
|
*/
|
|
4506
7555
|
setFilter(colIdx, value) {
|
|
7556
|
+
if (this._stagedRefusal("setFilter")) return;
|
|
4507
7557
|
const textBox = !this._filterKindFor(colIdx, (this.config.headers || [])[colIdx]);
|
|
4508
7558
|
const v = textBox && value != null && typeof value !== "string" ? String(value) : value;
|
|
4509
7559
|
if (textBox) {
|
|
@@ -4639,6 +7689,7 @@ var DataTable = class {
|
|
|
4639
7689
|
if (this._isClickable()) names.push("twm-dt--clickable");
|
|
4640
7690
|
if (this._gutter) names.push("twm-dt--select");
|
|
4641
7691
|
if (this._selecting) names.push("twm-dt--selecting");
|
|
7692
|
+
if (this._cellGrid) names.push("twm-dt--cells");
|
|
4642
7693
|
return names.join(" ");
|
|
4643
7694
|
}
|
|
4644
7695
|
/** `clickable` when it is said; otherwise exactly when rows open. */
|
|
@@ -4685,18 +7736,18 @@ var DataTable = class {
|
|
|
4685
7736
|
/** A row's key, as text, or null: `getRowKey`, else its original index. */
|
|
4686
7737
|
_rowKey(row, rowIdx) {
|
|
4687
7738
|
const fn = this.config.getRowKey;
|
|
4688
|
-
let
|
|
7739
|
+
let key3;
|
|
4689
7740
|
if (typeof fn === "function") {
|
|
4690
7741
|
try {
|
|
4691
|
-
|
|
7742
|
+
key3 = fn(row, rowIdx);
|
|
4692
7743
|
} catch (err) {
|
|
4693
7744
|
console.error("[DataTable] getRowKey threw", err);
|
|
4694
|
-
|
|
7745
|
+
key3 = null;
|
|
4695
7746
|
}
|
|
4696
7747
|
} else {
|
|
4697
|
-
|
|
7748
|
+
key3 = rowIdx;
|
|
4698
7749
|
}
|
|
4699
|
-
return
|
|
7750
|
+
return key3 == null ? null : String(key3);
|
|
4700
7751
|
}
|
|
4701
7752
|
/** The gestures `activateOn` names, as a set. */
|
|
4702
7753
|
_activateOn() {
|
|
@@ -5392,6 +8443,7 @@ var DataTable = class {
|
|
|
5392
8443
|
headerWrap.scrollLeft = x;
|
|
5393
8444
|
const residual = x - headerWrap.scrollLeft;
|
|
5394
8445
|
headerTable.style.transform = residual ? `translateX(${-residual}px)` : "";
|
|
8446
|
+
this._colGroups()?.align();
|
|
5395
8447
|
}
|
|
5396
8448
|
/** Drag-to-resize: hang a thin grab handle off the right edge of
|
|
5397
8449
|
* every header cell. Dragging it writes a per-column width override
|
|
@@ -5561,10 +8613,11 @@ var DataTable = class {
|
|
|
5561
8613
|
_installInteractions() {
|
|
5562
8614
|
if (!this._tbodyEl || !this._tableEl) return;
|
|
5563
8615
|
const { selectable, copyable } = this.config;
|
|
5564
|
-
const customMenu = this.config.contextMenuItems != null;
|
|
8616
|
+
const customMenu = this.config.contextMenuItems != null || !!this._structure()?.hasRowItems();
|
|
5565
8617
|
const tbody = this._tbodyEl;
|
|
5566
8618
|
const table = this._tableEl;
|
|
5567
|
-
if (
|
|
8619
|
+
if (this._cellGrid) this._cellGrid.install(tbody, table);
|
|
8620
|
+
if (selectable && !this._cellGrid) {
|
|
5568
8621
|
const handleRowClick = (event) => {
|
|
5569
8622
|
const rowEl = event.target.closest("tr");
|
|
5570
8623
|
if (!rowEl || rowEl.__rowIndex === void 0) return;
|
|
@@ -5759,13 +8812,13 @@ var DataTable = class {
|
|
|
5759
8812
|
const handleKeyDown = (event) => {
|
|
5760
8813
|
const ctrlLike = event.ctrlKey || event.metaKey;
|
|
5761
8814
|
if (ctrlLike && !event.altKey) {
|
|
5762
|
-
const
|
|
5763
|
-
if (
|
|
8815
|
+
const key3 = String(event.key || "").toLowerCase();
|
|
8816
|
+
if (key3 === "a" && selectable === "single") {
|
|
5764
8817
|
event.preventDefault();
|
|
5765
|
-
} else if (
|
|
8818
|
+
} else if (key3 === "a" && selectable) {
|
|
5766
8819
|
event.preventDefault();
|
|
5767
8820
|
this._selectAllShown();
|
|
5768
|
-
} else if (
|
|
8821
|
+
} else if (key3 === "c") {
|
|
5769
8822
|
event.preventDefault();
|
|
5770
8823
|
this.copyToClipboard(event.shiftKey ? "csv" : "tsv");
|
|
5771
8824
|
}
|
|
@@ -5788,7 +8841,7 @@ var DataTable = class {
|
|
|
5788
8841
|
const idx = tr.__rowIndex;
|
|
5789
8842
|
if (idx !== void 0) {
|
|
5790
8843
|
const on = this._state.selected.has(idx);
|
|
5791
|
-
tr.classList.toggle("selected", on);
|
|
8844
|
+
tr.classList.toggle("selected", on && !this._cellGrid);
|
|
5792
8845
|
const box = this._gutter ? tr.querySelector(":scope > td.twm-dt-select-cell > input") : null;
|
|
5793
8846
|
if (box) box.checked = on;
|
|
5794
8847
|
}
|
|
@@ -6045,7 +9098,10 @@ var DataTable = class {
|
|
|
6045
9098
|
items = null;
|
|
6046
9099
|
}
|
|
6047
9100
|
}
|
|
6048
|
-
|
|
9101
|
+
const list = Array.isArray(items) ? items.filter(Boolean) : [];
|
|
9102
|
+
const structural = this._structure()?.rowItems(ctx.rowIdx) || [];
|
|
9103
|
+
if (structural.length && list.length) list.push({ separator: true });
|
|
9104
|
+
return list.concat(structural);
|
|
6049
9105
|
}
|
|
6050
9106
|
/**
|
|
6051
9107
|
* STORY-0010. Fill the menu for this right-click: the consumer's items,
|
|
@@ -6164,6 +9220,9 @@ var DataTable = class {
|
|
|
6164
9220
|
};
|
|
6165
9221
|
|
|
6166
9222
|
export {
|
|
9223
|
+
createTextEditor,
|
|
9224
|
+
CellEditorHost,
|
|
9225
|
+
CellSelection,
|
|
6167
9226
|
createRafResizeObserver,
|
|
6168
9227
|
ISO_DATE,
|
|
6169
9228
|
ISO_DATETIME,
|
|
@@ -6175,9 +9234,15 @@ export {
|
|
|
6175
9234
|
matchDateFilter,
|
|
6176
9235
|
matchDurationFilter,
|
|
6177
9236
|
createChipsFilter,
|
|
9237
|
+
ACTIONS,
|
|
9238
|
+
DEFAULT_HISTORY_LIMIT,
|
|
9239
|
+
StagedHistory,
|
|
9240
|
+
detectSeries,
|
|
9241
|
+
projectSeries,
|
|
9242
|
+
attachInlineRenamer,
|
|
6178
9243
|
PAGING_HARD_LIMIT,
|
|
6179
9244
|
PAGING_SOFT_LIMIT,
|
|
6180
9245
|
resolvePaging,
|
|
6181
9246
|
DataTable
|
|
6182
9247
|
};
|
|
6183
|
-
//# sourceMappingURL=chunk-
|
|
9248
|
+
//# sourceMappingURL=chunk-Q36EPT2R.js.map
|