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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cell_selection.js — STORY-0003. The model under DataTable's CELL MODE
|
|
3
|
+
* (`selectionMode: 'cells'`): a cursor cell, a range from an anchor to that
|
|
4
|
+
* cursor, and every move the keys make over them. No DOM, so the rules can be
|
|
5
|
+
* tested — or drive another grid — without a table. DataTable's own half (the
|
|
6
|
+
* painting, the pointer, the keys, the editor) is `cell_grid.js`, which is
|
|
7
|
+
* internal to the table.
|
|
8
|
+
*
|
|
9
|
+
* ══ COORDINATES ═════════════════════════════════════════════════════════
|
|
10
|
+
*
|
|
11
|
+
* A cell is `{row, col}`. `row` is the row's POSITION in the order shown — the
|
|
12
|
+
* index into `getDisplayedRows()`, after the sort and the filters — because a
|
|
13
|
+
* range is a rectangle on the screen, not in the data. `col` is the DATA column
|
|
14
|
+
* index (the one `headers`, `renderCell` and `hiddenColumns` use); a range
|
|
15
|
+
* spans the columns SHOWN between its two edges, so a hidden column inside it
|
|
16
|
+
* is not part of it. The cursor moves only over the rows of the page drawn.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The selection model. Rows are positions `first..last`; columns are the data
|
|
21
|
+
* indices in `cols`, in the order they are shown. Both are handed in on every
|
|
22
|
+
* move (`setBounds`), because a filter, a page or a hidden column changes them
|
|
23
|
+
* under a selection that has to survive.
|
|
24
|
+
*/
|
|
25
|
+
export class CellSelection {
|
|
26
|
+
constructor() {
|
|
27
|
+
/** @type {{row:number, col:number}|null} */
|
|
28
|
+
this.anchor = null;
|
|
29
|
+
/** @type {{row:number, col:number}|null} the cursor */
|
|
30
|
+
this.focus = null;
|
|
31
|
+
this.first = 0;
|
|
32
|
+
this.last = -1;
|
|
33
|
+
this.cols = [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The rows (positions, inclusive) and columns (data indices, shown order)
|
|
37
|
+
* the cursor may stand on. A selection outside them is pulled in. */
|
|
38
|
+
setBounds(first, last, cols) {
|
|
39
|
+
this.first = first;
|
|
40
|
+
this.last = last;
|
|
41
|
+
this.cols = Array.isArray(cols) ? cols.slice() : [];
|
|
42
|
+
if (this.isEmptyGrid()) {
|
|
43
|
+
this.anchor = null;
|
|
44
|
+
this.focus = null;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (this.focus) this.focus = this._clamp(this.focus);
|
|
48
|
+
if (this.anchor) this.anchor = this._clamp(this.anchor);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
isEmptyGrid() {
|
|
52
|
+
return this.last < this.first || this.cols.length === 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_ord(col) {
|
|
56
|
+
const i = this.cols.indexOf(col);
|
|
57
|
+
return i >= 0 ? i : 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_clamp({ row, col }) {
|
|
61
|
+
const r = Math.max(this.first, Math.min(this.last, row));
|
|
62
|
+
const c = this.cols.includes(col) ? col
|
|
63
|
+
: this.cols[Math.max(0, Math.min(this.cols.length - 1, this._nearestOrd(col)))];
|
|
64
|
+
return { row: r, col: c };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A column that is not shown any more: the shown one nearest its index. */
|
|
68
|
+
_nearestOrd(col) {
|
|
69
|
+
let best = 0;
|
|
70
|
+
for (let i = 0; i < this.cols.length; i++) if (this.cols[i] <= col) best = i;
|
|
71
|
+
return best;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_inside(row, ord) {
|
|
75
|
+
return row >= this.first && row <= this.last && ord >= 0 && ord < this.cols.length;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Put the cursor on one cell and collapse the range onto it. */
|
|
79
|
+
setCursor(row, col) {
|
|
80
|
+
if (this.isEmptyGrid()) return false;
|
|
81
|
+
this.focus = this._clamp({ row, col });
|
|
82
|
+
this.anchor = { ...this.focus };
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Move the cursor to a cell and keep the anchor: the range grows. */
|
|
87
|
+
extendTo(row, col) {
|
|
88
|
+
if (!this.anchor) return this.setCursor(row, col);
|
|
89
|
+
if (this.isEmptyGrid()) return false;
|
|
90
|
+
this.focus = this._clamp({ row, col });
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_go(cell, extend) {
|
|
95
|
+
return extend ? this.extendTo(cell.row, cell.col) : this.setCursor(cell.row, cell.col);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** One step: the arrows. At an edge the cursor stays put. */
|
|
99
|
+
move(dRow, dCol, { extend = false } = {}) {
|
|
100
|
+
if (!this.focus) return this.setCursor(this.first, this.cols[0]);
|
|
101
|
+
const ord = Math.max(0, Math.min(this.cols.length - 1, this._ord(this.focus.col) + dCol));
|
|
102
|
+
const row = Math.max(this.first, Math.min(this.last, this.focus.row + dRow));
|
|
103
|
+
return this._go({ row, col: this.cols[ord] }, extend);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Ctrl+arrow: to the edge of the block of filled cells, as a spreadsheet
|
|
108
|
+
* does. From a filled cell whose neighbour is filled, to the last filled
|
|
109
|
+
* cell of that run; otherwise to the next filled cell along the line, or
|
|
110
|
+
* the edge of the grid when there is none.
|
|
111
|
+
*
|
|
112
|
+
* @param {number} dRow -1, 0 or 1
|
|
113
|
+
* @param {number} dCol -1, 0 or 1
|
|
114
|
+
* @param {(row:number, col:number) => boolean} filled
|
|
115
|
+
*/
|
|
116
|
+
jump(dRow, dCol, filled, { extend = false } = {}) {
|
|
117
|
+
if (!this.focus) return this.setCursor(this.first, this.cols[0]);
|
|
118
|
+
let r = this.focus.row;
|
|
119
|
+
let o = this._ord(this.focus.col);
|
|
120
|
+
const isFilled = (row, ord) => !!filled(row, this.cols[ord]);
|
|
121
|
+
if (this._inside(r + dRow, o + dCol)) {
|
|
122
|
+
if (isFilled(r, o) && isFilled(r + dRow, o + dCol)) {
|
|
123
|
+
while (this._inside(r + dRow, o + dCol) && isFilled(r + dRow, o + dCol)) {
|
|
124
|
+
r += dRow; o += dCol;
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
r += dRow; o += dCol;
|
|
128
|
+
while (!isFilled(r, o) && this._inside(r + dRow, o + dCol)) {
|
|
129
|
+
r += dRow; o += dCol;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return this._go({ row: r, col: this.cols[o] }, extend);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Home / End: the first or last column of the cursor's row; with
|
|
137
|
+
* `corner` (Ctrl), the first or last cell of the grid. */
|
|
138
|
+
lineEnd(toEnd, { extend = false, corner = false } = {}) {
|
|
139
|
+
if (!this.focus) return this.setCursor(this.first, this.cols[0]);
|
|
140
|
+
const col = toEnd ? this.cols[this.cols.length - 1] : this.cols[0];
|
|
141
|
+
const row = corner ? (toEnd ? this.last : this.first) : this.focus.row;
|
|
142
|
+
return this._go({ row, col }, extend);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Tab: one cell right, wrapping to the first cell of the next row (Shift:
|
|
146
|
+
* left, wrapping to the last cell of the row before). The last cell of
|
|
147
|
+
* the grid (the first, going back) stays put. */
|
|
148
|
+
tab(back = false) {
|
|
149
|
+
if (!this.focus) return this.setCursor(this.first, this.cols[0]);
|
|
150
|
+
let row = this.focus.row;
|
|
151
|
+
let ord = this._ord(this.focus.col) + (back ? -1 : 1);
|
|
152
|
+
if (ord >= this.cols.length) {
|
|
153
|
+
if (row >= this.last) return this.setCursor(row, this.focus.col);
|
|
154
|
+
row += 1; ord = 0;
|
|
155
|
+
} else if (ord < 0) {
|
|
156
|
+
if (row <= this.first) return this.setCursor(row, this.focus.col);
|
|
157
|
+
row -= 1; ord = this.cols.length - 1;
|
|
158
|
+
}
|
|
159
|
+
return this.setCursor(row, this.cols[ord]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Whether Tab (Shift+Tab with `back`) has nowhere to go: the cursor is
|
|
163
|
+
* on the grid's last cell (its first, going back). The grid then lets
|
|
164
|
+
* the key move focus on, out of the table. */
|
|
165
|
+
tabEnds(back = false) {
|
|
166
|
+
if (!this.focus || this.isEmptyGrid()) return false;
|
|
167
|
+
const col = back ? this.cols[0] : this.cols[this.cols.length - 1];
|
|
168
|
+
const row = back ? this.first : this.last;
|
|
169
|
+
return this.focus.row === row && this.focus.col === col;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Every cell of the grid: from its first cell to its last, which is
|
|
173
|
+
* where the cursor goes. */
|
|
174
|
+
selectAll() {
|
|
175
|
+
if (this.isEmptyGrid()) return false;
|
|
176
|
+
this.anchor = { row: this.first, col: this.cols[0] };
|
|
177
|
+
this.focus = { row: this.last, col: this.cols[this.cols.length - 1] };
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The range collapses onto the cursor. */
|
|
182
|
+
collapse() {
|
|
183
|
+
if (!this.focus) return false;
|
|
184
|
+
const was = this.anchor && (this.anchor.row !== this.focus.row || this.anchor.col !== this.focus.col);
|
|
185
|
+
this.anchor = { ...this.focus };
|
|
186
|
+
return !!was;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
clear() {
|
|
190
|
+
this.anchor = null;
|
|
191
|
+
this.focus = null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** `{top, bottom, left, right, columns}` — rows as positions, `left` and
|
|
195
|
+
* `right` and `columns` as data indices — or null with nothing selected. */
|
|
196
|
+
range() {
|
|
197
|
+
if (!this.focus || !this.anchor) return null;
|
|
198
|
+
const a = this._ord(this.anchor.col);
|
|
199
|
+
const b = this._ord(this.focus.col);
|
|
200
|
+
const lo = Math.min(a, b);
|
|
201
|
+
const hi = Math.max(a, b);
|
|
202
|
+
return {
|
|
203
|
+
top: Math.min(this.anchor.row, this.focus.row),
|
|
204
|
+
bottom: Math.max(this.anchor.row, this.focus.row),
|
|
205
|
+
left: this.cols[lo],
|
|
206
|
+
right: this.cols[hi],
|
|
207
|
+
columns: this.cols.slice(lo, hi + 1),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
contains(row, col) {
|
|
212
|
+
const r = this.range();
|
|
213
|
+
if (!r || row < r.top || row > r.bottom) return false;
|
|
214
|
+
return r.columns.includes(col);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
isCursor(row, col) {
|
|
218
|
+
return !!this.focus && this.focus.row === row && this.focus.col === col;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CHOICE CONTROLS — the one select, the one set of toggle chips and the one
|
|
3
|
+
* radio list, shared by `openForm` and the flow kit's settings widgets.
|
|
4
|
+
*
|
|
5
|
+
* Each draws a fixed list of options and reads back the option's OWN value —
|
|
6
|
+
* a number stays a number, an object stays the object — never the text of an
|
|
7
|
+
* `<option>` or a `value` attribute. That is why every control here keeps the
|
|
8
|
+
* values beside the DOM it draws and resolves the reader's choice by position.
|
|
9
|
+
*
|
|
10
|
+
* An option is a bare value, or `{ value, label?, description?, disabled?,
|
|
11
|
+
* reason? }`. Each also takes `{ group, options }` entries: `selectBox` draws
|
|
12
|
+
* one as an `<optgroup>`, the chips and the radio list as a `.twm-choice__group`
|
|
13
|
+
* heading over its options. A group with no options draws no heading there.
|
|
14
|
+
*
|
|
15
|
+
* A DISABLED OPTION IS SHOWN, WITH ITS REASON, AND IS NEVER CHOSEN BY VALUE:
|
|
16
|
+
* a stored value that names one is not selected (the select falls to its first
|
|
17
|
+
* enabled option, the radios to none, the chips leave it unpressed), unless
|
|
18
|
+
* `keepMissing` keeps it as a value the list does not offer. The reader cannot
|
|
19
|
+
* pick one either. What a caller does about a disabled option selected some
|
|
20
|
+
* other way (a script setting `.value`) is the caller's: `selectBox.entry()`
|
|
21
|
+
* and `choiceList.entry()` say which option is selected, disabled or not.
|
|
22
|
+
*
|
|
23
|
+
* `keepMissing` (the kit's rule, on by default): a stored value the list does
|
|
24
|
+
* not offer is KEPT, marked with `notFound(text)`, so a settings panel cannot
|
|
25
|
+
* silently turn it into the first entry on the next save. A form that should
|
|
26
|
+
* offer only what is listed passes `keepMissing: false`.
|
|
27
|
+
*
|
|
28
|
+
* `loose`: a stored value matches an option whose value has the same TEXT
|
|
29
|
+
* (`2` matches `'2'`), for a caller whose stored values may arrive as strings.
|
|
30
|
+
* Off by default: the kit's values keep their type, and `'2'` is not `2`.
|
|
31
|
+
* Text is only the fallback: an option whose value IS the stored value (`===`)
|
|
32
|
+
* wins over an earlier one that only reads the same (`findChoice`).
|
|
33
|
+
*
|
|
34
|
+
* Nothing here names a consumer's vocabulary; every word on screen is the
|
|
35
|
+
* caller's (`label`, `reason`, `description`, `notFound`).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** A value as one line of text shows it: a string as itself, anything else as JSON. */
|
|
39
|
+
export const shownValue = (v) => (v === undefined ? '' : typeof v === 'string' ? v : JSON.stringify(v));
|
|
40
|
+
|
|
41
|
+
function el(tag, className, text) {
|
|
42
|
+
const n = document.createElement(tag);
|
|
43
|
+
if (className) n.className = className;
|
|
44
|
+
if (text !== undefined && text !== null) n.textContent = String(text);
|
|
45
|
+
return n;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isGroup = (o) => o !== null && typeof o === 'object' && Array.isArray(o.options) && 'group' in o;
|
|
49
|
+
|
|
50
|
+
/** One option, normalised: `{value, label, description, disabled, reason}`. */
|
|
51
|
+
function choiceOption(o) {
|
|
52
|
+
if (o === null || typeof o !== 'object') {
|
|
53
|
+
return { value: o, label: shownValue(o), description: null, disabled: false, reason: null };
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
value: o.value,
|
|
57
|
+
label: o.label != null ? String(o.label) : shownValue(o.value),
|
|
58
|
+
description: o.description != null ? String(o.description) : null,
|
|
59
|
+
disabled: Boolean(o.disabled),
|
|
60
|
+
reason: o.reason != null && o.reason !== '' ? String(o.reason) : null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Every option of a list, groups flattened in order. */
|
|
65
|
+
export function flatChoices(options) {
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const o of options || []) {
|
|
68
|
+
if (isGroup(o)) for (const inner of o.options) out.push(choiceOption(inner));
|
|
69
|
+
else out.push(choiceOption(o));
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Where the chips and the radio list draw a group heading: the position in
|
|
76
|
+
* the flattened list of each non-empty group's first option, to its heading.
|
|
77
|
+
*/
|
|
78
|
+
function groupHeads(options) {
|
|
79
|
+
const heads = new Map();
|
|
80
|
+
let at = 0;
|
|
81
|
+
for (const o of options || []) {
|
|
82
|
+
if (!isGroup(o)) { at += 1; continue; }
|
|
83
|
+
if (o.options.length) heads.set(at, String(o.group ?? ''));
|
|
84
|
+
at += o.options.length;
|
|
85
|
+
}
|
|
86
|
+
return heads;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const groupHeading = (text) => el('div', 'twm-choice__group', text);
|
|
90
|
+
|
|
91
|
+
/** Does stored `value` name option value `v`? */
|
|
92
|
+
function sameChoice(v, value, loose = false) {
|
|
93
|
+
if (v === value) return true;
|
|
94
|
+
if (!loose || v === undefined || value === undefined || v === null || value === null) return false;
|
|
95
|
+
return String(v) === String(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The position in `list` (normalised options) of the option stored value `v`
|
|
100
|
+
* names, or -1: the first whose value IS `v`, else — with `loose` — the first
|
|
101
|
+
* whose value has the same text. `from` skips the leading entries.
|
|
102
|
+
*/
|
|
103
|
+
function choiceIndex(list, v, loose = false, from = 0) {
|
|
104
|
+
for (let i = from; i < list.length; i += 1) if (list[i].value === v) return i;
|
|
105
|
+
if (!loose) return -1;
|
|
106
|
+
for (let i = from; i < list.length; i += 1) if (sameChoice(list[i].value, v, true)) return i;
|
|
107
|
+
return -1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The option of `list` stored value `v` names (`choiceIndex`), or undefined. */
|
|
111
|
+
export function findChoice(list, v, loose = false) {
|
|
112
|
+
const i = choiceIndex(list, v, loose);
|
|
113
|
+
return i >= 0 ? list[i] : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** An option's text: its label, and the reason it is unavailable when it is. */
|
|
117
|
+
const optionText = (o) => (o.disabled && o.reason ? `${o.label} — ${o.reason}` : o.label);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A `<select>` over `options`.
|
|
121
|
+
*
|
|
122
|
+
* `empty` (text) draws a first option that stands for "no value" (`undefined`).
|
|
123
|
+
* `valueAttr`: what each `<option value>` carries — `'index'` (its position;
|
|
124
|
+
* the kit's) or `'value'` (the option's value as text, so a script that sets
|
|
125
|
+
* `.value = 'b'` picks option `'b'`). The value READ BACK is the option's own
|
|
126
|
+
* either way.
|
|
127
|
+
*
|
|
128
|
+
* Returns `{ box, valueOf(), entry(), select(value), setOptions(options, value?) }`:
|
|
129
|
+
* `select` re-selects by value (true when the value is offered and enabled);
|
|
130
|
+
* `setOptions` redraws the list and places `value` (default: the current
|
|
131
|
+
* value) the way the first draw places one, so with `keepMissing` a value the
|
|
132
|
+
* list lacks shows as its own not-found entry.
|
|
133
|
+
*/
|
|
134
|
+
export function selectBox(options, value, {
|
|
135
|
+
empty = null, label = '', notFound = (v) => `${v} (not found)`, keepMissing = true,
|
|
136
|
+
valueAttr = 'index', loose = false, className = 'twm-flow-input twm-flow-input--select',
|
|
137
|
+
} = {}) {
|
|
138
|
+
const box = el('select', className);
|
|
139
|
+
if (label) box.setAttribute('aria-label', label);
|
|
140
|
+
let entries = [];
|
|
141
|
+
|
|
142
|
+
const add = (parent, o) => {
|
|
143
|
+
const attr = valueAttr === 'index' ? String(entries.length) : (o.value === undefined ? '' : String(o.value));
|
|
144
|
+
// `createElement`, not `new Option`: the constructor is a window
|
|
145
|
+
// global, and a page that installed only `document` has none.
|
|
146
|
+
const opt = el('option', '', optionText(o));
|
|
147
|
+
opt.value = attr;
|
|
148
|
+
if (o.disabled) {
|
|
149
|
+
opt.disabled = true;
|
|
150
|
+
if (o.reason) opt.title = o.reason;
|
|
151
|
+
}
|
|
152
|
+
parent.appendChild(opt);
|
|
153
|
+
entries.push(o);
|
|
154
|
+
};
|
|
155
|
+
const firstEnabled = () => {
|
|
156
|
+
const i = entries.findIndex((e) => !e.disabled);
|
|
157
|
+
return i >= 0 ? i : 0;
|
|
158
|
+
};
|
|
159
|
+
const select = (v) => {
|
|
160
|
+
const start = empty !== null ? 1 : 0;
|
|
161
|
+
const at = choiceIndex(entries, v, loose, start);
|
|
162
|
+
if (at < 0 || entries[at].disabled || entries[at].stray) return false;
|
|
163
|
+
box.selectedIndex = at;
|
|
164
|
+
return true;
|
|
165
|
+
};
|
|
166
|
+
const place = (v) => {
|
|
167
|
+
if (select(v)) return;
|
|
168
|
+
if (keepMissing && v !== undefined && v !== null && v !== '') {
|
|
169
|
+
// A stored value the list no longer offers is KEPT as its own option:
|
|
170
|
+
// it must not silently become the first entry on the next save.
|
|
171
|
+
add(box, { value: v, label: notFound(shownValue(v)), description: null, disabled: false, reason: null,
|
|
172
|
+
stray: true });
|
|
173
|
+
box.selectedIndex = entries.length - 1;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
box.selectedIndex = empty !== null ? 0 : firstEnabled();
|
|
177
|
+
};
|
|
178
|
+
const fill = (opts, v) => {
|
|
179
|
+
box.replaceChildren();
|
|
180
|
+
entries = [];
|
|
181
|
+
if (empty !== null) add(box, { value: undefined, label: String(empty), description: null, disabled: false,
|
|
182
|
+
reason: null });
|
|
183
|
+
for (const o of opts || []) {
|
|
184
|
+
if (isGroup(o)) {
|
|
185
|
+
const g = el('optgroup');
|
|
186
|
+
g.label = String(o.group ?? '');
|
|
187
|
+
for (const inner of o.options) add(g, choiceOption(inner));
|
|
188
|
+
box.appendChild(g);
|
|
189
|
+
} else {
|
|
190
|
+
add(box, choiceOption(o));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
place(v);
|
|
194
|
+
};
|
|
195
|
+
const entry = () => (box.selectedIndex < 0 ? null : entries[box.selectedIndex] ?? null);
|
|
196
|
+
const valueOf = () => entry()?.value;
|
|
197
|
+
|
|
198
|
+
fill(options, value);
|
|
199
|
+
return {
|
|
200
|
+
box,
|
|
201
|
+
valueOf,
|
|
202
|
+
entry,
|
|
203
|
+
select,
|
|
204
|
+
setOptions(opts, v = valueOf()) { fill(opts, v); },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Toggle chips over a fixed list: a multi-select. The value is the list of
|
|
210
|
+
* pressed values in the list's order, and `onChange(list | undefined)` hears
|
|
211
|
+
* each toggle (`undefined` when none is pressed). With `keepMissing`, a stored
|
|
212
|
+
* entry the list lacks is kept, marked, until it is toggled off. A disabled
|
|
213
|
+
* option is a chip that cannot be pressed, its reason under its label.
|
|
214
|
+
*
|
|
215
|
+
* Returns `{ el, valueOf(), setValue(list), setOptions(options) }`;
|
|
216
|
+
* `setOptions` keeps the pressed values the new list still offers.
|
|
217
|
+
*/
|
|
218
|
+
export function toggleChips({ options, value, label, readOnly, onChange, notFound = (v) => `${v} (not found)`,
|
|
219
|
+
keepMissing = true, loose = false }) {
|
|
220
|
+
const root = el('div', 'twm-flow-togglechips');
|
|
221
|
+
root.setAttribute('role', 'group');
|
|
222
|
+
if (label) root.setAttribute('aria-label', label);
|
|
223
|
+
let shown = []; // the options as given, groups and all
|
|
224
|
+
let list = [];
|
|
225
|
+
let chosen = new Set();
|
|
226
|
+
let strays = [];
|
|
227
|
+
|
|
228
|
+
const emit = () => {
|
|
229
|
+
const out = valueOf();
|
|
230
|
+
onChange?.(out.length ? out : undefined);
|
|
231
|
+
};
|
|
232
|
+
const chipFor = (text, pressed, toggle, { stray = false, reason = null, disabled = false } = {}) => {
|
|
233
|
+
const b = el('button', `twm-flow-togglechip${stray ? ' twm-flow-togglechip--missing' : ''}`, text);
|
|
234
|
+
b.type = 'button';
|
|
235
|
+
b.setAttribute('aria-pressed', pressed ? 'true' : 'false');
|
|
236
|
+
b.disabled = Boolean(readOnly) || disabled;
|
|
237
|
+
if (reason) {
|
|
238
|
+
b.title = reason;
|
|
239
|
+
b.appendChild(el('span', 'twm-choice__reason', reason));
|
|
240
|
+
}
|
|
241
|
+
b.addEventListener('click', () => {
|
|
242
|
+
const on = toggle();
|
|
243
|
+
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
244
|
+
emit();
|
|
245
|
+
});
|
|
246
|
+
return b;
|
|
247
|
+
};
|
|
248
|
+
const draw = (opts, stored) => {
|
|
249
|
+
shown = opts || [];
|
|
250
|
+
list = flatChoices(shown);
|
|
251
|
+
const heads = groupHeads(shown);
|
|
252
|
+
const wanted = Array.isArray(stored) ? stored : [];
|
|
253
|
+
const offered = (v) => {
|
|
254
|
+
const o = findChoice(list, v, loose);
|
|
255
|
+
return o && !o.disabled ? o : undefined;
|
|
256
|
+
};
|
|
257
|
+
chosen = new Set(wanted.map(offered).filter(Boolean).map((o) => o.value));
|
|
258
|
+
strays = keepMissing ? wanted.filter((v) => !offered(v)) : [];
|
|
259
|
+
root.replaceChildren();
|
|
260
|
+
list.forEach((o, i) => {
|
|
261
|
+
if (heads.has(i)) root.appendChild(groupHeading(heads.get(i)));
|
|
262
|
+
root.appendChild(chipFor(o.label, chosen.has(o.value), () => {
|
|
263
|
+
if (o.disabled) return false; // never chosen, even with the button forced
|
|
264
|
+
if (chosen.has(o.value)) chosen.delete(o.value); else chosen.add(o.value);
|
|
265
|
+
return chosen.has(o.value);
|
|
266
|
+
}, { disabled: o.disabled, reason: o.disabled ? o.reason : null }));
|
|
267
|
+
});
|
|
268
|
+
for (const v of [...strays]) {
|
|
269
|
+
root.appendChild(chipFor(notFound(shownValue(v)), true, () => {
|
|
270
|
+
const i = strays.indexOf(v);
|
|
271
|
+
if (i >= 0) { strays.splice(i, 1); return false; }
|
|
272
|
+
strays.push(v);
|
|
273
|
+
return true;
|
|
274
|
+
}, { stray: true }));
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const valueOf = () => [...list.filter((o) => chosen.has(o.value)).map((o) => o.value), ...strays];
|
|
278
|
+
draw(options, value);
|
|
279
|
+
return {
|
|
280
|
+
el: root,
|
|
281
|
+
valueOf,
|
|
282
|
+
setValue: (stored) => draw(shown, stored),
|
|
283
|
+
setOptions: (opts) => draw(opts, valueOf()),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* A one-of-N as radio rows, each with its label — and a sentence under it when
|
|
289
|
+
* the option has a `description`, and its `reason` when it is disabled. `name`
|
|
290
|
+
* groups the radios (the caller's, so it is unique where it needs to be).
|
|
291
|
+
*
|
|
292
|
+
* Returns `{ el, focus(), valueOf(), entry(), select(value), setOptions(options) }`.
|
|
293
|
+
* `onChange(value)` hears the reader's choice. With `keepMissing`, a stored
|
|
294
|
+
* value the list lacks is named in a note under the list (and kept by the
|
|
295
|
+
* caller, since no radio holds it).
|
|
296
|
+
*/
|
|
297
|
+
export function choiceList({ options, value, name, label, readOnly, onChange, notFound = (v) => `${v} (not found)`,
|
|
298
|
+
keepMissing = true, loose = false }) {
|
|
299
|
+
const root = el('div', 'twm-flow-choices');
|
|
300
|
+
root.setAttribute('role', 'radiogroup');
|
|
301
|
+
if (label) root.setAttribute('aria-label', label);
|
|
302
|
+
let list = [];
|
|
303
|
+
let radios = [];
|
|
304
|
+
|
|
305
|
+
const draw = (opts, current) => {
|
|
306
|
+
list = flatChoices(opts);
|
|
307
|
+
const heads = groupHeads(opts);
|
|
308
|
+
radios = [];
|
|
309
|
+
root.replaceChildren();
|
|
310
|
+
const named = choiceIndex(list, current, loose);
|
|
311
|
+
const at = named >= 0 && !list[named].disabled ? named : -1;
|
|
312
|
+
list.forEach((o, i) => {
|
|
313
|
+
if (heads.has(i)) root.appendChild(groupHeading(heads.get(i)));
|
|
314
|
+
const row = el('label', `twm-flow-choice${o.disabled ? ' twm-choice--disabled' : ''}`);
|
|
315
|
+
const radio = el('input', 'twm-flow-choice__radio');
|
|
316
|
+
radio.type = 'radio';
|
|
317
|
+
radio.name = name;
|
|
318
|
+
radio.checked = i === at;
|
|
319
|
+
radio.disabled = Boolean(readOnly) || o.disabled;
|
|
320
|
+
radio.addEventListener('change', () => { if (radio.checked) onChange?.(o.value); });
|
|
321
|
+
radios.push(radio);
|
|
322
|
+
const words = el('span', 'twm-flow-choice__words');
|
|
323
|
+
words.appendChild(el('span', 'twm-flow-choice__label', o.label));
|
|
324
|
+
if (o.description !== null) words.appendChild(el('span', 'twm-flow-choice__sub', o.description));
|
|
325
|
+
if (o.disabled && o.reason) words.appendChild(el('span', 'twm-choice__reason', o.reason));
|
|
326
|
+
row.append(radio, words);
|
|
327
|
+
root.appendChild(row);
|
|
328
|
+
});
|
|
329
|
+
if (keepMissing && current !== undefined && named < 0) {
|
|
330
|
+
root.appendChild(el('p', 'twm-flow-field__note', notFound(shownValue(current))));
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const checkedAt = () => radios.findIndex((r) => r.checked);
|
|
334
|
+
const entry = () => list[checkedAt()] ?? null;
|
|
335
|
+
const api = {
|
|
336
|
+
el: root,
|
|
337
|
+
focus: () => (radios.find((r) => r.checked && !r.disabled) || radios.find((r) => !r.disabled))
|
|
338
|
+
?.focus({ preventScroll: true }),
|
|
339
|
+
entry,
|
|
340
|
+
valueOf: () => entry()?.value,
|
|
341
|
+
select(v) {
|
|
342
|
+
const named = choiceIndex(list, v, loose);
|
|
343
|
+
const at = named >= 0 && !list[named].disabled ? named : -1;
|
|
344
|
+
radios.forEach((r, i) => { r.checked = i === at; });
|
|
345
|
+
return at >= 0;
|
|
346
|
+
},
|
|
347
|
+
setOptions(opts) { draw(opts, api.valueOf()); },
|
|
348
|
+
};
|
|
349
|
+
draw(options, value);
|
|
350
|
+
return api;
|
|
351
|
+
}
|