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/keymap.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* keymap.js —
|
|
2
|
+
* keymap.js — the window manager's keyboard shortcuts.
|
|
3
3
|
*
|
|
4
4
|
* Alt+H split focused tile horizontally (new empty pane)
|
|
5
5
|
* Alt+V split focused tile vertically (new empty pane)
|
|
@@ -16,18 +16,46 @@
|
|
|
16
16
|
* Ctrl+ArrowLeft/Right iterate virtual desktops
|
|
17
17
|
* Shift+ArrowLeft/Right iterate bottom-panel tabs (when present)
|
|
18
18
|
* Ctrl+Tab / Ctrl+Shift+Tab next / previous main-panel tab (focused tile)
|
|
19
|
-
* F1..
|
|
19
|
+
* F1..F12 jump to the Nth top-nav page (the table says F1..F8)
|
|
20
|
+
* Backspace go back one step up the breadcrumb
|
|
20
21
|
* Ctrl+K open command palette
|
|
21
|
-
* ? open Help
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* ? open Help
|
|
23
|
+
* Esc close the palette
|
|
24
|
+
* any other Alt+letter swallowed (no browser menu-access mid-flow)
|
|
24
25
|
*
|
|
25
|
-
* The
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* ── The bindings are DATA, and an embedder may change them ─────────────
|
|
27
|
+
* Every chord above is one row of `DEFAULT_CHORDS`: a chord id, the action it
|
|
28
|
+
* runs, and whether it also fires while focus is in an editable field. An
|
|
29
|
+
* embedder passes `bindings: { [chordId]: action | false | fn }` (through
|
|
30
|
+
* `createShell({ keymap: { bindings } })`):
|
|
28
31
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
32
|
+
* 'F1': false the chord is the window manager's no more; the event
|
|
33
|
+
* is not prevented, so the application's own handler
|
|
34
|
+
* gets it untouched
|
|
35
|
+
* 'F2': 'nav:0' the chord runs another built-in action (`ACTIONS`)
|
|
36
|
+
* 'Alt+W': (e, ctx) => … the chord runs the embedder's function; the event is
|
|
37
|
+
* prevented unless it returns `false`
|
|
38
|
+
*
|
|
39
|
+
* Only the chords listed in `DEFAULT_CHORDS` can be rebound — the keymap
|
|
40
|
+
* owns those and nothing else. An unknown chord id, or an unknown action
|
|
41
|
+
* name, is reported ONCE (a warning when the keymap is installed) and
|
|
42
|
+
* ignored, so a typo shows up at boot instead of as a key that silently does
|
|
43
|
+
* nothing.
|
|
44
|
+
*
|
|
45
|
+
* ── An event somebody else handled is theirs ──────────────────────────
|
|
46
|
+
* The listener is on `document`, so every component's own keydown handler
|
|
47
|
+
* runs first. One that called `preventDefault()` — a list's Backspace that
|
|
48
|
+
* deletes a row, an editor's Alt+Arrow — keeps its behaviour: the keymap
|
|
49
|
+
* skips every event that is already `defaultPrevented`.
|
|
50
|
+
*
|
|
51
|
+
* The user-facing reference is `KEYMAP_SHORTCUTS` below, which the palette's
|
|
52
|
+
* built-in *Keyboard shortcuts* command shows as a help topic (see
|
|
53
|
+
* `keyboardShortcutsTopic`); `keymapShortcuts(bindings)` is the same table
|
|
54
|
+
* after an embedder's bindings, and that is what the shell hands the palette.
|
|
55
|
+
* Keep the table and `DEFAULT_CHORDS` in step when binding new chords here.
|
|
56
|
+
*
|
|
57
|
+
* Unless a chord is marked as working in fields, the handler refuses to act
|
|
58
|
+
* when focus is in an editable field.
|
|
31
59
|
*
|
|
32
60
|
* ── It returns a disposer, and that is not decoration ──────────────────
|
|
33
61
|
* The listener is on `document` and is bound to ONE shell's `wm` and
|
|
@@ -37,138 +65,385 @@
|
|
|
37
65
|
* attached for ever. Two handlers is not "twice as responsive": `Ctrl+K`
|
|
38
66
|
* toggles the dead palette open and the live one closed in the same keystroke,
|
|
39
67
|
* and `Alt+W` closes a tile in a tree nobody can see. `createShell().dispose()`
|
|
40
|
-
* calls this.
|
|
68
|
+
* calls this. The `?` help shortcut is one of these chords, so it comes off
|
|
69
|
+
* with the rest — importing the help modal installs no listener of its own.
|
|
41
70
|
*/
|
|
42
71
|
|
|
43
|
-
|
|
44
|
-
const onKeyDown = (e) => {
|
|
45
|
-
const inField = e.target?.closest?.(
|
|
46
|
-
'input, textarea, select, [contenteditable="true"]');
|
|
72
|
+
import { HelpModal } from '../help/help_modal.js';
|
|
47
73
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
/**
|
|
75
|
+
* The window manager's default bindings, as the user reads them: one row per
|
|
76
|
+
* chord (or family of chords), `keys` as drawn, `action` in plain words.
|
|
77
|
+
*/
|
|
78
|
+
export const KEYMAP_SHORTCUTS = Object.freeze([
|
|
79
|
+
{ keys: 'Ctrl+K', action: 'Open the command palette' },
|
|
80
|
+
{ keys: '?', action: 'Open help' },
|
|
81
|
+
{ keys: 'Esc', action: 'Close the palette or a modal' },
|
|
82
|
+
{ keys: 'F1..F8', action: 'Jump to the Nth top-nav page' },
|
|
83
|
+
{ keys: 'Backspace', action: 'Go back one step up the breadcrumb' },
|
|
84
|
+
{ keys: 'Alt+H / Alt+V', action: 'Split the focused tile horizontally / vertically (empty pane)' },
|
|
85
|
+
{ keys: 'Alt+Shift+H / Alt+Shift+V', action: "Open the focused tile's content in a horizontal / vertical split" },
|
|
86
|
+
{ keys: 'Alt+T', action: "Open the focused tile's content in a new tab" },
|
|
87
|
+
{ keys: 'Alt+N', action: "Open the focused tile's content in a new window" },
|
|
88
|
+
{ keys: 'Alt+W', action: 'Close the focused tile' },
|
|
89
|
+
{ keys: 'Alt+F', action: 'Float the focused tile into a window, or dock it back' },
|
|
90
|
+
{ keys: 'Alt+Arrow', action: 'Move focus to the neighbouring tile' },
|
|
91
|
+
{ keys: 'Alt+Shift+Arrow', action: 'Swap the focused tile with its neighbour' },
|
|
92
|
+
{ keys: 'Alt+1..9', action: 'Switch to desktop N' },
|
|
93
|
+
{ keys: 'Alt+Shift+1..9', action: 'Move the focused tile to desktop N' },
|
|
94
|
+
{ keys: 'Ctrl+Left / Ctrl+Right', action: 'Previous / next desktop' },
|
|
95
|
+
{ keys: 'Shift+Left / Shift+Right', action: 'Previous / next bottom-panel tab' },
|
|
96
|
+
{ keys: 'Ctrl+Tab / Ctrl+Shift+Tab', action: 'Next / previous tab in the focused tile' },
|
|
97
|
+
].map((r) => Object.freeze(r)));
|
|
98
|
+
|
|
99
|
+
/** The help topic id the palette's *Keyboard shortcuts* command opens. */
|
|
100
|
+
export const KEYBOARD_SHORTCUTS_TOPIC_ID = 'keyboard-shortcuts';
|
|
73
101
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
const _esc = (s) => String(s).replace(/[&<>"']/g, (c) => (
|
|
103
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A shortcuts table (`KEYMAP_SHORTCUTS`, or `keymapShortcuts(bindings)`) as a
|
|
107
|
+
* help topic (`{id, title, html}`, the shape a HelpProvider's `loadTopic`
|
|
108
|
+
* answers). The palette hands it to HelpModal as the FALLBACK for its built-in
|
|
109
|
+
* command: an application whose provider defines `keyboard-shortcuts` shows
|
|
110
|
+
* its own topic; one that defines none — or registers no help at all — shows
|
|
111
|
+
* this table instead of an error.
|
|
112
|
+
*/
|
|
113
|
+
export function keyboardShortcutsTopic(shortcuts = KEYMAP_SHORTCUTS) {
|
|
114
|
+
const rows = shortcuts.map((r) =>
|
|
115
|
+
`<tr><td><kbd>${_esc(r.keys)}</kbd></td><td>${_esc(r.action)}</td></tr>`).join('');
|
|
116
|
+
return {
|
|
117
|
+
id: KEYBOARD_SHORTCUTS_TOPIC_ID,
|
|
118
|
+
title: 'Keyboard shortcuts',
|
|
119
|
+
html: '<table class="help-shortcuts"><thead><tr><th>Keys</th><th>Action</th></tr></thead>'
|
|
120
|
+
+ `<tbody>${rows}</tbody></table>`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ══ Actions ════════════════════════════════════════════════════════════
|
|
125
|
+
// What a chord can run, by name. `run(ctx, e)` answers `false` when the event
|
|
126
|
+
// is to be left unprevented (an F-key with no Nth top-nav button, Esc). `label`
|
|
127
|
+
// is the shortcuts table's words; an action without one is not listed.
|
|
128
|
+
|
|
129
|
+
const DEFAULT_NAV_SELECTOR = '.twm-global-top-bar .twm-bar-center.twm-top-nav .twm-top-nav__btn';
|
|
130
|
+
const DIRS = ['left', 'right', 'up', 'down'];
|
|
131
|
+
const _where = (d) => (d === 'up' || d === 'down' ? d : `on the ${d}`);
|
|
132
|
+
|
|
133
|
+
/** The fixed-name actions. Parameterised ones (`nav:N`, `desktop:N`,
|
|
134
|
+
* `toDesktop:N`, all counted from 0) are built by `_action`. */
|
|
135
|
+
const ACTIONS = {
|
|
136
|
+
'palette': { label: 'Open the command palette', run: ({ palette }) => { palette.toggle(); } },
|
|
137
|
+
'palette:close': { label: 'Close the palette', run: ({ palette }) => {
|
|
138
|
+
// Not prevented either way: a managed modal handles its own Esc.
|
|
139
|
+
if (palette.isOpen()) palette.close();
|
|
140
|
+
return false;
|
|
141
|
+
} },
|
|
142
|
+
'help': { label: 'Open help', run: () => { HelpModal.toggle(); } },
|
|
143
|
+
'back': { label: 'Go back one step up the breadcrumb', run: ({ wm }) => { wm.navigateBack?.(); } },
|
|
144
|
+
'tab:next': { label: 'Next tab in the focused tile', run: ({ wm }) => { wm.cycleFocusedTab(1); } },
|
|
145
|
+
'tab:prev': { label: 'Previous tab in the focused tile', run: ({ wm }) => { wm.cycleFocusedTab(-1); } },
|
|
146
|
+
'desktop:next': { label: 'Next desktop', run: ({ wm }) => { wm.cycleDesktop(1); } },
|
|
147
|
+
'desktop:prev': { label: 'Previous desktop', run: ({ wm }) => { wm.cycleDesktop(-1); } },
|
|
148
|
+
'panelTab:next': { label: 'Next bottom-panel tab', run: () => _cyclePanelTab(1) },
|
|
149
|
+
'panelTab:prev': { label: 'Previous bottom-panel tab', run: () => _cyclePanelTab(-1) },
|
|
150
|
+
'split:h': { label: 'Split the focused tile horizontally (empty pane)', run: ({ wm }) => { wm.split('h'); } },
|
|
151
|
+
'split:v': { label: 'Split the focused tile vertically (empty pane)', run: ({ wm }) => { wm.split('v'); } },
|
|
152
|
+
'splitWith:h': { label: "Open the focused tile's content in a horizontal split", run: ({ wm }) => { wm.splitFocusedWith('h'); } },
|
|
153
|
+
'splitWith:v': { label: "Open the focused tile's content in a vertical split", run: ({ wm }) => { wm.splitFocusedWith('v'); } },
|
|
154
|
+
'openInTab': { label: "Open the focused tile's content in a new tab", run: ({ wm }) => { wm.openFocusedInTab(); } },
|
|
155
|
+
'openInWindow': { label: "Open the focused tile's content in a new window", run: ({ wm }) => { wm.openFocusedInWindow(); } },
|
|
156
|
+
'close': { label: 'Close the focused tile', run: ({ wm }) => { wm.closeFocused(); } },
|
|
157
|
+
'float': { label: 'Float the focused tile into a window, or dock it back', run: ({ wm }) => { wm.toggleManagedFocused(); } },
|
|
158
|
+
// Bare Alt+letter: browsers open their menu bar on it, mid-flow.
|
|
159
|
+
'suppress': { label: null, run: () => {} },
|
|
160
|
+
...Object.fromEntries(DIRS.flatMap((d) => [
|
|
161
|
+
[`focus:${d}`, { label: `Move focus to the tile ${_where(d)}`, run: ({ wm }) => { wm.focusDir(d); } }],
|
|
162
|
+
[`move:${d}`, { label: `Swap the focused tile with its neighbour ${_where(d)}`, run: ({ wm }) => { wm.moveFocused(d); } }],
|
|
163
|
+
])),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** The action a name refers to, or null. */
|
|
167
|
+
function _action(name) {
|
|
168
|
+
if (typeof name !== 'string') return null;
|
|
169
|
+
if (Object.hasOwn(ACTIONS, name)) return ACTIONS[name];
|
|
170
|
+
const m = /^(nav|desktop|toDesktop):(\d+)$/.exec(name);
|
|
171
|
+
if (!m) return null;
|
|
172
|
+
const n = Number(m[2]);
|
|
173
|
+
if (m[1] === 'nav') {
|
|
174
|
+
return {
|
|
175
|
+
label: `Jump to top-nav page ${n + 1}`,
|
|
79
176
|
// The selector is a CONFIG option, not a constant. An embedder that
|
|
80
177
|
// moves its sections out of the top bar — into an icon rail, say —
|
|
81
|
-
// would otherwise find F1..F8 silently doing nothing
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
178
|
+
// would otherwise find F1..F8 silently doing nothing. Passing
|
|
179
|
+
// `navSelector` is how such an embedder keeps the keys.
|
|
180
|
+
run: ({ navSelector }) => {
|
|
181
|
+
const btns = document.querySelectorAll(navSelector || DEFAULT_NAV_SELECTOR);
|
|
182
|
+
if (n >= btns.length) return false;
|
|
183
|
+
btns[n].click();
|
|
184
|
+
return undefined;
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (m[1] === 'desktop') {
|
|
189
|
+
return { label: `Switch to desktop ${n + 1}`, run: ({ wm }) => { wm.switchDesktop(n); } };
|
|
190
|
+
}
|
|
191
|
+
return { label: `Move the focused tile to desktop ${n + 1}`, run: ({ wm }) => { wm.moveFocusedToDesktop(n); } };
|
|
192
|
+
}
|
|
95
193
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const dir = e.key === 'ArrowRight' ? 1 : -1;
|
|
105
|
-
const next = ((cur < 0 ? 0 : cur) + dir + tabs.length) % tabs.length;
|
|
106
|
-
e.preventDefault();
|
|
107
|
-
tabs[next].click();
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
194
|
+
/** Shift+Left/Right: previous/next bottom-panel tab; nothing without one. */
|
|
195
|
+
function _cyclePanelTab(dir) {
|
|
196
|
+
const tabs = [...document.querySelectorAll('.twm-bp__tabs .twm-bp__tab')];
|
|
197
|
+
if (!tabs.length) return false;
|
|
198
|
+
const cur = tabs.findIndex((t) => t.classList.contains('twm-bp__tab--on'));
|
|
199
|
+
tabs[((cur < 0 ? 0 : cur) + dir + tabs.length) % tabs.length].click();
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
111
202
|
|
|
112
|
-
|
|
113
|
-
if (e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && !inField
|
|
114
|
-
&& (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
|
|
115
|
-
e.preventDefault();
|
|
116
|
-
wm.cycleDesktop(e.key === 'ArrowRight' ? 1 : -1);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
203
|
+
// ══ Chords ═════════════════════════════════════════════════════════════
|
|
119
204
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
205
|
+
const ARROWS = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
|
206
|
+
const DIGITS = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
207
|
+
const F_KEYS = Array.from({ length: 12 }, (_, i) => `F${i + 1}`);
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Every chord the window manager binds: `[chordId, action, worksInFields]`.
|
|
211
|
+
* Alt is never typed into a field, and Ctrl+K / Ctrl+Tab / Esc are not text
|
|
212
|
+
* keys; everything else would steal an editing key, so it is skipped while
|
|
213
|
+
* focus is in an input, a textarea, a select or a contenteditable.
|
|
214
|
+
*
|
|
215
|
+
* `Alt+Letter` is not a key but the family "any Alt+letter no other row
|
|
216
|
+
* binds"; it only prevents the browser's menu-access.
|
|
217
|
+
*/
|
|
218
|
+
const DEFAULT_CHORDS = [
|
|
219
|
+
['Ctrl+K', 'palette', true],
|
|
220
|
+
['Meta+K', 'palette', true],
|
|
221
|
+
['?', 'help', false],
|
|
222
|
+
['Escape', 'palette:close', true],
|
|
223
|
+
...F_KEYS.map((f, i) => [f, `nav:${i}`, false]),
|
|
224
|
+
['Backspace', 'back', false],
|
|
225
|
+
['Alt+H', 'split:h', true],
|
|
226
|
+
['Alt+V', 'split:v', true],
|
|
227
|
+
['Alt+Shift+H', 'splitWith:h', true],
|
|
228
|
+
['Alt+Shift+V', 'splitWith:v', true],
|
|
229
|
+
['Alt+T', 'openInTab', true],
|
|
230
|
+
['Alt+N', 'openInWindow', true],
|
|
231
|
+
['Alt+W', 'close', true],
|
|
232
|
+
['Alt+F', 'float', true],
|
|
233
|
+
...ARROWS.map((k, i) => [`Alt+${k}`, `focus:${DIRS[i]}`, true]),
|
|
234
|
+
...ARROWS.map((k, i) => [`Alt+Shift+${k}`, `move:${DIRS[i]}`, true]),
|
|
235
|
+
...DIGITS.map((d) => [`Alt+${d}`, `desktop:${d - 1}`, true]),
|
|
236
|
+
...DIGITS.map((d) => [`Alt+Shift+${d}`, `toDesktop:${d - 1}`, true]),
|
|
237
|
+
['Ctrl+ArrowLeft', 'desktop:prev', false],
|
|
238
|
+
['Ctrl+ArrowRight', 'desktop:next', false],
|
|
239
|
+
['Shift+ArrowLeft', 'panelTab:prev', false],
|
|
240
|
+
['Shift+ArrowRight', 'panelTab:next', false],
|
|
241
|
+
['Ctrl+Tab', 'tab:next', true],
|
|
242
|
+
['Meta+Tab', 'tab:next', true],
|
|
243
|
+
['Ctrl+Shift+Tab', 'tab:prev', true],
|
|
244
|
+
['Meta+Shift+Tab', 'tab:prev', true],
|
|
245
|
+
['Alt+Letter', 'suppress', false],
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
/** The chords each `KEYMAP_SHORTCUTS` row stands for, row for row. */
|
|
249
|
+
const SHORTCUT_CHORDS = [
|
|
250
|
+
['Ctrl+K', 'Meta+K'],
|
|
251
|
+
['?'],
|
|
252
|
+
['Escape'],
|
|
253
|
+
F_KEYS,
|
|
254
|
+
['Backspace'],
|
|
255
|
+
['Alt+H', 'Alt+V'],
|
|
256
|
+
['Alt+Shift+H', 'Alt+Shift+V'],
|
|
257
|
+
['Alt+T'],
|
|
258
|
+
['Alt+N'],
|
|
259
|
+
['Alt+W'],
|
|
260
|
+
['Alt+F'],
|
|
261
|
+
ARROWS.map((k) => `Alt+${k}`),
|
|
262
|
+
ARROWS.map((k) => `Alt+Shift+${k}`),
|
|
263
|
+
DIGITS.map((d) => `Alt+${d}`),
|
|
264
|
+
DIGITS.map((d) => `Alt+Shift+${d}`),
|
|
265
|
+
['Ctrl+ArrowLeft', 'Ctrl+ArrowRight'],
|
|
266
|
+
['Shift+ArrowLeft', 'Shift+ArrowRight'],
|
|
267
|
+
['Ctrl+Tab', 'Meta+Tab', 'Ctrl+Shift+Tab', 'Meta+Shift+Tab'],
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
const DEFAULTS = new Map(DEFAULT_CHORDS.map(([id, action, inField]) => [id, { action, inField }]));
|
|
271
|
+
const BY_LOWER = new Map([...DEFAULTS.keys()].map((id) => [id.toLowerCase(), id]));
|
|
272
|
+
|
|
273
|
+
const MODIFIER_ORDER = ['Ctrl', 'Meta', 'Alt', 'Shift'];
|
|
274
|
+
const MODIFIER_NAMES = {
|
|
275
|
+
ctrl: 'Ctrl', control: 'Ctrl', meta: 'Meta', cmd: 'Meta', command: 'Meta',
|
|
276
|
+
alt: 'Alt', option: 'Alt', shift: 'Shift',
|
|
277
|
+
};
|
|
278
|
+
const KEY_ALIASES = { esc: 'Escape', left: 'ArrowLeft', right: 'ArrowRight', up: 'ArrowUp', down: 'ArrowDown' };
|
|
279
|
+
|
|
280
|
+
/** Shift is how a `?` is typed, so it is not part of that chord; letters,
|
|
281
|
+
* digits and named keys (Tab, ArrowLeft) keep it. */
|
|
282
|
+
const _shiftCounts = (key) => key.length !== 1 || /^[\p{L}\p{N}]$/u.test(key);
|
|
283
|
+
|
|
284
|
+
function _compose(mods, key) {
|
|
285
|
+
const k = key.length === 1 ? key.toUpperCase() : key;
|
|
286
|
+
const used = MODIFIER_ORDER.filter((m) => mods.has(m) && (m !== 'Shift' || _shiftCounts(key)));
|
|
287
|
+
return [...used, k].join('+');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The chord id a keydown event is read as, in `DEFAULT_CHORDS`' spelling, or
|
|
292
|
+
* null. This is NOT an exact match on the modifier set: it reads an event the
|
|
293
|
+
* way the window manager always has, so a page with no bindings behaves as it
|
|
294
|
+
* did before chords could be rebound. In order, first match wins:
|
|
295
|
+
*
|
|
296
|
+
* Ctrl or Meta (either or both, Alt allowed) + K, no Shift Ctrl+K (Meta+K
|
|
297
|
+
* when Ctrl is not held)
|
|
298
|
+
* Ctrl or Meta (either or both) + Tab, no Alt Ctrl+Tab / Ctrl+Shift+Tab
|
|
299
|
+
* (Meta+… when Ctrl is not held)
|
|
300
|
+
* Esc, any modifiers Escape
|
|
301
|
+
* ?, any modifiers ?
|
|
302
|
+
* Backspace, neither Ctrl nor Meta (Shift and Alt ignored) Backspace
|
|
303
|
+
* F1..F12, Shift+Left/Right, Ctrl+Left/Right exactly those modifiers
|
|
304
|
+
* with Alt held, Ctrl and Meta are ignored:
|
|
305
|
+
* Alt(+Shift)+1..9, +H, +V, +Arrow Shift picks the Alt+Shift+… row
|
|
306
|
+
* Alt+T / N / W / F Shift ignored
|
|
307
|
+
* any other Alt+letter Alt+Letter
|
|
308
|
+
*
|
|
309
|
+
* A disabled chord therefore takes every event read as it: `{Backspace:
|
|
310
|
+
* false}` leaves Shift+Backspace and Alt+Backspace to the page as well.
|
|
311
|
+
*/
|
|
312
|
+
function _chordOfEvent(e) {
|
|
313
|
+
const key = e.key;
|
|
314
|
+
if (!key || key === 'Control' || key === 'Shift' || key === 'Alt' || key === 'Meta') return null;
|
|
315
|
+
const { ctrlKey: ctrl, metaKey: meta, altKey: alt, shiftKey: shift } = e;
|
|
316
|
+
const lower = key.toLowerCase();
|
|
317
|
+
const cm = ctrl ? 'Ctrl' : 'Meta';
|
|
318
|
+
if ((ctrl || meta) && lower === 'k' && !shift) return `${cm}+K`;
|
|
319
|
+
if ((ctrl || meta) && !alt && key === 'Tab') return `${cm}+${shift ? 'Shift+' : ''}Tab`;
|
|
320
|
+
if (key === 'Escape') return 'Escape';
|
|
321
|
+
if (key === '?') return '?';
|
|
322
|
+
if (key === 'Backspace') return ctrl || meta ? null : 'Backspace';
|
|
323
|
+
if (!alt) {
|
|
324
|
+
const only = (m) => ctrl === (m === 'Ctrl') && shift === (m === 'Shift') && !meta;
|
|
325
|
+
if (/^F([1-9]|1[0-2])$/.test(key)) return !ctrl && !meta && !shift ? key : null;
|
|
326
|
+
if (key === 'ArrowLeft' || key === 'ArrowRight') {
|
|
327
|
+
if (only('Shift')) return `Shift+${key}`;
|
|
328
|
+
if (only('Ctrl')) return `Ctrl+${key}`;
|
|
131
329
|
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
const sh = shift ? 'Shift+' : '';
|
|
333
|
+
if (/^[1-9]$/.test(key)) return `Alt+${sh}${key}`;
|
|
334
|
+
if (lower === 'h' || lower === 'v') return `Alt+${sh}${lower.toUpperCase()}`;
|
|
335
|
+
if (lower === 't' || lower === 'n' || lower === 'w' || lower === 'f') return `Alt+${lower.toUpperCase()}`;
|
|
336
|
+
if (ARROWS.includes(key)) return `Alt+${sh}${key}`;
|
|
337
|
+
if (/^[a-z]$/.test(lower)) return 'Alt+Letter';
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** An embedder's chord id in canonical spelling, or null if the keymap does
|
|
342
|
+
* not bind it. Modifiers in any order and case; `Esc` and `Left` are read. */
|
|
343
|
+
function _canonicalChord(id) {
|
|
344
|
+
if (typeof id !== 'string' || !id) return null;
|
|
345
|
+
const parts = id.split('+').map((p) => p.trim());
|
|
346
|
+
const keyRaw = parts.pop();
|
|
347
|
+
if (!keyRaw) return null;
|
|
348
|
+
const mods = new Set();
|
|
349
|
+
for (const p of parts) {
|
|
350
|
+
const m = MODIFIER_NAMES[p.toLowerCase()];
|
|
351
|
+
if (!m) return null;
|
|
352
|
+
mods.add(m);
|
|
353
|
+
}
|
|
354
|
+
const key = KEY_ALIASES[keyRaw.toLowerCase()] ?? keyRaw;
|
|
355
|
+
return BY_LOWER.get(_compose(mods, key).toLowerCase()) ?? null;
|
|
356
|
+
}
|
|
132
357
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
358
|
+
/**
|
|
359
|
+
* The defaults with an embedder's bindings applied: a `Map` of chord id →
|
|
360
|
+
* `{ run, label, inField, isDefault }`, or `{ disabled: true }` for a chord the
|
|
361
|
+
* embedder took away — and what was wrong with `bindings`, for the caller to
|
|
362
|
+
* report.
|
|
363
|
+
*/
|
|
364
|
+
function _resolve(bindings) {
|
|
365
|
+
const table = new Map();
|
|
366
|
+
for (const [id, { action, inField }] of DEFAULTS) {
|
|
367
|
+
const a = _action(action);
|
|
368
|
+
table.set(id, { run: a.run, label: a.label, inField, isDefault: true });
|
|
369
|
+
}
|
|
370
|
+
const problems = [];
|
|
371
|
+
if (bindings == null) return { table, problems };
|
|
372
|
+
if (typeof bindings !== 'object') {
|
|
373
|
+
problems.push('`bindings` must be an object of { chord: action | false | function } — ignored');
|
|
374
|
+
return { table, problems };
|
|
375
|
+
}
|
|
376
|
+
for (const [rawId, value] of Object.entries(bindings)) {
|
|
377
|
+
const id = _canonicalChord(rawId);
|
|
378
|
+
if (!id) { problems.push(`unknown chord "${rawId}" in bindings — ignored`); continue; }
|
|
379
|
+
const { action: defaultAction, inField } = DEFAULTS.get(id);
|
|
380
|
+
if (value === false) {
|
|
381
|
+
table.set(id, { disabled: true });
|
|
382
|
+
} else if (typeof value === 'function') {
|
|
383
|
+
table.set(id, {
|
|
384
|
+
run: (ctx, e) => value(e, { wm: ctx.wm, palette: ctx.palette }),
|
|
385
|
+
label: 'Defined by the application', inField, isDefault: false,
|
|
386
|
+
});
|
|
387
|
+
} else {
|
|
388
|
+
const a = _action(value);
|
|
389
|
+
if (!a) { problems.push(`unknown action ${JSON.stringify(value)} for chord "${rawId}" — ignored`); continue; }
|
|
390
|
+
table.set(id, { run: a.run, label: a.label, inField, isDefault: value === defaultAction });
|
|
164
391
|
}
|
|
392
|
+
}
|
|
393
|
+
return { table, problems };
|
|
394
|
+
}
|
|
165
395
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
396
|
+
/**
|
|
397
|
+
* The shortcuts table after `bindings`: a `KEYMAP_SHORTCUTS` row where nothing
|
|
398
|
+
* it stands for was changed and, for a row that was, one line per chord still
|
|
399
|
+
* bound, naming the action it now runs. A disabled chord is not listed. Pure:
|
|
400
|
+
* it reports nothing (`installKeymap` does).
|
|
401
|
+
*
|
|
402
|
+
* @param {object} [bindings] as for `installKeymap`
|
|
403
|
+
* @returns {ReadonlyArray<{keys: string, action: string}>}
|
|
404
|
+
*/
|
|
405
|
+
export function keymapShortcuts(bindings = null) {
|
|
406
|
+
const { table } = _resolve(bindings);
|
|
407
|
+
const rows = [];
|
|
408
|
+
SHORTCUT_CHORDS.forEach((chords, i) => {
|
|
409
|
+
if (chords.every((id) => table.get(id).isDefault)) { rows.push(KEYMAP_SHORTCUTS[i]); return; }
|
|
410
|
+
for (const id of chords) {
|
|
411
|
+
const b = table.get(id);
|
|
412
|
+
if (b.disabled || !b.label) continue;
|
|
413
|
+
rows.push(Object.freeze({ keys: id, action: b.label }));
|
|
171
414
|
}
|
|
415
|
+
});
|
|
416
|
+
return Object.freeze(rows);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Install the window manager's keyboard shortcuts on `document`.
|
|
421
|
+
*
|
|
422
|
+
* @param {object} cfg
|
|
423
|
+
* @param {object} cfg.wm
|
|
424
|
+
* @param {object} cfg.palette
|
|
425
|
+
* @param {string} [cfg.navSelector] what F1..F12 click (see `nav:N`)
|
|
426
|
+
* @param {object} [cfg.bindings] `{ [chordId]: action | false | fn }` —
|
|
427
|
+
* see the head of this file
|
|
428
|
+
* @param {object} [cfg.logger] told of each problem with `bindings` once,
|
|
429
|
+
* as a warning; `console` when omitted
|
|
430
|
+
* @returns {() => void} the disposer
|
|
431
|
+
*/
|
|
432
|
+
export function installKeymap({ wm, palette, navSelector = null, bindings = null, logger = null } = {}) {
|
|
433
|
+
const { table, problems } = _resolve(bindings);
|
|
434
|
+
const log = logger ?? console;
|
|
435
|
+
for (const p of problems) log.warn?.(`[keymap] ${p}`);
|
|
436
|
+
const ctx = { wm, palette, navSelector };
|
|
437
|
+
|
|
438
|
+
const onKeyDown = (e) => {
|
|
439
|
+
// A component that handled the key keeps it.
|
|
440
|
+
if (e.defaultPrevented) return;
|
|
441
|
+
const chord = _chordOfEvent(e);
|
|
442
|
+
if (!chord) return;
|
|
443
|
+
const b = table.get(chord);
|
|
444
|
+
if (!b || b.disabled) return;
|
|
445
|
+
if (!b.inField && e.target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
|
|
446
|
+
if (b.run(ctx, e) !== false) e.preventDefault();
|
|
172
447
|
};
|
|
173
448
|
|
|
174
449
|
document.addEventListener('keydown', onKeyDown);
|