flexdesk 0.16.1 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/LICENSE +0 -0
- package/README.md +2435 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/canvas.js +18 -0
- package/charts.js +21 -0
- package/core.js +14 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3165 -159
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +707 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
- package/dist/chunk-LWV23YVE.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
- package/dist/chunk-RMTRWWEO.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3165 -159
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3290 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/editor.js +20 -0
- package/flow.js +21 -0
- package/host.js +14 -0
- package/package.json +12 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +293 -0
- package/src/ui/components/cell_editors.js +480 -0
- package/src/ui/components/cell_grid.js +761 -0
- package/src/ui/components/cell_paste.js +243 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +696 -71
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +361 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/tsv.js +74 -0
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/tiles.js +27 -0
- package/widgets.js +69 -0
- package/wm.js +28 -0
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-B5Q53LAJ.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- package/src/ui/controllers/panel_state_machine.js +0 -634
package/src/tiling/zoom.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/ui/base/page_base.js
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -23,6 +23,7 @@ const GAP = 8;
|
|
|
23
23
|
* trigger: buttonElement,
|
|
24
24
|
* options: [
|
|
25
25
|
* { type: 'option1', label: 'Option 1', icon: 'icon_name', description: 'Description' },
|
|
26
|
+
* { separator: true },
|
|
26
27
|
* { type: 'option2', label: 'Option 2', icon: 'icon_name', description: 'Description' }
|
|
27
28
|
* ],
|
|
28
29
|
* onSelect: (option) => console.log('Selected:', option.type),
|
|
@@ -34,12 +35,22 @@ const GAP = 8;
|
|
|
34
35
|
* dropdown.toggle();
|
|
35
36
|
* dropdown.destroy();
|
|
36
37
|
* ```
|
|
38
|
+
*
|
|
39
|
+
* ── Separators ────────────────────────────────────────────────────────
|
|
40
|
+
* An entry `{ separator: true }` draws a divider instead of a button: a
|
|
41
|
+
* `<div role="separator" class="twm-action-dropdown-separator">`, which takes
|
|
42
|
+
* no focus, is never reached by the arrow keys (they move between the option
|
|
43
|
+
* buttons only) and never reaches `onSelect` (it has no click handler). A
|
|
44
|
+
* separator only separates: one at the start or the end of the list, or one
|
|
45
|
+
* right after another, is dropped, so a list assembled from groups that may
|
|
46
|
+
* come out empty never draws a stray or doubled rule.
|
|
37
47
|
*/
|
|
38
48
|
export class ActionDropdown {
|
|
39
49
|
/**
|
|
40
50
|
* @param {Object} config
|
|
41
51
|
* @param {HTMLElement} config.trigger - Button element that triggers the dropdown
|
|
42
|
-
* @param {Array} config.options - Array of option objects with { type, label, icon, description? }
|
|
52
|
+
* @param {Array} config.options - Array of option objects with { type, label, icon, description? },
|
|
53
|
+
* or `{ separator: true }` for a divider between them
|
|
43
54
|
* @param {Function} config.onSelect - Callback when an option is selected
|
|
44
55
|
* @param {string} [config.className=''] - Additional CSS class for the dropdown
|
|
45
56
|
* @param {string} [config.menuId] - Optional ID for the menu element
|
|
@@ -98,13 +109,30 @@ export class ActionDropdown {
|
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
/**
|
|
101
|
-
* Render option buttons into a
|
|
112
|
+
* Render option buttons, and dividers for separator entries, into a
|
|
113
|
+
* container element. `data-index` stays the entry's index in `options`.
|
|
102
114
|
* @param {HTMLElement} container
|
|
103
115
|
* @private
|
|
104
116
|
*/
|
|
105
117
|
_renderOptions(container) {
|
|
106
118
|
container.innerHTML = '';
|
|
119
|
+
let pendingSeparator = false;
|
|
107
120
|
this.options.forEach((opt, index) => {
|
|
121
|
+
if (opt?.separator) {
|
|
122
|
+
// Drawn only once an option follows it, and only if an option
|
|
123
|
+
// came before it: leading, trailing and doubled separators
|
|
124
|
+
// collapse.
|
|
125
|
+
pendingSeparator = container.childElementCount > 0;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (pendingSeparator) {
|
|
129
|
+
const sep = document.createElement('div');
|
|
130
|
+
sep.className = 'twm-action-dropdown-separator';
|
|
131
|
+
sep.setAttribute('role', 'separator');
|
|
132
|
+
container.appendChild(sep);
|
|
133
|
+
pendingSeparator = false;
|
|
134
|
+
}
|
|
135
|
+
|
|
108
136
|
const optionBtn = document.createElement('button');
|
|
109
137
|
optionBtn.type = 'button';
|
|
110
138
|
optionBtn.className = 'twm-action-dropdown-option';
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cell_editor_host.js — STORY-0003. ONE CELL BEING EDITED: open an editor in
|
|
3
|
+
* it, and close it exactly once — by a commit or by a cancel.
|
|
4
|
+
*
|
|
5
|
+
* The editor itself comes from a FACTORY, so a consumer can bring any editor
|
|
6
|
+
* it likes (DataTable's `editors` option); the plain text box below is the
|
|
7
|
+
* default. A factory is called with
|
|
8
|
+
*
|
|
9
|
+
* { ...args, value, row, rowIdx, colIdx, column: {index, header, type},
|
|
10
|
+
* initialText, commit(value, move?), cancel() }
|
|
11
|
+
*
|
|
12
|
+
* and returns either an element, or
|
|
13
|
+
* `{ el, getValue?, parse?, multiline?, focus?, destroy? }`.
|
|
14
|
+
* `initialText` is the character typed to open the editor (null when it was
|
|
15
|
+
* opened by F2, Enter or a double-click): the editor starts with it in place
|
|
16
|
+
* of the value, as a spreadsheet does.
|
|
17
|
+
*
|
|
18
|
+
* ══ THE KEYS, WHICH THE HOST OWNS ════════════════════════════════════════
|
|
19
|
+
*
|
|
20
|
+
* Enter commit, and the table moves the cursor down (Shift: up)
|
|
21
|
+
* Tab commit, and the cursor moves right (Shift: left)
|
|
22
|
+
* Escape cancel: the cell is drawn as it was, and nothing is called
|
|
23
|
+
* focus out commit, without moving — a click elsewhere keeps the edit
|
|
24
|
+
*
|
|
25
|
+
* Enter and Tab commit `getValue()` — an input, select or textarea's `value`
|
|
26
|
+
* when the factory returned the field itself; any other editor without a
|
|
27
|
+
* `getValue` calls `commit(value)` itself. Such an editor has no value the host
|
|
28
|
+
* can read: Enter, Tab and the focus leaving do nothing, and `commit()` from
|
|
29
|
+
* outside (its row leaving a virtual window) CANCELS it. Every key
|
|
30
|
+
* pressed in the editor stops there: the table's own keys (the arrows, Ctrl+A,
|
|
31
|
+
* Ctrl+C) are not the editor's.
|
|
32
|
+
*
|
|
33
|
+
* ══ A VALUE THAT DOES NOT PARSE (STORY-0087) ═══════════════════════════
|
|
34
|
+
*
|
|
35
|
+
* An editor with a `parse()` — `() => {ok: true, value} | {ok: false, error}`
|
|
36
|
+
* — commits what `parse()` reads, not `getValue()`. A refusal on Enter or Tab
|
|
37
|
+
* keeps the editor open, and its cell carries the message
|
|
38
|
+
* (`twm-dt-cell--error`, `title`, `data-error`) until the next keystroke in
|
|
39
|
+
* it; nothing is committed. A refusal when the focus leaves, or on `commit()`
|
|
40
|
+
* from outside, closes the editor and hands `onRefuse` the message and the
|
|
41
|
+
* text (`getValue()`); still nothing is committed. A `multiline` editor
|
|
42
|
+
* keeps Enter for a new line: Ctrl/Cmd+Enter commits it.
|
|
43
|
+
*
|
|
44
|
+
* `commit(value, move?)` takes an optional move ('down', 'up', 'right',
|
|
45
|
+
* 'left'), for an editor that commits on a key of its own.
|
|
46
|
+
*
|
|
47
|
+
* ══ EXACTLY ONCE ═════════════════════════════════════════════════════════
|
|
48
|
+
*
|
|
49
|
+
* A session closes once. Escape's own repaint takes the focused input out of
|
|
50
|
+
* the document, and a browser fires `focusout` for that — the commit listener.
|
|
51
|
+
* The session is marked closed BEFORE anything is taken out, so that late
|
|
52
|
+
* `focusout` (and a second Enter, and an editor that calls `commit` twice)
|
|
53
|
+
* finds nothing to do.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The default editor: a text box holding the value as text (or the character
|
|
58
|
+
* that opened it). Left unchanged, it commits the value it was given — not
|
|
59
|
+
* that value turned into a string — so opening and leaving a number cell is
|
|
60
|
+
* no edit at all.
|
|
61
|
+
*/
|
|
62
|
+
export function createTextEditor({ value, initialText = null }) {
|
|
63
|
+
const input = document.createElement('input');
|
|
64
|
+
input.type = 'text';
|
|
65
|
+
input.className = 'twm-dt-editor';
|
|
66
|
+
input.spellcheck = false;
|
|
67
|
+
input.setAttribute('autocomplete', 'off');
|
|
68
|
+
const start = value == null ? '' : String(value);
|
|
69
|
+
input.value = initialText != null ? String(initialText) : start;
|
|
70
|
+
return {
|
|
71
|
+
el: input,
|
|
72
|
+
getValue: () => (initialText == null && input.value === start ? value : input.value),
|
|
73
|
+
focus: () => {
|
|
74
|
+
input.focus({ preventScroll: true });
|
|
75
|
+
const end = input.value.length;
|
|
76
|
+
try { input.setSelectionRange(end, end); } catch (_) { /* not a text type */ }
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** What a factory returned, as one shape — or null when it returned nothing. */
|
|
82
|
+
function normalise(made) {
|
|
83
|
+
if (!made) return null;
|
|
84
|
+
const isNode = typeof Node !== 'undefined' && made instanceof Node;
|
|
85
|
+
const el = isNode ? made : made.el;
|
|
86
|
+
if (!el || (typeof Node !== 'undefined' && !(el instanceof Node))) return null;
|
|
87
|
+
const own = isNode ? {} : made;
|
|
88
|
+
let getValue = typeof own.getValue === 'function' ? own.getValue : null;
|
|
89
|
+
// A form field's value is the editor's; any other element says for itself.
|
|
90
|
+
if (!getValue && el.matches?.('input, select, textarea')) getValue = () => el.value;
|
|
91
|
+
const parse = typeof own.parse === 'function' ? own.parse : null;
|
|
92
|
+
// What a commit reads: the parse, else the value as it is.
|
|
93
|
+
let read = null;
|
|
94
|
+
if (parse) {
|
|
95
|
+
read = () => {
|
|
96
|
+
let r;
|
|
97
|
+
try { r = parse(); } catch (err) { r = { ok: false, error: err?.message || String(err) }; }
|
|
98
|
+
return r && r.ok === false ? { ok: false, error: String(r.error ?? 'Not valid') }
|
|
99
|
+
: { ok: true, value: r ? r.value : undefined };
|
|
100
|
+
};
|
|
101
|
+
} else if (getValue) read = () => ({ ok: true, value: getValue() });
|
|
102
|
+
return {
|
|
103
|
+
el,
|
|
104
|
+
getValue,
|
|
105
|
+
read,
|
|
106
|
+
multiline: !!own.multiline,
|
|
107
|
+
focus: typeof own.focus === 'function' ? own.focus : null,
|
|
108
|
+
destroy: typeof own.destroy === 'function' ? own.destroy : null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const MOVES = new Set(['down', 'up', 'right', 'left']);
|
|
113
|
+
|
|
114
|
+
export class CellEditorHost {
|
|
115
|
+
/**
|
|
116
|
+
* @param {object} o
|
|
117
|
+
* @param {(session, value, move, reason) => void} o.onCommit after the
|
|
118
|
+
* cell is drawn as it was again; `move` is 'down', 'up', 'right', 'left'
|
|
119
|
+
* or null; `reason` is 'key', 'blur', 'editor' or 'scroll'.
|
|
120
|
+
* @param {(session, reason) => void} o.onCancel
|
|
121
|
+
* @param {(session, error, text, reason) => void} [o.onRefuse] a value
|
|
122
|
+
* that did not parse, when the editor closed with it (the focus left,
|
|
123
|
+
* `commit()` from outside); without it such a close is a cancel.
|
|
124
|
+
*/
|
|
125
|
+
constructor({ onCommit, onCancel, onRefuse = null }) {
|
|
126
|
+
this._onCommit = onCommit;
|
|
127
|
+
this._onCancel = onCancel;
|
|
128
|
+
this._onRefuse = onRefuse;
|
|
129
|
+
/** The open session, or null. */
|
|
130
|
+
this.session = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Open an editor in `td`; `args` go to the factory beside the cell's own
|
|
135
|
+
* (DataTable's `locale`, `utc`, `messages`). Returns whether one opened (a factory that
|
|
136
|
+
* throws or returns nothing opens none, and the cell is untouched).
|
|
137
|
+
*/
|
|
138
|
+
open({ td, factory, value, row, rowIdx, colIdx, column, initialText = null, args = {}, ...extra }) {
|
|
139
|
+
if (this.session) return false;
|
|
140
|
+
const s = {
|
|
141
|
+
...extra, td, value, row, rowIdx, colIdx, column,
|
|
142
|
+
previous: value, closed: false, offs: [], saved: null, editor: null,
|
|
143
|
+
};
|
|
144
|
+
let made = null;
|
|
145
|
+
try {
|
|
146
|
+
made = factory({
|
|
147
|
+
...args, value, row, rowIdx, colIdx, column, initialText,
|
|
148
|
+
commit: (v, move = null) => this._finish(s, v, MOVES.has(move) ? move : null, 'editor'),
|
|
149
|
+
cancel: () => this._cancel(s, 'editor'),
|
|
150
|
+
});
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error('[DataTable] cell editor factory threw', err);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const editor = normalise(made);
|
|
156
|
+
if (!editor) return false;
|
|
157
|
+
s.editor = editor;
|
|
158
|
+
s.saved = [...td.childNodes];
|
|
159
|
+
s.savedTitle = td.getAttribute('title');
|
|
160
|
+
s.savedError = {
|
|
161
|
+
on: td.classList.contains('twm-dt-cell--error'),
|
|
162
|
+
message: td.getAttribute('data-error'),
|
|
163
|
+
};
|
|
164
|
+
this.session = s;
|
|
165
|
+
td.classList.add('twm-dt-cell--editing');
|
|
166
|
+
td.replaceChildren(editor.el);
|
|
167
|
+
|
|
168
|
+
const on = (type, fn) => {
|
|
169
|
+
editor.el.addEventListener(type, fn);
|
|
170
|
+
s.offs.push(() => editor.el.removeEventListener(type, fn));
|
|
171
|
+
};
|
|
172
|
+
on('keydown', (ev) => {
|
|
173
|
+
if (ev.isComposing) return;
|
|
174
|
+
ev.stopPropagation();
|
|
175
|
+
if (ev.key === 'Escape') {
|
|
176
|
+
ev.preventDefault();
|
|
177
|
+
this._cancel(s, 'key');
|
|
178
|
+
} else if (ev.key === 'Enter' && !ev.altKey && editor.read
|
|
179
|
+
&& (editor.multiline ? (ev.ctrlKey || ev.metaKey) : !(ev.ctrlKey || ev.metaKey))) {
|
|
180
|
+
ev.preventDefault();
|
|
181
|
+
this._tryKey(s, ev.shiftKey ? 'up' : 'down');
|
|
182
|
+
} else if (ev.key === 'Tab' && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.read) {
|
|
183
|
+
ev.preventDefault();
|
|
184
|
+
this._tryKey(s, ev.shiftKey ? 'left' : 'right');
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// A refusal's message stands until the text changes.
|
|
188
|
+
on('input', () => this._unmark(s));
|
|
189
|
+
// A press inside the editor is the editor's, not a click on the cell.
|
|
190
|
+
for (const type of ['click', 'dblclick', 'mousedown', 'contextmenu']) {
|
|
191
|
+
on(type, (ev) => ev.stopPropagation());
|
|
192
|
+
}
|
|
193
|
+
on('focusout', (ev) => {
|
|
194
|
+
if (s.closed || !editor.read) return;
|
|
195
|
+
const to = ev.relatedTarget;
|
|
196
|
+
if (to && editor.el.contains(to)) return;
|
|
197
|
+
// The window losing focus (another app) is not a click elsewhere.
|
|
198
|
+
const doc = editor.el.ownerDocument;
|
|
199
|
+
if (!to && doc && typeof doc.hasFocus === 'function' && !doc.hasFocus()) return;
|
|
200
|
+
this._settle(s, null, 'blur');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
if (editor.focus) editor.focus();
|
|
205
|
+
else editor.el.focus?.({ preventScroll: true });
|
|
206
|
+
} catch (_) { /* not focusable */ }
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Commit the open session with what its editor holds. */
|
|
211
|
+
commit(move = null, reason = 'editor') {
|
|
212
|
+
const s = this.session;
|
|
213
|
+
if (!s) return false;
|
|
214
|
+
if (!s.editor.read) { this._cancel(s, reason); return false; }
|
|
215
|
+
return this._settle(s, move, reason);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Enter or Tab: commit what parses; a refusal keeps the editor open,
|
|
219
|
+
* the message on its cell. */
|
|
220
|
+
_tryKey(s, move) {
|
|
221
|
+
const r = s.editor.read();
|
|
222
|
+
if (r.ok) return this._finish(s, r.value, move, 'key');
|
|
223
|
+
s.td.classList.add('twm-dt-cell--error');
|
|
224
|
+
s.td.setAttribute('data-error', r.error);
|
|
225
|
+
s.td.setAttribute('title', r.error);
|
|
226
|
+
s.marked = true;
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
_unmark(s) {
|
|
231
|
+
if (!s.marked) return;
|
|
232
|
+
s.marked = false;
|
|
233
|
+
this._restoreMark(s);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_restoreMark(s) {
|
|
237
|
+
const { on, message } = s.savedError;
|
|
238
|
+
s.td.classList.toggle('twm-dt-cell--error', on);
|
|
239
|
+
if (message == null) s.td.removeAttribute('data-error');
|
|
240
|
+
else s.td.setAttribute('data-error', message);
|
|
241
|
+
if (s.savedTitle == null) s.td.removeAttribute('title');
|
|
242
|
+
else s.td.setAttribute('title', s.savedTitle);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The editor closing with what it holds (the focus left, `commit()`):
|
|
246
|
+
* committed when it parses, else handed to `onRefuse`. */
|
|
247
|
+
_settle(s, move, reason) {
|
|
248
|
+
const r = s.editor.read();
|
|
249
|
+
if (r.ok) return this._finish(s, r.value, move, reason);
|
|
250
|
+
if (s.closed) return false;
|
|
251
|
+
let text = '';
|
|
252
|
+
try { text = s.editor.getValue ? s.editor.getValue() : ''; } catch (_) { /* gone */ }
|
|
253
|
+
this._close(s);
|
|
254
|
+
if (this._onRefuse) this._onRefuse(s, r.error, text, reason);
|
|
255
|
+
else this._onCancel?.(s, reason);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Cancel the open session: the cell is drawn as it was. */
|
|
260
|
+
cancel(reason = 'editor') {
|
|
261
|
+
const s = this.session;
|
|
262
|
+
if (!s) return false;
|
|
263
|
+
return this._cancel(s, reason);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Take the editor out and put the cell's own content back. */
|
|
267
|
+
_close(s) {
|
|
268
|
+
s.closed = true;
|
|
269
|
+
if (this.session === s) this.session = null;
|
|
270
|
+
for (const off of s.offs) { try { off(); } catch (_) { /* gone */ } }
|
|
271
|
+
s.offs = [];
|
|
272
|
+
try { s.editor.destroy?.(); } catch (err) {
|
|
273
|
+
console.error('[DataTable] cell editor destroy threw', err);
|
|
274
|
+
}
|
|
275
|
+
s.td.classList.remove('twm-dt-cell--editing');
|
|
276
|
+
s.td.replaceChildren(...s.saved);
|
|
277
|
+
this._restoreMark(s);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_finish(s, value, move, reason) {
|
|
281
|
+
if (s.closed) return false;
|
|
282
|
+
this._close(s);
|
|
283
|
+
this._onCommit?.(s, value, move, reason);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_cancel(s, reason) {
|
|
288
|
+
if (s.closed) return false;
|
|
289
|
+
this._close(s);
|
|
290
|
+
this._onCancel?.(s, reason);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
}
|