flexdesk 0.17.18 → 0.18.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 +91 -0
- package/README.md +1 -0
- package/css/flexdesk.css +7547 -5891
- package/css/reset.css +1 -1
- package/css/tokens.css +30 -1
- package/dist/charts.js +83 -13
- package/dist/charts.js.map +2 -2
- package/dist/chunk-3BLIVOFD.js +778 -0
- package/dist/chunk-3BLIVOFD.js.map +7 -0
- package/dist/{chunk-7OL6F2AM.js → chunk-4X4SY3CP.js} +7 -5
- package/dist/chunk-4X4SY3CP.js.map +7 -0
- package/dist/chunk-DNJ3N4T4.js +82 -0
- package/dist/chunk-DNJ3N4T4.js.map +7 -0
- package/dist/{chunk-PGZ4EHSE.js → chunk-HL3IIHU3.js} +501 -359
- package/dist/chunk-HL3IIHU3.js.map +7 -0
- package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
- package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
- package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
- package/dist/{chunk-XJNADRGR.js → chunk-NFZJ6P42.js} +188 -96
- package/dist/chunk-NFZJ6P42.js.map +7 -0
- package/dist/chunk-PAI2ZUWV.js +260 -0
- package/dist/chunk-PAI2ZUWV.js.map +7 -0
- package/dist/chunk-Q7BALEPX.js +527 -0
- package/dist/chunk-Q7BALEPX.js.map +7 -0
- package/dist/{chunk-2Z2ENHR3.js → chunk-QRBOIUU3.js} +7 -8
- package/dist/chunk-QRBOIUU3.js.map +7 -0
- package/dist/chunk-QXAOTGCC.js +40 -0
- package/dist/chunk-QXAOTGCC.js.map +7 -0
- package/dist/chunk-UKP22RUG.js +219 -0
- package/dist/chunk-UKP22RUG.js.map +7 -0
- package/dist/chunk-VLOAI3HS.js +43 -0
- package/dist/chunk-VLOAI3HS.js.map +7 -0
- package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
- package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
- package/dist/editor.js +3 -2
- package/dist/editor.js.map +1 -1
- package/dist/flexdesk.css +7547 -5891
- package/dist/flow.js +29 -97
- package/dist/flow.js.map +3 -3
- package/dist/git.js +1356 -0
- package/dist/git.js.map +7 -0
- package/dist/reset.css +1 -1
- package/dist/tiles.js +20 -15
- package/dist/tiles.js.map +2 -2
- package/dist/tokens.css +30 -1
- package/dist/widgets.js +4146 -1199
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +120 -32
- package/dist/wm.js.map +2 -2
- package/docs/README.md +9 -4
- package/docs/best-practices.md +2 -2
- package/docs/components/charts.md +40 -0
- package/docs/components/chat-shell.md +204 -0
- package/docs/components/conversation.md +10 -0
- package/docs/components/filter-builder.md +292 -0
- package/docs/components/forms-and-dialogs.md +30 -0
- package/docs/components/git.md +355 -0
- package/docs/components/input-fields.md +191 -2
- package/docs/components/keymap.md +8 -0
- package/docs/components/menus.md +86 -2
- package/docs/components/panels-and-desktops.md +16 -5
- package/docs/components/shell.md +108 -1
- package/docs/components/workspace-picker.md +193 -0
- package/docs/concepts/building-and-packaging.md +2 -2
- package/docs/concepts/packages-and-entry-points.md +14 -13
- package/docs/concepts/state-guard.md +3 -3
- package/docs/quickstart.md +1 -1
- package/git.js +13 -0
- package/package.json +6 -1
- package/src/charts/plot_config_editor.js +92 -6
- package/src/charts/plot_grid.js +6 -1
- package/src/flow/kit/dom.js +28 -119
- package/src/flow/kit/step_picker.js +1 -1
- package/src/flow/kit/value_picker.js +1 -1
- package/src/flow/outline/menu.js +1 -1
- package/src/git/diff_viewer.js +212 -0
- package/src/git/diff_window.js +51 -0
- package/src/git/git_graph.js +401 -0
- package/src/git/git_ops.js +61 -0
- package/src/git/git_panel.js +755 -0
- package/src/git/index.js +9 -0
- package/src/git/line_diff.js +202 -0
- package/src/tiles/tile_base.js +5 -4
- package/src/tiling/desktops.js +10 -8
- package/src/tiling/keymap.js +6 -5
- package/src/tiling/kind_taxonomy.js +15 -2
- package/src/tiling/shell.js +124 -17
- package/src/tiling/tile_renderer.js +2 -3
- package/src/ui/components/action_dropdown.js +4 -78
- package/src/ui/components/anchored_layer.js +255 -0
- package/src/ui/components/chat_shell.js +926 -0
- package/src/ui/components/chat_state_machine.js +223 -0
- package/src/ui/components/choice_controls.js +45 -2
- package/src/ui/components/color_field.js +3 -2
- package/src/ui/components/color_list_field.js +3 -2
- package/src/ui/components/combobox.js +422 -0
- package/src/ui/components/context_menu.js +8 -2
- package/src/ui/components/conversation_view.js +17 -2
- package/src/ui/components/data_table.js +2 -2
- package/src/ui/components/filter_builder.js +791 -0
- package/src/ui/components/filter_model.js +682 -0
- package/src/ui/components/inline_renamer.js +10 -5
- package/src/ui/components/managed_window.js +24 -47
- package/src/ui/components/modal.js +104 -117
- package/src/ui/components/option_list.js +160 -0
- package/src/ui/components/popover.js +94 -0
- package/src/ui/components/recent_items.js +169 -0
- package/src/ui/components/select.js +322 -0
- package/src/ui/components/tag_input.js +77 -8
- package/src/ui/components/workspace_picker.js +1253 -0
- package/src/ui/utils/fire_event.js +17 -0
- package/src/ui/utils/focusable.js +25 -0
- package/src/ui/utils/modal_host.js +27 -0
- package/src/ui/utils/tooltip_service.js +81 -14
- package/widgets.js +12 -2
- package/css/base.css +0 -10547
- package/css/overrides.css +0 -2596
- package/dist/chunk-2Z2ENHR3.js.map +0 -7
- package/dist/chunk-7JI7HHSY.js +0 -107
- package/dist/chunk-7JI7HHSY.js.map +0 -7
- package/dist/chunk-7OL6F2AM.js.map +0 -7
- package/dist/chunk-MIW7D6WC.js +0 -526
- package/dist/chunk-MIW7D6WC.js.map +0 -7
- package/dist/chunk-PGZ4EHSE.js.map +0 -7
- package/dist/chunk-XJNADRGR.js.map +0 -7
- /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Git panel, commit graph and diff viewer
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/git` · **Stylesheet:** `flexdesk/css` · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
A source-control panel, an SVG swimlane commit graph and a diff viewer. None of them runs git or reaches a backend: the panel and the graph call the methods of an **adapter** object you pass in, and the [git adapter contract](#the-git-adapter-contract-0180) below says what each method takes and answers, so that a Python, a C# or any other backend can implement it. The diff viewer needs no adapter at all.
|
|
6
|
+
|
|
7
|
+
`@flexdesk/git` is a separate entry, so an application that does not show a repository does not load it. Add it to your import map beside the others:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{ "imports": { "@flexdesk/git": "./vendor/flexdesk/git.js" } }
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## The panel: `createGitPanel` (0.18.0)
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { createGitPanel } from '@flexdesk/git';
|
|
17
|
+
|
|
18
|
+
const panel = createGitPanel(el, {
|
|
19
|
+
adapter, // the contract below; null shows emptyLabel
|
|
20
|
+
onCountChange: (n) => setBadge(n), // how many files have changes
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
panel.setAdapter(otherRepository); // another repository, or null
|
|
24
|
+
panel.stopPolling(); // while its tab is hidden
|
|
25
|
+
panel.startPolling(); // reads at once, then every pollInterval
|
|
26
|
+
await panel.refresh(); // read now
|
|
27
|
+
panel.destroy();
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
From the top, it shows the current branch with its menu (check out, create, delete), ahead and behind counts, Fetch, Pull, Push and a More menu (stash, pop stash, merge, rebase, remotes, tags); a commit box with a Commit button and a menu (amend, commit and push, commit and sync); the changed files, each with Stage or Unstage and, unless it is in conflict, Discard; and the commit graph, collapsed until opened.
|
|
31
|
+
|
|
32
|
+
| Option | Default | |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `adapter` | `null` | The git adapter. `isRepo` and `status` are required: without them it throws a `TypeError` naming what is missing, here and in `setAdapter`. |
|
|
35
|
+
| `onOpenDiff` | a diff window | `({ path, status, staged, renamedFrom?, ref? }) => void`, when a file row or a commit's file is opened (`ref` is the commit). Without it, the file opens in [`openGitDiffWindow`](#a-diff-in-a-window-opendiffwindow-opengitdiffwindow-0180), or nothing happens if the adapter has no `diffFile`. |
|
|
36
|
+
| `onCountChange` | none | `(count) => void`, whenever the number of changed files has been read; 0 when there is nothing to read. |
|
|
37
|
+
| `pollInterval` | `3000` | Milliseconds between status reads. `0` reads only on `refresh()`. A poll that finds the last read still out, or an operation running, waits for the next tick. |
|
|
38
|
+
| `emptyLabel` | `'No repository'` | Shown while there is no adapter. |
|
|
39
|
+
| `changesExpanded` | `true` | |
|
|
40
|
+
| `graphExpanded` | `false` | The graph reads the log only once it has been opened. |
|
|
41
|
+
| `showRemoteRefs` | `false` | As for [`createGitGraph`](#the-commit-graph-creategitgraph-0180). |
|
|
42
|
+
|
|
43
|
+
**What is on offer is what the adapter can do.** An action whose method the adapter lacks is not drawn: with `isRepo` and `status` alone the panel is a read-only view of the branch and the changed files.
|
|
44
|
+
|
|
45
|
+
**What you type is yours.** The panel's elements are built once and updated in place, so a status change never touches the commit box. Its text is cleared when a commit succeeds and when `setAdapter` changes the repository; a refused commit leaves it.
|
|
46
|
+
|
|
47
|
+
**Committing.** With nothing staged, Commit stages everything first (`stage({ paths: 'all' })`). With something staged, it commits that. Amend stages nothing, and an empty message on an amend keeps the old message (`message: null`). Commit and Sync is commit, pull, push, and stops at the first refusal.
|
|
48
|
+
|
|
49
|
+
**A failed operation** is an error toast carrying the backend's words, through FlexDesk's [toast helpers](notifications.md#notifications-notificationcenter), and the panel reads the repository again either way. An answer with `conflicts: true` is titled "… stopped on conflicts". A failed `isRepo` or `status` is said in the panel itself, and asked again at the next poll.
|
|
50
|
+
|
|
51
|
+
**A push with no upstream** asks for one (`setUpstream: true`).
|
|
52
|
+
|
|
53
|
+
**Deleting a branch** the adapter refuses with `notMerged: true` offers to force it, after asking. Any other refusal is an error toast and nothing more.
|
|
54
|
+
|
|
55
|
+
**One operation at a time.** While one runs, the actions in the bar, the branch menu and the commit box's buttons are disabled; an action that still arrives (a file row's, or one waiting on a dialog) does not run, and a toast says so. A row's menu in the graph is not part of this: the graph can stand alone and has no such lock.
|
|
56
|
+
|
|
57
|
+
**Another repository mid-operation.** An operation stays with the adapter it started on, through every step and dialog, whatever `setAdapter` has done since: Commit and Sync finishes on the repository it began on. Afterwards the panel reads whichever adapter it now has. A list still being read for a menu (remotes, tags, a commit's files) is dropped if the adapter changed.
|
|
58
|
+
|
|
59
|
+
**Keyboard.** Ctrl+Enter or Cmd+Enter commits from the box. A file row takes focus, Enter or Space opens it, and its actions show on focus as on hover.
|
|
60
|
+
|
|
61
|
+
## The commit graph: `createGitGraph` (0.18.0)
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import { createGitGraph } from '@flexdesk/git';
|
|
65
|
+
|
|
66
|
+
const graph = createGitGraph(el, {
|
|
67
|
+
adapter, // needs `log`; the rest is used when present
|
|
68
|
+
onOpenDiff: ({ path, status, ref }) => {},
|
|
69
|
+
onChanged: () => refreshTheRestOfMyPage(),
|
|
70
|
+
});
|
|
71
|
+
graph.setAdapter(otherRepository);
|
|
72
|
+
await graph.refresh();
|
|
73
|
+
graph.destroy();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The panel holds one of these; it also stands alone. Give `el` a height: the graph scrolls inside it and asks for the next page when the last row comes into view. Rows are 22 pixels and each draws its own SVG: lines passing through it, the commit's node, and curves to its parents.
|
|
77
|
+
|
|
78
|
+
| Option | Default | |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `adapter` | `null` | The git adapter. `log` is what it draws. |
|
|
81
|
+
| `onOpenDiff` | a diff window | As for the panel; `ref` is always the commit's hash. |
|
|
82
|
+
| `onChanged` | none | Told after a menu operation, so the rest of your page can follow. |
|
|
83
|
+
| `pageSize` | `50` | Commits a page. |
|
|
84
|
+
| `showRemoteRefs` | `false` | Draw remote branches' pills as well. A narrow panel leaves them out; a wide graph has the room. |
|
|
85
|
+
|
|
86
|
+
`refresh()` re-reads as many commits as are loaded and redraws only if something differs, so it costs no scroll position and is safe to call on every status change. A row's context menu (right-click, Shift+F10 or the Menu key) offers what the adapter can do: Checkout, Create Branch, Copy Commit Hash, Cherry-pick, Create Tag and View Changes, which opens the file if the commit changed one and lists them if it changed several.
|
|
87
|
+
|
|
88
|
+
Two pure functions are exported for your own drawing or tests:
|
|
89
|
+
|
|
90
|
+
**`computeGraphLanes(commits)`** answers `{ rows, laneCount }` for commits newest first, children before parents. A row is `{ lane, hasChild, outputLanes: [{ lane, parentHash }], passLanes }`. The rows for a list are the same as the first rows for any longer list that starts with it.
|
|
91
|
+
|
|
92
|
+
**`parseGitRefs(refs)`** reads a `refs` string (below) into `[{ name, type }]`, with `type` one of `'head'`, `'tag'`, `'branch'`, `'remote'`, and `name` short: `refs/remotes/origin/main` is `{ name: 'origin/main', type: 'remote' }`. The full names are what tell it from a local branch called `origin/main`. A ref of any other kind, such as `refs/stash`, is left out, and so is a short name: an adapter that forgets `--decorate=full` gets no pills.
|
|
93
|
+
|
|
94
|
+
The lane colours are the tokens `--twm-git-lane-0` to `--twm-git-lane-7`; a lane past the last takes them round again.
|
|
95
|
+
|
|
96
|
+
## The diff viewer: `createDiffViewer` (0.18.0)
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import { createDiffViewer } from '@flexdesk/git';
|
|
100
|
+
|
|
101
|
+
const viewer = createDiffViewer(el, { before, after });
|
|
102
|
+
|
|
103
|
+
viewer.update({
|
|
104
|
+
before: committedText,
|
|
105
|
+
after: currentText,
|
|
106
|
+
diff: rawDiffText, // optional
|
|
107
|
+
fields: [{ name: 'Priority', from: 'low', to: 'high' }], // optional; you parse them
|
|
108
|
+
});
|
|
109
|
+
viewer.setView('raw'); // 'fields', 'text', 'raw', or null to hand the choice back
|
|
110
|
+
viewer.view(); // what is showing
|
|
111
|
+
viewer.showMessage('Loading…');
|
|
112
|
+
viewer.destroy();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
It has three views, and offers one only when its data is there. The buttons that switch between them are drawn only when there are two or more.
|
|
116
|
+
|
|
117
|
+
| View | Offered when | Draws |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| Fields | `fields` is an array | Each `{ name, from, to }` as the name, the old value, an arrow and the new value. An absent or empty value reads as a dash. It knows nothing of where the values came from: the consumer parses them. |
|
|
120
|
+
| Text | `before` or `after` is a string; or `diff` has something to read | Line by line with old and new line numbers and no diff headers. `before`/`after` are compared here; without them `diff` is read. Unchanged lines beyond the context are one row saying how many. |
|
|
121
|
+
| Raw | `diff` is a string | The text as it came. |
|
|
122
|
+
|
|
123
|
+
Which view shows: the one set by the `view` option, `setView` or a click, while it stays on offer; else Fields if a field changed; else Text; else Raw.
|
|
124
|
+
|
|
125
|
+
| Option or data | Default | |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `before`, `after`, `diff`, `fields` | | As above. `update` takes these too, and replaces all of them. |
|
|
128
|
+
| `beforeStart`, `afterStart` | `1` | The line number of `before`'s and `after`'s first line, when you compare only part of a file. `update` takes these too. |
|
|
129
|
+
| `context` | `3` | Unchanged lines kept around each change. `Infinity` keeps them all. |
|
|
130
|
+
| `maxLines` | `1500` | Past this many differing lines a side, the texts are not compared, and it says so. Unchanged lines at the start and the end do not count, so a long file with a small edit is compared. |
|
|
131
|
+
| `view` | | The view to show while it is on offer. |
|
|
132
|
+
| `labels` | | Words to replace: `fields`, `text`, `raw`, `noChanges`, `noFields`, `tooLong`, and `gap: (hidden) => string`. |
|
|
133
|
+
| `onViewChange` | none | `(view) => void`, when a click changes the view. |
|
|
134
|
+
|
|
135
|
+
Text is always drawn as text, never as markup. Colours are the tokens `--twm-diff-add-bg`, `--twm-diff-add-fg`, `--twm-diff-del-bg` and `--twm-diff-del-fg`.
|
|
136
|
+
|
|
137
|
+
### The line diff, without the viewer (0.18.0)
|
|
138
|
+
|
|
139
|
+
**`computeLineDiff(before, after, { context, maxLines, beforeStart, afterStart })`** answers an array of lines, `[]` when the texts have the same lines (CRLF and LF are the same line end, and a final line end makes no line), or `null` when they are too long to compare.
|
|
140
|
+
|
|
141
|
+
**`parseUnifiedDiff(text)`** reads a unified diff into the same lines. It leaves out the `diff --git`, `index`, `---` and `+++` headers, and draws a `file` line before each file only when the diff has several.
|
|
142
|
+
|
|
143
|
+
**`diffStats(lines)`** answers `{ added, removed }`, for a one-glance summary such as "+2 lines". **`splitLines(text)`** and **`LINE_DIFF_LIMIT`** are exported too.
|
|
144
|
+
|
|
145
|
+
A line is one of:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
{ kind: 'context' | 'add' | 'del', oldLine, newLine, text } // numbers are 1-based; null on the side it is not on
|
|
149
|
+
{ kind: 'gap', oldLine: null, newLine: null, text: '', hidden } // `hidden` unchanged lines left out
|
|
150
|
+
{ kind: 'hunk' | 'meta' | 'file', oldLine: null, newLine: null, text }
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## A diff in a window: `openDiffWindow`, `openGitDiffWindow` (0.18.0)
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
import { openDiffWindow, openGitDiffWindow } from '@flexdesk/git';
|
|
157
|
+
|
|
158
|
+
await openDiffWindow({
|
|
159
|
+
title: 'notes.txt',
|
|
160
|
+
load: async () => ({ before, after }), // what `update` takes, or { ok: false, error }
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await openGitDiffWindow({ adapter, path: 'src/a.txt', status: 'M' });
|
|
164
|
+
await openGitDiffWindow({ adapter, path: 'src/a.txt', status: 'M', ref: hash }); // that commit's change
|
|
165
|
+
await openGitDiffWindow({ adapter, path: 'new.txt', status: 'R', renamedFrom: 'old.txt' });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
A modal holding a viewer, showing "Loading…" until `load` answers. A refusal or a throw is shown in the window. `openDiffWindow` also takes `icon`, `width` (880), `height` (600) and `labels`. The promise settles when the window closes.
|
|
169
|
+
|
|
170
|
+
## The git adapter contract (0.18.0)
|
|
171
|
+
|
|
172
|
+
An adapter is a plain object. It knows which repository it is for, so no payload names one: to show another repository, pass another adapter to `setAdapter`. Every method may answer directly or with a promise.
|
|
173
|
+
|
|
174
|
+
**Every answer** is a JSON object with `ok`:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
{ ok: true, /* the method's own fields */ }
|
|
178
|
+
{ ok: false, error: 'what the backend said' }
|
|
179
|
+
{ ok: false, error: 'what git said', conflicts: true } // merge, rebase, pull, stash pop, cherry-pick: stopped on conflicts
|
|
180
|
+
{ ok: false, error: 'what git said', notMerged: true } // deleteBranch, unforced: the branch is not fully merged
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
A method that throws or rejects is treated as `{ ok: false, error: e.message }`. Field names are exactly as written here, camelCase, for a C# or Python backend as well. A method with no payload below is called with no argument.
|
|
184
|
+
|
|
185
|
+
**Required:** `isRepo` and `status`. **Everything else is optional:** leave a method out and its action is not offered.
|
|
186
|
+
|
|
187
|
+
### Reading
|
|
188
|
+
|
|
189
|
+
| Method | Payload | Answer, besides `ok` |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `isRepo()` | | `isRepo: boolean`. `ok: false` is for when it could not ask, such as git not being installed. |
|
|
192
|
+
| `status()` | | [`StatusResult`](#statusresult) |
|
|
193
|
+
| `branches()` | | `branches: Branch[]` |
|
|
194
|
+
| `log(payload)` | `{ count: number, skip: number }` | `commits: Commit[]`, newest first, children before parents. `[]` for a repository with no commits, not a refusal. |
|
|
195
|
+
| `commitFiles(payload)` | `{ hash: string }` | `files: [{ path: string, status: FileStatus, renamedFrom?: string }]`, what that commit changed against its first parent: `git diff-tree --no-commit-id -r --root -M --name-status --diff-merges=first-parent <hash>`. A copy is `A`. |
|
|
196
|
+
| `diffFile(payload)` | `{ path: string, status?: FileStatus, renamedFrom?: string, ref?: string }` | [`DiffResult`](#diffresult) |
|
|
197
|
+
| `tags()` | | `tags: [{ name: string, hash: string }]` |
|
|
198
|
+
| `remotes()` | | `remotes: [{ name: string, url: string }]` |
|
|
199
|
+
|
|
200
|
+
#### StatusResult
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
{
|
|
204
|
+
ok: true,
|
|
205
|
+
branch: 'main', // the short name; on a detached HEAD, what `git rev-parse --abbrev-ref HEAD` says
|
|
206
|
+
files: [
|
|
207
|
+
{ path: 'src/a.txt', status: 'M', staged: false },
|
|
208
|
+
{ path: 'new_name.txt', status: 'R', staged: true, renamedFrom: 'old_name.txt' },
|
|
209
|
+
],
|
|
210
|
+
ahead: 2, // commits the upstream does not have; 0 without an upstream
|
|
211
|
+
behind: 1, // commits the upstream has and this does not; 0 without an upstream
|
|
212
|
+
hasUpstream: true,
|
|
213
|
+
rebasing: false, // optional: a rebase has stopped; offers Continue and Abort
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
One entry a path, so a file with both staged and unstaged changes is one entry. `path` is relative to the repository, with forward slashes, as plain text: not the quoted form `--porcelain=v1` gives a name with a space unless it is run with `-z`. `staged` is true when the index has a change for the path, whether or not the working copy has another.
|
|
218
|
+
|
|
219
|
+
`FileStatus` is one letter, the first row here that matches the two columns of `git status --porcelain=v1`:
|
|
220
|
+
|
|
221
|
+
| | Meaning | Columns |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `C` | Conflicted | `DD`, `AA`, or `U` in either |
|
|
224
|
+
| `U` | Untracked | `??` |
|
|
225
|
+
| `R` | Renamed; the entry has `renamedFrom` | `R` in either |
|
|
226
|
+
| `A` | Added (a copy too) | `A` in either, or `C` in the index column |
|
|
227
|
+
| `D` | Deleted | `D` in either |
|
|
228
|
+
| `M` | Modified | anything else |
|
|
229
|
+
|
|
230
|
+
The panel compares answers to decide whether to redraw, so answer the same fields in the same order when nothing has changed.
|
|
231
|
+
|
|
232
|
+
#### Branch
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
{
|
|
236
|
+
name: 'feature/x', // what `checkout` takes; a remote branch as `origin/main`, without `remotes/`
|
|
237
|
+
hash: 'a1b2c3d', // short
|
|
238
|
+
isCurrent: false,
|
|
239
|
+
isRemote: false, // its full name starts refs/remotes/; a `/` in the name says nothing
|
|
240
|
+
upstream: 'origin/x', // '' when it has none
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`git for-each-ref --format='%(refname)%09%(objectname:short)%09%(HEAD)%09%(upstream:short)%09%(symref)' refs/heads refs/remotes` gives all of it. Leave out a ref with a `%(symref)`: `refs/remotes/origin/HEAD` is only another name for a branch that is listed anyway, and `git branch -a` calls it `origin`, which reads as a local branch.
|
|
245
|
+
|
|
246
|
+
#### Commit
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
{
|
|
250
|
+
hash: 'a1b2c3d4e5f6…', // full. git log --format: %H
|
|
251
|
+
shortHash: 'a1b2c3d', // %h
|
|
252
|
+
author: 'A Person', // %an
|
|
253
|
+
email: 'a.person@example.org', // %ae
|
|
254
|
+
timestamp: 1700000000, // seconds since the epoch, %at
|
|
255
|
+
subject: 'Name the steps', // %s
|
|
256
|
+
refs: 'HEAD -> refs/heads/main, tag: refs/tags/v1, refs/remotes/origin/main', // %D with --decorate=full, one string; '' when none
|
|
257
|
+
parentHashes: ['b2c3d4e5f6a7…'], // full, %P split on spaces; [] for a root commit
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`log` must list all branches in `--topo-order` for the lanes to be right, and full ref names for the pills to be right: `git log --all --topo-order --decorate=full -n<count> --skip=<skip>`. Without `--decorate=full`, `%D` writes `origin/main` and a local `feature/x` the same way, and nothing can tell them apart.
|
|
262
|
+
|
|
263
|
+
#### DiffResult
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
{ ok: true, before: 'old text', after: 'new text', diff: '@@ -1 +1 @@\n…', language: 'python' }
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Answer `before` and `after`, or `diff`, or all three; a missing side of a new or deleted file is `''`. Without `ref`, the comparison is the last commit against the working copy, that is everything not yet committed, staged or not. With `ref`, it is that commit's first parent against that commit, and the working copy plays no part. With `renamedFrom`, the old side is read at that path, `path` being the new one.
|
|
270
|
+
|
|
271
|
+
| Asked for | `diff` | `before` / `after` |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| No `ref` | `git diff HEAD -- <path>` | `git show HEAD:<path>` / the file on disk |
|
|
274
|
+
| No `ref`, `U` | none: git has nothing to compare | `''` / the file on disk |
|
|
275
|
+
| No `ref`, `R` | `git diff HEAD -M -- <renamedFrom> <path>` | `git show HEAD:<renamedFrom>` / the file on disk |
|
|
276
|
+
| No `ref`, `C` | as for `M`: the file on disk holds the conflict markers | as for `M` |
|
|
277
|
+
| `ref` | `git diff -M <ref>^ <ref> -- [<renamedFrom>] <path>`; for a root commit, `git show --format= <ref> -- <path>` | `git show <ref>^:<renamedFrom or path>` / `git show <ref>:<path>` |
|
|
278
|
+
|
|
279
|
+
`language` is optional and FlexDesk does not read it: it is there for an `onOpenDiff` of yours that opens a code editor's diff view.
|
|
280
|
+
|
|
281
|
+
### Changing the working copy
|
|
282
|
+
|
|
283
|
+
Each answers `{ ok: true }` unless it says otherwise.
|
|
284
|
+
|
|
285
|
+
| Method | Payload | What it does |
|
|
286
|
+
|---|---|---|
|
|
287
|
+
| `init()` | | `git init` |
|
|
288
|
+
| `stage(payload)` | `{ paths: string[] \| 'all' }` | `git add -- <paths>`; `'all'` is `git add -A` |
|
|
289
|
+
| `unstage(payload)` | `{ paths: string[] }` | `git reset HEAD -- <paths>`. A rename comes as both its paths. |
|
|
290
|
+
| `discard(payload)` | `{ path: string, status: FileStatus, renamedFrom?: string }` | Takes the path back to the last commit, in the index and on disk alike, by `status` as below. The panel asks first. |
|
|
291
|
+
| `commit(payload)` | `{ message: string \| null, amend: boolean }` | `git commit -m <message>`, with `--amend` when asked. `message: null` comes only with `amend: true` and keeps the last message (`--no-edit`). Answers `hash: string`, the new commit. The panel stages before calling; this does not. |
|
|
292
|
+
| `stash(payload)` | `{ message?: string }` | `git stash push`, with `-m` when given |
|
|
293
|
+
| `stashPop()` | | `git stash pop`. May answer `conflicts`. |
|
|
294
|
+
|
|
295
|
+
`discard` by status. The panel never sends `C`: a conflict is resolved or the merge aborted.
|
|
296
|
+
|
|
297
|
+
| `status` | Command | |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| `M`, `D` | `git restore --source=HEAD --staged --worktree -- <path>` | `git checkout -- <path>` is not enough: it restores from the index, so a staged change survives. |
|
|
300
|
+
| `R` | `git restore --source=HEAD --staged --worktree -- <renamedFrom> <path>` | Puts the old name back and removes the new one. |
|
|
301
|
+
| `A` | `git rm -f -- <path>` | There is no last commit to go back to: the file goes. The panel's dialog says so. Works before the first commit too, when `git restore --source=HEAD` cannot. |
|
|
302
|
+
| `U` | Remove the file from disk | The panel's dialog says so. |
|
|
303
|
+
|
|
304
|
+
### Branches and history
|
|
305
|
+
|
|
306
|
+
| Method | Payload | What it does |
|
|
307
|
+
|---|---|---|
|
|
308
|
+
| `checkout(payload)` | `{ ref: string }` | `git checkout <ref>`: a branch, a remote branch or a commit's full hash |
|
|
309
|
+
| `createBranch(payload)` | `{ name: string, from?: string }` | `git checkout -b <name> [<from>]`: makes it and switches to it |
|
|
310
|
+
| `deleteBranch(payload)` | `{ name: string, force: boolean }` | `git branch -d <name>`, or `-D` when forced. Answers `notMerged: true` with `ok: false` only when `-d` refused because the branch is not fully merged (git's message, in the C locale, has `not fully merged`), and only then does the panel offer to force it. |
|
|
311
|
+
| `merge(payload)` | `{ branch: string }` | `git merge <branch>`. May answer `conflicts`. |
|
|
312
|
+
| `rebase(payload)` | `{ onto: string }` or `{ continue: true }` or `{ abort: true }` | `git rebase <onto>`, `--continue` or `--abort`. May answer `conflicts`. |
|
|
313
|
+
| `cherryPick(payload)` | `{ hash: string }` | `git cherry-pick <hash>`. May answer `conflicts`. |
|
|
314
|
+
| `createTag(payload)` | `{ name: string, hash?: string }` | `git tag <name> [<hash>]`, on HEAD without one |
|
|
315
|
+
| `deleteTag(payload)` | `{ name: string }` | `git tag -d <name>` |
|
|
316
|
+
|
|
317
|
+
### Remotes
|
|
318
|
+
|
|
319
|
+
| Method | Payload | What it does |
|
|
320
|
+
|---|---|---|
|
|
321
|
+
| `fetch()` | | `git fetch --all` |
|
|
322
|
+
| `pull()` | | `git pull`. May answer `conflicts`. |
|
|
323
|
+
| `push(payload)` | `{ setUpstream: boolean }` | `git push`; with `setUpstream`, `git push -u <remote> <current branch>` |
|
|
324
|
+
| `addRemote(payload)` | `{ name: string, url: string }` | `git remote add` |
|
|
325
|
+
| `removeRemote(payload)` | `{ name: string }` | `git remote remove` |
|
|
326
|
+
|
|
327
|
+
### Writing an adapter
|
|
328
|
+
|
|
329
|
+
Over a pywebview bridge whose methods take the repository in the payload:
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
const adapter = {
|
|
333
|
+
isRepo: () => api.git_is_repo({ repoPath }),
|
|
334
|
+
status: () => api.git_status({ repoPath }),
|
|
335
|
+
log: (p) => api.git_log({ repoPath, ...p }),
|
|
336
|
+
stage: (p) => api.git_stage({ repoPath, ...p }),
|
|
337
|
+
discard: ({ path, status, renamedFrom }) => api.git_discard({ repoPath, path, status, renamedFrom }),
|
|
338
|
+
// …
|
|
339
|
+
};
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Over HTTP, with the answers already in this shape:
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
const call = (name) => (payload) => fetch(`/api/git/${name}`, {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
headers: { 'content-type': 'application/json' },
|
|
348
|
+
body: JSON.stringify(payload ?? {}),
|
|
349
|
+
}).then((r) => r.json());
|
|
350
|
+
|
|
351
|
+
const adapter = Object.fromEntries(['isRepo', 'status', 'branches', 'log', 'stage', 'unstage', 'commit', 'push']
|
|
352
|
+
.map((name) => [name, call(name)]));
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`demo/git.js` is a complete adapter over a repository in memory, and `demo/git.html` shows it.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Input fields: slider and
|
|
1
|
+
# Input fields: slider, tags, select and combobox
|
|
2
2
|
|
|
3
3
|
> **Package:** `@flexdesk/widgets` · **More fields:** the toggle and colour fields are in [Forms and dialogs](forms-and-dialogs.md) · [All docs](../README.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Stand-alone inputs. `SliderField` draws a range input, with an optional number box, readout, default and enable toggle. `createTagInput` draws a list of short strings as removable chips followed by a text box. `createSelect` is a select drawn in the DOM, and `createCombobox` a text box that also offers a list.
|
|
6
6
|
|
|
7
7
|
## SliderField: a number box, a readout, a default and an enable toggle (0.17.0)
|
|
8
8
|
|
|
@@ -70,3 +70,192 @@ tags.dispose();
|
|
|
70
70
|
The handle is `{ element, input, getValue, setValue, focus, dispose }`
|
|
71
71
|
(`input` is `null` when read-only). The styles are the `twm-tag-input` block in
|
|
72
72
|
`flexdesk.css`. `tests/tag_input.test.mjs` covers each of the above in jsdom.
|
|
73
|
+
|
|
74
|
+
## Suggestions and one spelling for tags: `suggestions`, `normalise` (0.18.0)
|
|
75
|
+
|
|
76
|
+
`createTagInput` takes two more options. Neither changes a tag input that does
|
|
77
|
+
not pass them.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
createTagInput(host, {
|
|
81
|
+
value: ['baseline'],
|
|
82
|
+
suggestions: ['baseline', 'stress', 'path-finding'], // or async (query, { signal }) => [...]
|
|
83
|
+
normalise: (s) => s.toLowerCase().replace(/\s+/g, '-'),
|
|
84
|
+
onChange: save,
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- **`suggestions`** offers known tags under the text box: strings, or
|
|
89
|
+
`{ value, label?, description?, icon?, hint? }`. Focus shows them and typing
|
|
90
|
+
narrows them. Whatever their source, a row is in the one spelling, says a tag once and is not
|
|
91
|
+
a tag already in the list. An array is filtered here with the earliest match first and then
|
|
92
|
+
alphabetically. A function is asked with what is in the box, keeps its own order and may
|
|
93
|
+
return a promise, as in [`createCombobox`](#a-text-box-that-also-offers-a-list-createcombobox-0180).
|
|
94
|
+
- The arrow keys walk the rows, and **Enter** or a **comma** takes the active one. With no row
|
|
95
|
+
active, which is how it starts, both still commit what was typed, and so does a blur:
|
|
96
|
+
a list of known tags never stops you adding a new one.
|
|
97
|
+
- **`normalise(text)`** is the one spelling of a tag, handed the trimmed text and applied to the
|
|
98
|
+
value it opens with, to what is typed, to the suggestions and to `setValue`. With it
|
|
99
|
+
`Path Finding` and `path-finding` cannot both exist.
|
|
100
|
+
|
|
101
|
+
Since 0.18.0 the list a tag input opens with, and `setValue`'s, drop a repeat
|
|
102
|
+
unless `allowDuplicates` is set, as typing always did.
|
|
103
|
+
|
|
104
|
+
## A select drawn in the DOM: `createSelect` (0.18.0)
|
|
105
|
+
|
|
106
|
+
A native `<select>` styles down to its closed box: the list belongs to the
|
|
107
|
+
operating system. `createSelect` draws the list itself, so a row can have a glyph,
|
|
108
|
+
a second line, a hint and a reason, under group headings, with a search box and a
|
|
109
|
+
way to type a value nobody listed. `selectBox`, the native one, stays for a list with
|
|
110
|
+
nothing to say in a tight strip.
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
import { createSelect } from '@flexdesk/widgets';
|
|
114
|
+
|
|
115
|
+
const phase = createSelect({
|
|
116
|
+
value: 'review',
|
|
117
|
+
allowNew: true,
|
|
118
|
+
options: [
|
|
119
|
+
{ value: 'draft', label: 'Draft', icon: 'edit', description: 'Not ready for anyone', hint: 'D' },
|
|
120
|
+
{ group: 'In progress', options: [
|
|
121
|
+
{ value: 'review', label: 'Review', icon: 'rate_review' },
|
|
122
|
+
{ value: 'held', label: 'Held', disabled: true, reason: 'needs an owner' },
|
|
123
|
+
] },
|
|
124
|
+
],
|
|
125
|
+
onChange: (value, option) => save(value),
|
|
126
|
+
});
|
|
127
|
+
toolbar.appendChild(phase.el);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| Option | What it does |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `options` | Bare values, `{ value, label?, icon?, description?, hint?, keywords?, disabled?, reason? }`, or `{ group, options }`. `keywords` is searched and never drawn. A function is read each time the list opens and the trigger is painted, for a list that grows while the page lives |
|
|
133
|
+
| `value`, `onChange(value, option)` | `onChange` hears the reader choosing a different value, with the entry as you gave it (`null` for a new value). A value keeps its type: an option whose value is 4 gives 4 |
|
|
134
|
+
| `search` | The search box. By default it is there with `allowNew` or more than seven options; `true`, `false` or `{ placeholder }` decides. It narrows on the label, the value, the description and `keywords`, is emptied on every open, and takes focus |
|
|
135
|
+
| `allowNew`, `newLabel(typed)` | A typed text no option has as its value or label is offered as a last row, "Use “…”" by default, and is the value when chosen |
|
|
136
|
+
| `placeholder` | Shown while nothing is chosen: `null`, `undefined` or `''`, with no option of its own. `setPlaceholder(text)` changes it, for a list that is being fetched |
|
|
137
|
+
| `notFound(value)` | How a value in no option reads on the trigger; as itself by default |
|
|
138
|
+
| `loose` | A stored `'2'` names option `2` |
|
|
139
|
+
| `showIcon` | Draw the chosen option's glyph on the trigger too. On by default |
|
|
140
|
+
| `name` | A hidden input of this name carries the value as text, for a surrounding form, and fires a bubbling `change` when the reader chooses. `el` is then a wrapper round the trigger and the input |
|
|
141
|
+
| `carrier` | That hidden input, yours, instead of `name`: a form template's field that something finds by `[data-f]` or an id. Its attributes are left as they are |
|
|
142
|
+
| `className`, `panelClassName`, `title`, `ariaLabel`, `disabled`, `align` | Extra classes on the trigger and on the list's panel, its tooltip and name, whether it is disabled, and which trigger edge the list lines up with |
|
|
143
|
+
| `emptyText(typed)` | What a list with no rows says |
|
|
144
|
+
|
|
145
|
+
The handle is `{ el, input, value, isOpen, setOptions(options, value?), setPlaceholder(text), setDisabled(on), open(), close(), focus(), destroy() }`.
|
|
146
|
+
`value` reads and writes, and writing does not call `onChange`; so does `setOptions`, which
|
|
147
|
+
leaves the value alone unless you pass one.
|
|
148
|
+
|
|
149
|
+
**The value is never silently changed.** A value in none of the options, or naming a
|
|
150
|
+
disabled one, stays the value and is shown. A list that arrives late or has moved on
|
|
151
|
+
does not turn it into something else on the next save.
|
|
152
|
+
|
|
153
|
+
**Keys.** ArrowDown, ArrowUp, Enter or Space open the list; the arrows walk the rows
|
|
154
|
+
that may be chosen and wrap; Enter takes the active row (Space too, with no search box).
|
|
155
|
+
With no search box Home and End go to the ends and a printable key walks to the
|
|
156
|
+
label it begins. Tab closes the list and goes on from the trigger. Escape closes
|
|
157
|
+
the list and nothing else, so the dialog around it stays open. Focus stays on the
|
|
158
|
+
trigger, or in the search box, and `aria-activedescendant` names the active row.
|
|
159
|
+
|
|
160
|
+
The list is on an [anchored layer](menus.md#an-anchored-layer-positionanchored-openanchoredlayer-0180):
|
|
161
|
+
it is on the page only while open, and `destroy()` takes everything away. The classes are `.twm-select`
|
|
162
|
+
(`--empty`, `.twm-is-open`), `.twm-option-panel` and `.twm-option-list__row` (`--active`;
|
|
163
|
+
`aria-selected` marks the current value). No state is held in a `data-*` attribute.
|
|
164
|
+
|
|
165
|
+
## A text box that also offers a list: `createCombobox` (0.18.0)
|
|
166
|
+
|
|
167
|
+
The text in the box is the value; the list only suggests. For a value that may be
|
|
168
|
+
anything but usually is one of a few known ones: a code, a unit, a name. A field
|
|
169
|
+
whose value is an opaque id with a separate name is `createSelect`'s.
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
import { createCombobox } from '@flexdesk/widgets';
|
|
173
|
+
|
|
174
|
+
const code = createCombobox({
|
|
175
|
+
value: 'gbp',
|
|
176
|
+
options: [
|
|
177
|
+
{ value: 'GBP', label: 'GBP · £', description: 'Pound sterling' },
|
|
178
|
+
{ value: 'CAD', label: 'CAD · $', description: 'Canadian dollar', keywords: 'loonie' },
|
|
179
|
+
],
|
|
180
|
+
normalise: (text) => text.toUpperCase(),
|
|
181
|
+
isCandidate: (text) => /^[A-Z]{3}$/.test(text),
|
|
182
|
+
validate: (value) => (/^[A-Z]{3}$/.test(value) ? null : 'Three letters.'),
|
|
183
|
+
onChange: (value, option) => { model.code = value; },
|
|
184
|
+
});
|
|
185
|
+
form.appendChild(code.el);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- **The box holds the value.** `label` is what the list shows; choosing a row puts its
|
|
189
|
+
`value` in the box.
|
|
190
|
+
- **`onChange(value, option)`** fires on every keystroke and pick (`option` is the
|
|
191
|
+
entry you gave when one was picked, else `null`), so "type a value, click Save"
|
|
192
|
+
cannot save the previous one.
|
|
193
|
+
- **`normalise(text)`** applies to what is reported every time, and rewrites the box
|
|
194
|
+
only when the text is settled (a pick, Enter, Tab or blur), because rewriting
|
|
195
|
+
an input moves the caret. A handed value it changes is reported once at
|
|
196
|
+
construction, so your model does not keep what the box no longer shows.
|
|
197
|
+
- **`autoHighlight`** (on by default) makes the first surviving row active as you
|
|
198
|
+
type, so Enter completes it; the row whose value IS the typed text wins. Since the
|
|
199
|
+
list also searches labels, descriptions and `keywords`, a complete code typed in full
|
|
200
|
+
can sit inside another row's words: **`isCandidate(text)`** says the text is already a
|
|
201
|
+
well-formed value, so that nothing is made active and Enter keeps it.
|
|
202
|
+
- **`validate(value)`** warns under the box once the text is settled, and `error` reads it.
|
|
203
|
+
It does not refuse: the value is still reported.
|
|
204
|
+
- **Enter and Escape** are swallowed while the list is open, and only then: they close
|
|
205
|
+
the list and not the dialog around it.
|
|
206
|
+
|
|
207
|
+
**Options found as the reader types.** `options` may be a function:
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
createCombobox({
|
|
211
|
+
options: async (query, { signal }) => {
|
|
212
|
+
const res = await fetch(`/people?q=${encodeURIComponent(query)}`, { signal });
|
|
213
|
+
return res.json(); // [{ value, label, description? }]
|
|
214
|
+
},
|
|
215
|
+
debounce: 200, // ms before it is asked; 150 by default
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
It does its own filtering and is asked with `''` when the list opens without typing. An
|
|
220
|
+
answer that is no longer the latest is dropped, and `signal` aborts when a newer
|
|
221
|
+
one is asked for. While the first answer is coming the list says `loadingText`; rows
|
|
222
|
+
already drawn stay until the next answer; no rows closes the list; a rejection is logged
|
|
223
|
+
and the list says `errorText`. An answer after Escape does not reopen it.
|
|
224
|
+
|
|
225
|
+
The handle is `{ el, input, value, error, isOpen, setOptions(options), open(text?), close(), focus(), destroy() }`.
|
|
226
|
+
It also takes `name`, `placeholder`, `title`, `ariaLabel`, `disabled`, `className` and `panelClassName`. A pick
|
|
227
|
+
fires a bubbling `change` on the input, for a form around it.
|
|
228
|
+
|
|
229
|
+
**`attachInputOptions(input, options)`** is the list on its own, under a text box of yours.
|
|
230
|
+
`createCombobox` and the tag input's suggestions are this. It takes `options`,
|
|
231
|
+
`onPick(option)` (the entry you gave is `option.source`), `anchor`, `isCurrent`, `autoHighlight`,
|
|
232
|
+
`isCandidate`, `normalise`, `openOnFocus`, `passEnter`, `loadingText`, `errorText`, `debounce`,
|
|
233
|
+
`panelClassName` and `onSettle`, and answers `{ isOpen, open, close, takeActive, setOptions, destroy }`.
|
|
234
|
+
`takeActive()` chooses the active row and says whether there was one, for a key of your own that
|
|
235
|
+
also means "this one", such as the tag input's comma.
|
|
236
|
+
|
|
237
|
+
`tests/select_combobox_popover.test.mjs` covers these and the tag input's options in jsdom.
|
|
238
|
+
|
|
239
|
+
## A list under a name being edited: `attachInlineRenamer` (0.18.1)
|
|
240
|
+
|
|
241
|
+
`attachInlineRenamer`'s `onStart` now runs before the renamer adds its own key listeners, and is handed the input as `{ value, triggerEvent, input }`. So what it attaches hears each key first, which is what [`attachInputOptions`](#a-text-box-that-also-offers-a-list-createcombobox-0180) asks for:
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
const renamer = attachInlineRenamer({
|
|
245
|
+
label,
|
|
246
|
+
onStart: ({ input }) => {
|
|
247
|
+
list = attachInputOptions(input, {
|
|
248
|
+
options: names,
|
|
249
|
+
onPick: (option) => {
|
|
250
|
+
input.value = option.value;
|
|
251
|
+
renamer.commit('pick');
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
onCommit: (name) => save(name),
|
|
256
|
+
onFinish: () => list.destroy(),
|
|
257
|
+
onCancel: () => list.destroy(),
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Enter on an arrow-highlighted row commits the row; with no row highlighted it commits what was typed. Esc with the list open closes only the list, and the next one cancels. Taking the field off the page ends the edit first, so the blur a browser fires for it commits nothing after a commit or an Esc.
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
The window manager binds a set of keyboard chords: F1 to F12, Backspace, Ctrl+K, Ctrl+Tab, Ctrl/Shift+Arrows, the Alt chords, `?` and Esc. `keymap.bindings` takes any of them back or rebinds it. The same page covers what depends on those keys and on how a user moves around: what Back and the breadcrumb do, the content zoom control (`chrome.zoom`), and the shell options `backToOpenList`, `floatActiveTab` and `tabLayout`.
|
|
6
6
|
|
|
7
|
+
## F1..F12 and a page rail (0.18.0)
|
|
8
|
+
|
|
9
|
+
F1..F12 press the Nth page button. With no `navSelector` they now find the
|
|
10
|
+
buttons of a [page rail](shell.md#the-pages-as-a-vertical-rail-chromerail-0180)
|
|
11
|
+
(`chrome.rail`, `mountPageRail`) as well as those of `chrome.topNav`, so a rail
|
|
12
|
+
needs no selector of yours. Pass `navSelector` only for page buttons in markup of
|
|
13
|
+
your own; it still replaces the default.
|
|
14
|
+
|
|
7
15
|
## The digit chords follow the key's position, not its character (0.17.14)
|
|
8
16
|
|
|
9
17
|
Alt+1..9 switch to desktop N and Alt+Shift+1..9 move the focused tile to desktop
|