flexdesk 0.17.2 → 0.17.4
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 +100 -63
- package/README.md +27 -4517
- package/css/base.css +18 -1
- package/css/flexdesk.css +18 -1
- package/dist/{chunk-4A7SJC3T.js → chunk-RKNFBX5K.js} +15 -4
- package/dist/{chunk-4A7SJC3T.js.map → chunk-RKNFBX5K.js.map} +2 -2
- package/dist/flexdesk.css +18 -1
- package/dist/tiles.js +1 -1
- package/dist/widgets.js +59 -26
- package/dist/widgets.js.map +2 -2
- package/docs/README.md +97 -0
- package/docs/best-practices.md +45 -0
- package/docs/components/charts.md +211 -0
- package/docs/components/command-palette.md +102 -0
- package/docs/components/conversation.md +72 -0
- package/docs/components/data-table-grid.md +328 -0
- package/docs/components/data-table.md +596 -0
- package/docs/components/editor.md +22 -0
- package/docs/components/flow-canvas-editor.md +482 -0
- package/docs/components/flow-editors.md +176 -0
- package/docs/components/flow-lane-editor.md +165 -0
- package/docs/components/flow-outline-editor.md +176 -0
- package/docs/components/forms-and-dialogs.md +294 -0
- package/docs/components/input-fields.md +72 -0
- package/docs/components/keymap.md +143 -0
- package/docs/components/layout-widgets.md +163 -0
- package/docs/components/markdown-and-outline.md +106 -0
- package/docs/components/menus.md +86 -0
- package/docs/components/notifications.md +194 -0
- package/docs/components/panels-and-desktops.md +127 -0
- package/docs/components/record-list-and-fact-table.md +128 -0
- package/docs/components/settings-view.md +112 -0
- package/docs/components/shell.md +189 -0
- package/docs/components/slide-out-panel.md +68 -0
- package/docs/components/tiles.md +210 -0
- package/docs/components/wizard.md +54 -0
- package/docs/concepts/building-and-packaging.md +33 -0
- package/docs/concepts/domain-neutrality.md +110 -0
- package/docs/concepts/host-port.md +137 -0
- package/docs/concepts/packages-and-entry-points.md +32 -0
- package/docs/concepts/peer-dependencies.md +42 -0
- package/docs/concepts/state-guard.md +58 -0
- package/docs/concepts/styling.md +47 -0
- package/docs/quickstart.md +179 -0
- package/package.json +2 -1
- package/src/ui/components/notification_history.js +17 -24
- package/src/ui/components/slide_out_panel.js +14 -3
- package/src/ui/notification_center.js +41 -3
- package/src/ui/utils/auto_scrollbars.js +16 -2
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Forms and dialogs
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/widgets` · **See also:** [Input fields](input-fields.md), [Wizard](wizard.md) · [All docs](../README.md)
|
|
4
|
+
|
|
5
|
+
`openForm` opens a modal form and resolves with the values, or with `null` when the user cancels. `openConfirm` asks a yes-or-no question. The field types are `text`, `number`, `password`, `select`, `textarea`, `checkbox`, `toggle`, `picker`, `checklist`, `radio`, `color` and `colorList`.
|
|
6
|
+
|
|
7
|
+
## Forms: `openForm`
|
|
8
|
+
|
|
9
|
+
`openForm({ title, fields, defaults, submitLabel })` (in `@flexdesk/widgets`)
|
|
10
|
+
opens a modal form and resolves with `{ [name]: value }`, or `null` when it is
|
|
11
|
+
cancelled. Fields are `text`, `number`, `password`, `select`, `textarea`,
|
|
12
|
+
`checkbox`, `toggle`, `picker`, `checklist`, `radio`, `color` and `colorList`;
|
|
13
|
+
`{ section: 'Label' }` entries are headings and `{ row: [...] }` entries put
|
|
14
|
+
fields side by side. The additions below are opt-in, with one change every form
|
|
15
|
+
sees, even one that uses none of them: a `select` or `radio` comes back with
|
|
16
|
+
its option's OWN value and type (a numeric option submits `2`, not `'2'`; an
|
|
17
|
+
option whose value is `null` submits `null`), a default of that type selects
|
|
18
|
+
its option (`defaults: { s: 2 }` selects the option whose value is `2`), and an
|
|
19
|
+
option object with no `label` shows its value. A select with no option
|
|
20
|
+
selected still reads and submits `''`.
|
|
21
|
+
|
|
22
|
+
### A form that adapts: `onFieldChange`
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
openForm({
|
|
26
|
+
fields: [
|
|
27
|
+
{ name: 'type', label: 'Type', type: 'select', options: ['bug', 'story'] },
|
|
28
|
+
{ section: 'Story', name: 'story' }, // a NAMED section can be hidden
|
|
29
|
+
{ name: 'points', label: 'Points', type: 'number', required: true },
|
|
30
|
+
],
|
|
31
|
+
onFieldChange: (name, value, api) => {
|
|
32
|
+
const story = api.get('type') === 'story';
|
|
33
|
+
api.setVisible('story', story);
|
|
34
|
+
api.setVisible('points', story);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`onFieldChange(name, value, api)` is called once when the form opens (with
|
|
40
|
+
`name === null`, so the form starts consistent with its defaults) and after
|
|
41
|
+
every field's `change`. `api` addresses fields and named sections by name:
|
|
42
|
+
|
|
43
|
+
- `setVisible(name, on)` hides or shows the row. A hidden row keeps its value
|
|
44
|
+
and is still submitted (a disabled option it holds submits as no value:
|
|
45
|
+
`''` for a select or combobox, `null` for a radio), but it is not asked
|
|
46
|
+
about: its `required` and its
|
|
47
|
+
`validator` stand down until it is shown again.
|
|
48
|
+
- `get(name)` reads the control as it stands: a checkbox as a boolean, a
|
|
49
|
+
select or radio as its option's own value (a select with no option selected
|
|
50
|
+
as `''`, a radio with none as `null`), a combobox as the value of the option
|
|
51
|
+
its text names — exactly what it submits — or as its text when it names
|
|
52
|
+
none, a checklist as the array of chosen values, anything else as its
|
|
53
|
+
string.
|
|
54
|
+
- `set(name, value)` writes it. It does not call `onFieldChange` back, so a
|
|
55
|
+
hook that fills one field from another cannot loop. A value a select or
|
|
56
|
+
radio does not offer, or offers only disabled, clears it (a select then
|
|
57
|
+
submits `''`, a radio `null`).
|
|
58
|
+
- `setOptions(name, options)` replaces the options of a `select` (a
|
|
59
|
+
combobox's suggestions too), `checklist` or `radio` while the form is open
|
|
60
|
+
— see below.
|
|
61
|
+
|
|
62
|
+
A hook that throws is logged and the form carries on.
|
|
63
|
+
|
|
64
|
+
### Hidden rows stay hidden
|
|
65
|
+
|
|
66
|
+
`.twm-modal__row` is a grid, and a grid rule beats the `display: none` that
|
|
67
|
+
the `hidden` attribute gets from the browser, so a hidden row stayed on
|
|
68
|
+
screen. `flexdesk.css` now has `.twm-modal__row[hidden],
|
|
69
|
+
.twm-modal__section[hidden] { display: none !important }`, and `setVisible`
|
|
70
|
+
also sets the row's inline `display`, so it holds under a stylesheet of your
|
|
71
|
+
own as well.
|
|
72
|
+
|
|
73
|
+
### A value you look up: `type: 'picker'`
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
{ name: 'parent', label: 'Parent', type: 'picker', required: true,
|
|
77
|
+
pick: async (current) => ({ value: 'EPIC-0001', label: 'EPIC-0001 — Auth rewrite' }),
|
|
78
|
+
displayFor: (value) => titleOf(value) }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The row shows a read-only box and a search button. The button, or a click on
|
|
82
|
+
the box, awaits `pick(currentValue)`. Return `{ value, label }` (the label is
|
|
83
|
+
shown, the value is submitted), a bare value (shown through `displayFor`, or
|
|
84
|
+
as itself), or `null` to leave the field as it was. `displayFor(value)` gives
|
|
85
|
+
the text for the value the form opens with and for `api.set`. The value is
|
|
86
|
+
kept in a hidden input, which the browser's own `required` check skips, so
|
|
87
|
+
`required: true` on a picker is checked by the form instead. The markup is
|
|
88
|
+
`.twm-picker` > `.twm-picker__display` + `.twm-btn.twm-picker__btn`.
|
|
89
|
+
|
|
90
|
+
### A select you may type into: `create`
|
|
91
|
+
|
|
92
|
+
A `select` with a `create` spec is a type-ahead combobox: pick an option or
|
|
93
|
+
type a new value. `create: { onCreate, hint }` makes the typed value into an
|
|
94
|
+
entity before the form resolves, and shows "↳ will create *hint* …" while you
|
|
95
|
+
type. `create: true` is the plain case: the typed value is submitted as typed,
|
|
96
|
+
with no hint, because nothing is created. Until now `create: true` failed the
|
|
97
|
+
submit with "onCreate is not a function".
|
|
98
|
+
|
|
99
|
+
`tests/open_form.test.mjs` mounts the real `openForm` in jsdom and covers all
|
|
100
|
+
four, plus a plain form submitting as before.
|
|
101
|
+
|
|
102
|
+
### Options: typed, unavailable, grouped, and changed while open (0.17.0)
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
openForm({
|
|
106
|
+
fields: [
|
|
107
|
+
{ name: 'country', label: 'Country', type: 'select', options: ['no', 'it'] },
|
|
108
|
+
{ name: 'town', label: 'Town', type: 'select', options: [] },
|
|
109
|
+
{ name: 'plan', label: 'Plan', type: 'select', options: [
|
|
110
|
+
{ value: 1, label: 'Free' },
|
|
111
|
+
{ value: 2, label: 'Pro', disabled: true, reason: 'needs a licence' },
|
|
112
|
+
{ group: 'Teams', options: [{ value: 10, label: 'Small team' }, { value: 50, label: 'Large team' }] },
|
|
113
|
+
] },
|
|
114
|
+
{ name: 'days', label: 'Days', type: 'checklist', required: true,
|
|
115
|
+
options: [{ value: 'mon', label: 'Mon' }, { value: 'tue', label: 'Tue' }] },
|
|
116
|
+
{ name: 'mode', label: 'Mode', type: 'radio', options: [
|
|
117
|
+
{ value: 'fast', label: 'Fast', description: 'Skips the checks.' },
|
|
118
|
+
{ value: 'safe', label: 'Safe' },
|
|
119
|
+
] },
|
|
120
|
+
],
|
|
121
|
+
onFieldChange: (name, value, api) => {
|
|
122
|
+
if (name === null || name === 'country') api.setOptions('town', townsOf(api.get('country')));
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
// → { country: 'it', town: 'Rome', plan: 10, days: ['mon'], mode: 'fast' }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
An option is a bare value or `{ value, label?, disabled?, reason?,
|
|
129
|
+
description? }`.
|
|
130
|
+
|
|
131
|
+
- **Values keep their type.** An option whose value is the number 10 submits
|
|
132
|
+
10, and `api.get` reads 10 — not `'10'`. A default matches an option of the
|
|
133
|
+
same text, so `defaults: { plan: '10' }` selects 10. A combobox entry typed
|
|
134
|
+
as an option's text submits (and `api.get` reads) that option's value. An
|
|
135
|
+
option whose value IS the given one wins over one that only has its text:
|
|
136
|
+
with options `[1, '1']`, a default `'1'` submits `'1'`.
|
|
137
|
+
- **A disabled option** is shown with its `reason` (in a select's option text
|
|
138
|
+
and tooltip, under a radio, on a chip) and is never the submitted value: a
|
|
139
|
+
default naming it does not select it, `api.set` to it clears a select,
|
|
140
|
+
radio or combobox and leaves a checklist's chip unpressed, the reader
|
|
141
|
+
cannot pick it, a combobox does not suggest it, and a submit
|
|
142
|
+
that finds one selected anyway (a script set `.value`, or every option is
|
|
143
|
+
disabled) is refused with "Not available: *reason*" on the field. In a
|
|
144
|
+
hidden row, which is not asked about, it submits as no value instead.
|
|
145
|
+
- **`{ group, options }`** in a select's list is an `<optgroup>` headed
|
|
146
|
+
*group*. In a checklist or radio it is a `.twm-choice__group` heading over
|
|
147
|
+
that group's chips or rows (a group with no options draws none). A combobox
|
|
148
|
+
suggests a group's enabled options, without headings.
|
|
149
|
+
- **`api.setOptions(name, options)`** replaces the list while the form is
|
|
150
|
+
open and keeps the current value when the new list offers it enabled.
|
|
151
|
+
Otherwise a select falls to its first enabled option, a radio to none and a
|
|
152
|
+
checklist drops what is gone. It does not call `onFieldChange` back.
|
|
153
|
+
- **`type: 'checklist'`** draws toggle chips and submits the array of chosen
|
|
154
|
+
values in the list's order (`[]` for none). `required` means at least one,
|
|
155
|
+
and nothing chosen is "Required." on the field.
|
|
156
|
+
- **`type: 'radio'`** draws one radio row per option, with its `description`
|
|
157
|
+
under it, and submits the chosen value, or `null`. `required` means one is
|
|
158
|
+
chosen.
|
|
159
|
+
|
|
160
|
+
The select, the chips and the radio list are the flow kit's
|
|
161
|
+
(`src/ui/components/choice_controls.js`): the settings panel's `enum` select,
|
|
162
|
+
`enum-chips`, `upstream-columns` and `choice-cards` draw with the same code, so
|
|
163
|
+
a form and a settings panel cannot drift apart. In a form they are classed
|
|
164
|
+
`.twm-flow-togglechips` / `.twm-flow-togglechip` and `.twm-flow-choices` /
|
|
165
|
+
`.twm-flow-choice`; an unavailable option's reason is `.twm-choice__reason`.
|
|
166
|
+
|
|
167
|
+
`tests/open_form_choices.test.mjs` covers each of these in jsdom.
|
|
168
|
+
|
|
169
|
+
### Colours: `type: 'color'` and `type: 'colorList'` (0.17.0)
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
openForm({
|
|
173
|
+
fields: [
|
|
174
|
+
{ name: 'accent', label: 'Accent', type: 'color', default: 'rgb(31,119,180)',
|
|
175
|
+
presets: ['#1f77b4', '#ff7f0e', '#2ca02c'] },
|
|
176
|
+
{ name: 'palette', label: 'Palette', type: 'colorList', default: ['#1f77b4', '#ff7f0e'] },
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
// -> { accent: '#1f77b4', palette: ['#1f77b4', '#ff7f0e'] }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
A `color` row is a swatch that opens the native picker, a hex box kept in step
|
|
183
|
+
with it (picking writes the box; typing a valid hex moves the swatch), and,
|
|
184
|
+
with `presets`, a strip of preset swatches. It submits lowercase `#rrggbb`, or
|
|
185
|
+
`''` when left empty. A `colorList` row is an ordered list of such fields: add,
|
|
186
|
+
remove, and reorder by drag or by the up / down buttons; `presets` are offered
|
|
187
|
+
on every row, and it submits the colours in their current order as an array of
|
|
188
|
+
`#rrggbb`. An `rgb()` / `rgba()` default (or `api.set` value) is shown as its
|
|
189
|
+
hex; alpha is dropped, because the native picker has none. A box that does not
|
|
190
|
+
hold `#rgb` or `#rrggbb` is a field error ("Not a hex colour", with the row
|
|
191
|
+
numbers for a list) and the form does not resolve; `required` on either is
|
|
192
|
+
checked by the form. `api.get` reads a `color` as its box text and a
|
|
193
|
+
`colorList` as the array of its rows' text.
|
|
194
|
+
|
|
195
|
+
Both are also components of their own in `@flexdesk/widgets`:
|
|
196
|
+
`new ColorField({ value, presets, name, label, onChange })` and
|
|
197
|
+
`new ColorListField({ value, presets, newColor, addLabel, name, onChange })`,
|
|
198
|
+
each with `render()`, `getValue()` (null while something typed is not a
|
|
199
|
+
colour), `setValue()`, `focus()` and `dispose()`; `onChange(value, { commit })`
|
|
200
|
+
reports every edit and, with `commit: true`, every committed one.
|
|
201
|
+
`normalizeHex(value)` (`#rgb`, `#rrggbb`, `rgb()` -> `#rrggbb` or null) and
|
|
202
|
+
`isHexColor(value)` are exported with them, and `validateField` (in
|
|
203
|
+
`@flexdesk/tiles`) now rejects a `FieldTypes.COLOR` value that is not a hex
|
|
204
|
+
colour. The markup is `.twm-color-field` > `.twm-color-field__swatch` +
|
|
205
|
+
`.twm-color-field__hex` (+ `.twm-color-field__presets`), and `.twm-color-list`
|
|
206
|
+
rows are `.twm-color-list__row`. `tests/color_field.test.mjs` covers all of it.
|
|
207
|
+
|
|
208
|
+
### Number bounds and fields side by side: `min`, `max` and `{ row }` (0.17.0)
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
openForm({
|
|
212
|
+
fields: [
|
|
213
|
+
{ name: 'steps', label: 'Steps', type: 'number', min: 2, max: 200, step: 1 },
|
|
214
|
+
{ row: [
|
|
215
|
+
{ name: 'lo', label: 'Min', type: 'number', hint: 'lowest value' },
|
|
216
|
+
{ name: 'hi', label: 'Max', type: 'number', min: 0 },
|
|
217
|
+
] },
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
// → { steps: 50, lo: 0, hi: 10 }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- **`min` / `max`** on a `number` field are the input's own `min` and `max`
|
|
224
|
+
attributes, and a value outside them is refused on submit with an error
|
|
225
|
+
under that field: "Must be between 2 and 200." (or "Must be at least 2." /
|
|
226
|
+
"Must be at most 200." with only one). The browser's own range check stops
|
|
227
|
+
such a submit first; the form cancels its bubble and shows the field error
|
|
228
|
+
instead, and it checks the range itself as well, before the field's
|
|
229
|
+
`validator`. An empty field that is not `required` is not out of range. A
|
|
230
|
+
row hidden by `setVisible` drops its bounds until it is shown again, as it
|
|
231
|
+
drops `required`. As with any `<input type="number">`, a `min` is also where
|
|
232
|
+
`step` counts from.
|
|
233
|
+
- **`{ row: [field, ...] }`** puts its fields on one line, sharing the width.
|
|
234
|
+
The row is layout only and has no name: each field in it is an ordinary
|
|
235
|
+
field with its own label, hint, error, `setVisible`, value and submitted
|
|
236
|
+
key, so hiding one hides only that one. The first keeps the form's label
|
|
237
|
+
column; the others size their label to its text. A form narrower than
|
|
238
|
+
360px stacks them (a container query, `twm-form`, on a form that has a row;
|
|
239
|
+
a form without one is not made a container). A form with a row opens at the
|
|
240
|
+
wide (640px) layout. The row is `.twm-modal__field-group`.
|
|
241
|
+
|
|
242
|
+
A form with neither renders exactly as before.
|
|
243
|
+
`tests/open_form_rows_bounds.test.mjs` covers both in jsdom.
|
|
244
|
+
|
|
245
|
+
### A switch: `ToggleField` and `type: 'toggle'` (0.17.0)
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
import { ToggleField } from '@flexdesk/widgets';
|
|
249
|
+
|
|
250
|
+
const autosave = new ToggleField({
|
|
251
|
+
label: 'Autosave', onText: 'On', offText: 'Off', // all optional
|
|
252
|
+
checked: true, compact: false, disabled: false,
|
|
253
|
+
onChange: (on) => settings.set('autosave', on),
|
|
254
|
+
});
|
|
255
|
+
host.appendChild(autosave.render());
|
|
256
|
+
autosave.get(); // true
|
|
257
|
+
autosave.set(false); // does not call onChange
|
|
258
|
+
autosave.setDisabled(true);
|
|
259
|
+
autosave.dispose();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
A track and a knob drawn over a real checkbox with `role="switch"` and an
|
|
263
|
+
`aria-checked` that follows it. Space and Enter toggle the focused switch, a
|
|
264
|
+
click on the track, the state text or the label toggles it, the focused input
|
|
265
|
+
draws a ring on the track, and a disabled switch ignores clicks and keys alike.
|
|
266
|
+
`ariaLabel` names a switch with no visible label. The markup is
|
|
267
|
+
`label.twm-toggle` (`--compact`, `--disabled`) > `.twm-toggle__control` >
|
|
268
|
+
`input.twm-toggle__input` + `.twm-toggle__track` > `.twm-toggle__knob`, then
|
|
269
|
+
`.twm-toggle__state` and `.twm-toggle__label`; `flexdesk.css` styles it from
|
|
270
|
+
tokens alone, with no colour literal.
|
|
271
|
+
|
|
272
|
+
In a form, `{ name, label, type: 'toggle', onText?, offText?, disabled?,
|
|
273
|
+
compact? }` mounts one and submits a boolean, like a checkbox; `api.get` reads
|
|
274
|
+
it as a boolean and `api.set` moves it. `tests/toggle_field.test.mjs` covers
|
|
275
|
+
both.
|
|
276
|
+
|
|
277
|
+
## Confirms show text, not markup (0.17.0)
|
|
278
|
+
|
|
279
|
+
`openConfirm({ title, message, confirmLabel, cancelLabel, danger, icon })`
|
|
280
|
+
resolves `true` or `false`. Its `message` and both labels are **text**: they
|
|
281
|
+
are set with `textContent`, so a `<` shows as a `<`. Until 0.17.0 they went into
|
|
282
|
+
`innerHTML`, and `showDeleteConfirmDialog` / `showCloseConfirmDialog` put
|
|
283
|
+
`itemName` and `itemType` into that message raw: a flow named
|
|
284
|
+
`<img src=x onerror=…>` rendered an image and ran its handler, and a name with
|
|
285
|
+
an unclosed `<` swallowed the rest of the message.
|
|
286
|
+
|
|
287
|
+
A caller that passed markup in `message` now sees the tags; write a `\n` for a
|
|
288
|
+
line break instead (the message body is `white-space: pre-line`).
|
|
289
|
+
`additionalMessage` on `showDeleteConfirmDialog` follows on its own paragraph
|
|
290
|
+
exactly as before. A body that needs real markup is `openModal`'s job: it takes
|
|
291
|
+
the caller's own DOM.
|
|
292
|
+
|
|
293
|
+
`tests/confirm_escaping.test.mjs` opens each of them in jsdom with hostile
|
|
294
|
+
names and labels.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Input fields: slider and tags
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
Two 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.
|
|
6
|
+
|
|
7
|
+
## SliderField: a number box, a readout, a default and an enable toggle (0.17.0)
|
|
8
|
+
|
|
9
|
+
`new SliderField({ fieldId, field, onChange })` (from `@flexdesk/widgets`)
|
|
10
|
+
draws a range input. Its companions are opt-in through `field`; with none of
|
|
11
|
+
them the DOM is the range and the helper line, as it always was.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { SliderField } from '@flexdesk/widgets';
|
|
15
|
+
|
|
16
|
+
const slider = new SliderField({
|
|
17
|
+
fieldId: 'rate',
|
|
18
|
+
field: {
|
|
19
|
+
min: 0, max: 1, step: 0.01, value: 0.25,
|
|
20
|
+
numberInput: true, // an editable box, kept in sync both ways
|
|
21
|
+
readout: (v) => `${Math.round(v * 100)}%`, // or true for the plain value
|
|
22
|
+
defaultValue: 0.25, // --modified state and a reset button
|
|
23
|
+
toggleable: true, disabledValue: null, // an enable checkbox before the slider
|
|
24
|
+
},
|
|
25
|
+
onChange: (value, { immediate }) => save(value),
|
|
26
|
+
});
|
|
27
|
+
host.appendChild(slider.render());
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Field / method | What it does |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `numberInput` | A text box beside the slider. Dragging updates it on every frame; Enter (or leaving the box) commits what was typed, clamped to `[min, max]` and snapped to `step`. Text that is not a finite number (`abc`, empty, `Infinity`) changes nothing and the box shows the value again; Escape does the same |
|
|
33
|
+
| `readout` | A span after the slider: `true` shows the value, a function `(value: number) => string` formats it. It follows the drag live |
|
|
34
|
+
| `defaultValue` | While the value differs from it the root carries `twm-slider-field--modified` and the reset button shows; reset restores it and reports it through `onChange`, like a release |
|
|
35
|
+
| `toggleable`, `enabled`, `disabledValue` | A checkbox before the slider (`enabled: false` starts it off). While off, the inputs are disabled, the root carries `twm-slider-field--off`, and `onChange`/`getValue()` report `disabledValue` (default `null`) |
|
|
36
|
+
| `onChange(value, { immediate })` | Called once per commit: a drag release, a typed Enter, a reset, a toggle. The value is the slider's value as a string, as before. With `emitOnInput` it is also called on every drag frame with `immediate: true` |
|
|
37
|
+
| `setValue(x, { silent })`, `setRange({ min, max, step }, { silent })`, `setEnabled(on, { silent })`, `reset({ silent })` | Change the field from code. Each reports at most once, nothing with `silent: true`, and nothing when the reported value did not move, so a caller that writes back from its own `onChange` does not loop. `setValue` refuses a non-finite value (returns `false`); `setRange` refits the value and the default |
|
|
38
|
+
| `getValue()`, `isEnabled()`, `isModified()` | What `onChange` would report now, the toggle state, and whether the value differs from `defaultValue` |
|
|
39
|
+
|
|
40
|
+
## A list of tags: `createTagInput` (0.17.0)
|
|
41
|
+
|
|
42
|
+
`createTagInput(host, { value, placeholder, readOnly, allowDuplicates, onChange })`
|
|
43
|
+
(in `@flexdesk/widgets`) draws a list of short strings as removable chips
|
|
44
|
+
followed by a text box, and appends it to `host`.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const tags = createTagInput(host, {
|
|
48
|
+
value: ['baseline'],
|
|
49
|
+
placeholder: 'Add a tag…',
|
|
50
|
+
onChange: (list) => save(list), // a fresh copy after each change the reader makes
|
|
51
|
+
});
|
|
52
|
+
tags.getValue(); // ['baseline']
|
|
53
|
+
tags.setValue(['a', 'b']); // redraws; does not call onChange
|
|
54
|
+
tags.dispose();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- **Enter**, a **comma** and **blur** commit what was typed: it is split at
|
|
58
|
+
commas and each piece trimmed, so `a, b` then Enter adds two chips and
|
|
59
|
+
`onChange` receives `['a', 'b']`.
|
|
60
|
+
- Empty pieces are ignored, and so is a piece already in the list unless
|
|
61
|
+
`allowDuplicates` is set. A commit that adds nothing calls no `onChange`.
|
|
62
|
+
- **Backspace** in an empty box removes the last chip.
|
|
63
|
+
- A **paste** that carries a comma is split into chips at once; a paste
|
|
64
|
+
without one is ordinary text in the box.
|
|
65
|
+
- Each chip's remove button has an `aria-label`, "Remove <tag>" by default;
|
|
66
|
+
`removeLabel(tag)` words it (for another language). `label` names the text
|
|
67
|
+
box and the group.
|
|
68
|
+
- `readOnly` draws the chips alone: no remove buttons and no text box.
|
|
69
|
+
|
|
70
|
+
The handle is `{ element, input, getValue, setValue, focus, dispose }`
|
|
71
|
+
(`input` is `null` when read-only). The styles are the `twm-tag-input` block in
|
|
72
|
+
`flexdesk.css`. `tests/tag_input.test.mjs` covers each of the above in jsdom.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Keys, Back and tab layout
|
|
2
|
+
|
|
3
|
+
> **Package:** `@flexdesk/wm` · **See also:** [Command palette](command-palette.md) · [All docs](../README.md)
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## Your keys, not the window manager's: `keymap` (0.17.0)
|
|
8
|
+
|
|
9
|
+
The window manager binds F1..F12, Backspace, Ctrl+K, Ctrl+Tab, Ctrl/Shift+Arrows,
|
|
10
|
+
the Alt chords, `?` and Esc, and swallows any other bare Alt+letter. Each of
|
|
11
|
+
those chords can be taken back or rebound:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
await createShell({
|
|
15
|
+
// …everything above…
|
|
16
|
+
keymap: {
|
|
17
|
+
bindings: {
|
|
18
|
+
'F1': false, // F1 is the page's: not prevented, nothing clicked
|
|
19
|
+
'F2': 'nav:0', // F2 opens the first top-nav kind
|
|
20
|
+
'Backspace': false, // Backspace never calls navigateBack
|
|
21
|
+
'Alt+W': (e, { wm }) => { /* yours; return false to leave e unprevented */ },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A value is `false`, an action name, or a function. The actions are `palette`,
|
|
28
|
+
`palette:close`, `help`, `back`, `nav:N`, `desktop:N`, `toDesktop:N` (all
|
|
29
|
+
counted from 0), `desktop:next|prev`, `tab:next|prev`, `panelTab:next|prev`,
|
|
30
|
+
`split:h|v`, `splitWith:h|v`, `openInTab`, `openInWindow`, `close`, `float`,
|
|
31
|
+
`focus:left|right|up|down`, `move:left|right|up|down` and `suppress`. The chord
|
|
32
|
+
ids are the defaults in `src/tiling/keymap.js`: `Ctrl+K`, `Meta+K`, `?`,
|
|
33
|
+
`Escape`, `F1`..`F12`, `Backspace`, `Alt+H`, `Alt+Shift+H`, `Alt+ArrowLeft`,
|
|
34
|
+
`Alt+Shift+1`, `Ctrl+Tab`, `Shift+ArrowRight` and so on, with `Alt+Letter` for
|
|
35
|
+
the Alt+letter suppression. Modifiers may come in any order and case. A chord id
|
|
36
|
+
or action the keymap does not know is warned about once, through `logger` (else
|
|
37
|
+
`console`), when the shell is built, and is then ignored. Only the window
|
|
38
|
+
manager's own chords can be rebound. To add a new key, put your own listener on
|
|
39
|
+
the page. `keymap` itself must be an object whose only key is `bindings`:
|
|
40
|
+
anything else (a string, a typo such as `{ binding: … }`) makes `createShell`
|
|
41
|
+
throw a `TypeError`, so a binding you meant to change never stays in force
|
|
42
|
+
unnoticed.
|
|
43
|
+
|
|
44
|
+
**A key press is read as loosely as before.** With no bindings every key does
|
|
45
|
+
what it did in 0.16, and a binding takes over every key press its chord was
|
|
46
|
+
read as. Backspace ignores Shift and Alt, so `{ Backspace: false }` also hands
|
|
47
|
+
Shift+Backspace and Alt+Backspace to the page. Alt+T/N/W/F ignore Shift. With
|
|
48
|
+
Alt held, Ctrl and Meta are ignored, so Alt+Ctrl+H is `Alt+H`. Ctrl+K and
|
|
49
|
+
Ctrl+Tab also match with Meta held, and Ctrl wins when both are held:
|
|
50
|
+
Ctrl+Meta+K is `Ctrl+K`, and Meta+K is `Meta+K` only without Ctrl. Esc and `?`
|
|
51
|
+
ignore every modifier. F1..F12, Shift+Left/Right and Ctrl+Left/Right match only
|
|
52
|
+
with exactly those modifiers.
|
|
53
|
+
|
|
54
|
+
**A key a component handled is the component's.** The keymap skips every
|
|
55
|
+
keydown that is already `defaultPrevented`. A list that deletes a row on
|
|
56
|
+
Backspace calls `preventDefault()`, and the window manager does not navigate
|
|
57
|
+
back as well.
|
|
58
|
+
|
|
59
|
+
**`?` is installed by the shell.** Importing `HelpModal` no longer adds a
|
|
60
|
+
`document` listener as a side effect. A shell binds `?` as one of its chords,
|
|
61
|
+
and `shell.dispose()` removes it with the rest. A page that uses the help modal
|
|
62
|
+
without a shell calls `installHelpShortcut()` from `@flexdesk/widgets`, which
|
|
63
|
+
returns its disposer. `HelpModal.toggle()` is what `?` does.
|
|
64
|
+
|
|
65
|
+
**The *Keyboard shortcuts* command shows the bindings in effect.** It lists
|
|
66
|
+
`keymapShortcuts(bindings)` from `@flexdesk/wm`. That is `KEYMAP_SHORTCUTS`
|
|
67
|
+
with each changed row replaced by one line per chord still bound, and no line
|
|
68
|
+
for a chord you disabled. `createCommandPalette({ shortcuts })` takes that table.
|
|
69
|
+
The command's hint names the keys that open help in it: `?` by default, `F9`
|
|
70
|
+
after `{ '?': false, F9: 'help' }`, and none when no key opens help (for
|
|
71
|
+
example after `{ '?': 'palette' }`).
|
|
72
|
+
|
|
73
|
+
**`chrome.zoom`** paints a − / track / + / readout control into the element you
|
|
74
|
+
pass, restores the saved zoom through the host's `state` capability, and scales
|
|
75
|
+
the content of tiles and floating windows. It deliberately scales *content only*
|
|
76
|
+
— never the root, a tile's frame or a window's frame — because CSS `zoom`
|
|
77
|
+
establishes a scaled coordinate space and every window drag, resize and snap
|
|
78
|
+
measures in real pixels. `shell.chrome.zoom` exposes `get()` and `set(percent)`
|
|
79
|
+
for a settings pane. An embedder that builds its own `WindowManager` instead of
|
|
80
|
+
calling `createShell` mounts the same control with `mountZoomControl(el, { root,
|
|
81
|
+
host })`.
|
|
82
|
+
|
|
83
|
+
**`backToOpenList`** is for apps that open a record from a list in a new tab.
|
|
84
|
+
Without it, Back in that record has no history to pop, so it walks the taxonomy
|
|
85
|
+
up and rewrites the record into its parent, and the tile now shows the list
|
|
86
|
+
twice. With it, Back closes the record tab and activates the list it came from:
|
|
87
|
+
the nearest tab to the left, else to the right, that is not a record (no
|
|
88
|
+
`props.id`) and belongs to the same top-nav section. Per-tab history still comes
|
|
89
|
+
first, and a record with no such list beside it still walks up in place.
|
|
90
|
+
|
|
91
|
+
**`floatActiveTab`** is for apps whose tabs are separate records rather than
|
|
92
|
+
views of one pane. The float button, Alt+F and the tile menu take only the tab
|
|
93
|
+
on screen and leave its siblings in the tile, so the window holds one tab and
|
|
94
|
+
draws no strip. Without it the whole pane floats, strip and all.
|
|
95
|
+
|
|
96
|
+
**`tabLayout`** says where a tile draws its tabs. `'bottom'` (the default) is
|
|
97
|
+
the spreadsheet strip under the body, shown from two tabs. `'top'` is the
|
|
98
|
+
editor tab bar between the header and the body, also from two tabs.
|
|
99
|
+
`'header'` (0.8.0) puts that tab bar inside the header in place of the title,
|
|
100
|
+
on every tile, one tab included, with a `+` after the last tab that opens the
|
|
101
|
+
taxonomy root in a new tab (`wm.newTab(leafId)`, the same seed a split uses).
|
|
102
|
+
Pass `newTabButton: false` (0.8.1) to leave the `+` out.
|
|
103
|
+
A panel tile has no tabs and keeps its title. Under `'header'` the tab, not
|
|
104
|
+
the header, is what you drag to move a single-tab pane; the empty part of the
|
|
105
|
+
header still floats the pane on a pull-down or a double-click. Set
|
|
106
|
+
`data-twm-tabs` on the shell's root, or call `wm.setTabLayout()`, to switch
|
|
107
|
+
live; nothing mounted in a tile is unmounted.
|
|
108
|
+
|
|
109
|
+
**Back and the breadcrumb are one rule.** `wm.navigateBack()` (Backspace) and
|
|
110
|
+
`wm.navigateUp(kind, props, { ctx })` (every breadcrumb crumb) share it. With
|
|
111
|
+
`backToOpenList`, a record whose section has a list open closes onto that list;
|
|
112
|
+
otherwise Back walks the taxonomy up and a crumb navigates to its level, in
|
|
113
|
+
place. A crumb naming an entity, such as an epic above a story, opens that
|
|
114
|
+
entity. `navigate` stays the verb for going to exactly a target; use
|
|
115
|
+
`navigateUp` for anything that means "up to this level".
|
|
116
|
+
|
|
117
|
+
A filter or other props passed to `openInPrimary` for a list that is already
|
|
118
|
+
showing are applied to it. A bare open, such as a top-nav click, keeps the tab
|
|
119
|
+
as you left it.
|
|
120
|
+
|
|
121
|
+
**Back in a floating window** always acts on that window, whichever option is
|
|
122
|
+
set. The last click decides: inside a window, Backspace walks that window up;
|
|
123
|
+
anywhere else, it walks the focused tile as before. With `backToOpenList`, a
|
|
124
|
+
record in a window closes onto an open list of its section, first among the
|
|
125
|
+
window's own tabs and then in any tile of its desktop.
|
|
126
|
+
|
|
127
|
+
**A page's own levels come first** (0.4.7). A page can have levels that are not
|
|
128
|
+
tabs, such as a section whose list opens one member in place of itself. Return
|
|
129
|
+
`navigateBack()` from the content factory's mount, and Backspace asks it before
|
|
130
|
+
anything above: it climbs one of the page's own levels and returns `true`. Only
|
|
131
|
+
a strict `true` counts. `false`, any other value, a missing function or a throw
|
|
132
|
+
(logged as a warning) all let Back walk the tab history, the open list and the
|
|
133
|
+
taxonomy exactly as before. It is asked in the same place as the rest of Back:
|
|
134
|
+
the window you last clicked in, else the focused tile. Return
|
|
135
|
+
`canNavigateBack()` as well, and `wm.canNavigateBack()` reports the page's own
|
|
136
|
+
levels, for that same window or tile. `TileRenderer.contentOf(leafId)` reads
|
|
137
|
+
what a tile's factory returned. To draw the page's levels, call
|
|
138
|
+
`renderBreadcrumb(container, segments)` from `@flexdesk/wm`, with
|
|
139
|
+
`segments = [{ label, icon?, onClick? }]`. It uses the tile breadcrumb's
|
|
140
|
+
markup, so it looks the same, and each `onClick` is yours: the last segment,
|
|
141
|
+
and any without `onClick`, is drawn as plain text. The current segment's item
|
|
142
|
+
carries `aria-current="page"`, and every label has a `title` with its full text,
|
|
143
|
+
so a label your CSS truncates still reads whole (0.15.1).
|