@xoong/controls 0.0.41 → 0.0.43
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/package.json +124 -23
- package/src/avatar.js +142 -0
- package/src/controls.css +7 -0
- package/src/date.js +4 -4
- package/src/index.d.ts +436 -0
- package/src/index.js +10 -0
- package/src/inline-edit.js +99 -0
- package/src/otp.js +152 -0
- package/src/progress.js +98 -0
- package/src/range-slider.js +83 -0
- package/src/segmented.js +73 -0
- package/src/slider.js +2 -1
- package/src/split-button.js +42 -0
- package/src/stat.js +58 -0
- package/src/stepper.js +9 -6
- package/src/steps.js +73 -0
- package/src/textarea.js +57 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// Types for @xoong/controls. The package registers custom elements (importing it, or one subpath such as `@xoong/controls/tabs`, is the whole API),
|
|
2
|
+
// so these types describe the elements: an interface per element, the `xo-*` tag map (document.querySelector('xo-tabs') is typed) and the events.
|
|
3
|
+
//
|
|
4
|
+
// ADDING AN ELEMENT takes a few lines in this file:
|
|
5
|
+
// 1. an interface `XoFooElement extends XoElement` (or `XoControl` for a form control) with its attributes/properties and methods;
|
|
6
|
+
// 2. one line in `XoControlsTagMap`: 'xo-foo': XoFooElement;
|
|
7
|
+
// 3. one line per event in `XoControlsEventMap` (only for events that carry a detail): 'xo-foo-change': CustomEvent<{ value: string }>;
|
|
8
|
+
// 4. in package.json `exports`, a subpath with the `types` condition pointing at this file: "./foo": { "types": "./src/index.d.ts", "default": "./src/foo.js" }
|
|
9
|
+
// Properties that mirror an attribute (`static props`) are declared with their runtime type: Boolean -> boolean, Number -> number, String -> string.
|
|
10
|
+
import type { XoElement, XoControl } from '@xoong/core';
|
|
11
|
+
|
|
12
|
+
// ---- shared shapes ------------------------------------------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/** A choice of `xo-combobox` / `xo-tags` as the element holds it. */
|
|
15
|
+
export interface XoOption { value: string; label: string; disabled?: boolean }
|
|
16
|
+
/** What you may assign to `options`: plain strings, or objects (label defaults to the value). */
|
|
17
|
+
export type XoOptionInput = string | { value: string | number; label?: string; disabled?: boolean };
|
|
18
|
+
/** The part of a request schema node the combobox reads (the form binder sets it): an `enum`, or an array of an `enum` for `multiple`. */
|
|
19
|
+
export interface XoSchemaNodeLike { t?: string; enum?: unknown[]; items?: { enum?: unknown[]; [key: string]: unknown }; [key: string]: unknown }
|
|
20
|
+
/** A row of `xo-chart`. */
|
|
21
|
+
export interface XoChartRow { label: string; value: number }
|
|
22
|
+
/** What you may assign to `rows`: numbers (labelled 1, 2, ...) or `{ label, value }` rows. */
|
|
23
|
+
export type XoChartRowInput = number | { label: string | number; value: number | string };
|
|
24
|
+
/** The value of `<xo-date range>`: ISO `YYYY-MM-DD` strings, '' when not chosen. */
|
|
25
|
+
export interface XoDateRange { start: string; end: string }
|
|
26
|
+
/** Consent choices: one boolean per category. */
|
|
27
|
+
export type XoConsentChoices = Record<string, boolean>;
|
|
28
|
+
|
|
29
|
+
// ---- elements -----------------------------------------------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** `<xo-tabs>`: sections with `xo-tab="Label"` become an accessible tab set. */
|
|
32
|
+
export interface XoTabsElement extends XoElement {
|
|
33
|
+
/** Key of the selected tab (the section's `id`, or its position). */
|
|
34
|
+
value: string;
|
|
35
|
+
/** Accessible name of the tab list. */
|
|
36
|
+
label: string;
|
|
37
|
+
/** Select a tab by key (or position); unknown keys are ignored. */
|
|
38
|
+
select(key: string | number, options?: { focus?: boolean }): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `<xo-menu>`: a menu button; with `value` items it is a choice menu. */
|
|
42
|
+
export interface XoMenuElement extends XoElement {
|
|
43
|
+
label: string;
|
|
44
|
+
/** The chosen radio item, in a choice menu. */
|
|
45
|
+
value: string;
|
|
46
|
+
readonly isOpen: boolean;
|
|
47
|
+
/** Open the menu; `at` also moves the focus to the first or last item. */
|
|
48
|
+
open(at?: 'first' | 'last'): void;
|
|
49
|
+
close(options?: { focus?: boolean }): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `<xo-combobox>`: pick a value from a list you can filter by typing; `multiple` picks several (the value is then an array of strings). */
|
|
53
|
+
export interface XoComboboxElement extends XoControl {
|
|
54
|
+
value: string | string[];
|
|
55
|
+
placeholder: string;
|
|
56
|
+
label: string;
|
|
57
|
+
multiple: boolean;
|
|
58
|
+
/** Let the person add a value that is not in the list. */
|
|
59
|
+
creatable: boolean;
|
|
60
|
+
/** Leave filtering to you: listen to `xo-search` and set `options`. */
|
|
61
|
+
remote: boolean;
|
|
62
|
+
/** The choices. Assign strings or objects; reading gives the normalised list. */
|
|
63
|
+
get options(): XoOption[];
|
|
64
|
+
set options(list: readonly XoOptionInput[] | null | undefined);
|
|
65
|
+
/** The schema node of the field (set by the form binder): an `enum` gives the options. */
|
|
66
|
+
schema: XoSchemaNodeLike | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `<xo-tags>`: a combobox of free text values (`multiple` and `creatable` by default). */
|
|
70
|
+
export interface XoTagsElement extends XoComboboxElement {}
|
|
71
|
+
|
|
72
|
+
/** `<xo-carousel>`: scrolling slides with arrows, dots and optional autoplay. */
|
|
73
|
+
export interface XoCarouselElement extends XoElement {
|
|
74
|
+
label: string;
|
|
75
|
+
/** Slides in view at once (also the CSS custom property `--xo-carousel-items`). */
|
|
76
|
+
items: number;
|
|
77
|
+
/** Milliseconds between slides; 0 is off. */
|
|
78
|
+
autoplay: number;
|
|
79
|
+
loop: boolean;
|
|
80
|
+
dots: boolean;
|
|
81
|
+
counter: boolean;
|
|
82
|
+
noArrows: boolean;
|
|
83
|
+
/** Index of the first visible slide. */
|
|
84
|
+
readonly index: number;
|
|
85
|
+
readonly count: number;
|
|
86
|
+
next(): void;
|
|
87
|
+
prev(): void;
|
|
88
|
+
/** Scroll so that slide `i` is the first visible one. */
|
|
89
|
+
goTo(i: number): void;
|
|
90
|
+
play(): void;
|
|
91
|
+
pause(): void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `<xo-switch>`: one setting on or off; the value is a boolean. */
|
|
95
|
+
export interface XoSwitchElement extends XoControl {
|
|
96
|
+
value: boolean;
|
|
97
|
+
checked: boolean;
|
|
98
|
+
label: string;
|
|
99
|
+
/** Sets aria-busy and ignores clicks while a save is running. */
|
|
100
|
+
busy: boolean;
|
|
101
|
+
/** `"sm"` for the small switch. */
|
|
102
|
+
size: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** `<xo-date>`: a date field with a calendar; the value is `YYYY-MM-DD` (a `{ start, end }` object with `range`). */
|
|
106
|
+
export interface XoDateElement extends XoControl {
|
|
107
|
+
value: string | XoDateRange;
|
|
108
|
+
label: string;
|
|
109
|
+
/** Earliest and latest allowed day, `YYYY-MM-DD`. */
|
|
110
|
+
min: string;
|
|
111
|
+
max: string;
|
|
112
|
+
range: boolean;
|
|
113
|
+
placeholder: string;
|
|
114
|
+
/** First day of the week, 1 (Monday) to 7 (Sunday); 0 means the locale's. */
|
|
115
|
+
weekStart: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `<xo-time>`: a time of day, `HH:mm` (24 hours), '' when empty. */
|
|
119
|
+
export interface XoTimeElement extends XoControl {
|
|
120
|
+
value: string;
|
|
121
|
+
label: string;
|
|
122
|
+
/** Minutes between choices (1 to 30, default 5). */
|
|
123
|
+
step: number;
|
|
124
|
+
/** Earliest and latest allowed time, `HH:mm`. */
|
|
125
|
+
min: string;
|
|
126
|
+
max: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** `<xo-slider>`: a number on a track; the value is a number ('' when empty). */
|
|
130
|
+
export interface XoSliderElement extends XoControl {
|
|
131
|
+
value: number | '';
|
|
132
|
+
label: string;
|
|
133
|
+
min: number;
|
|
134
|
+
max: number;
|
|
135
|
+
step: number;
|
|
136
|
+
/** Comma separated names of the stops, for screen readers and the visible value. */
|
|
137
|
+
labels: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** `<xo-stepper>`: a number with minus and plus buttons; the value is a number ('' when empty). */
|
|
141
|
+
export interface XoStepperElement extends XoControl {
|
|
142
|
+
value: number | '';
|
|
143
|
+
label: string;
|
|
144
|
+
min: number;
|
|
145
|
+
max: number;
|
|
146
|
+
step: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** `<xo-password>`: a password field with a show/hide button; the value is a string. */
|
|
150
|
+
export interface XoPasswordElement extends XoControl {
|
|
151
|
+
value: string;
|
|
152
|
+
label: string;
|
|
153
|
+
placeholder: string;
|
|
154
|
+
autocomplete: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** `<xo-copy>`: a read-only value with a Copy button. */
|
|
158
|
+
export interface XoCopyElement extends XoControl {
|
|
159
|
+
value: string;
|
|
160
|
+
label: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** `<xo-file>`: a file field with a drop zone; the value is a `File` ('' when empty), an array of files with `multiple`. */
|
|
164
|
+
export interface XoFileElement extends XoControl {
|
|
165
|
+
value: File | File[] | '';
|
|
166
|
+
label: string;
|
|
167
|
+
/** The native `accept` syntax (`image/*`, `.pdf`). */
|
|
168
|
+
accept: string;
|
|
169
|
+
/** Bytes, or text such as `2MB`, `500KB`. */
|
|
170
|
+
maxSize: string;
|
|
171
|
+
multiple: boolean;
|
|
172
|
+
hint: string;
|
|
173
|
+
/** Upload progress from 0 to 1, shown as a bar; null hides it. */
|
|
174
|
+
progress: number | null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** `<xo-image>`: a lazy image that keeps its space and has a placeholder and a fallback. `load` and `error` are re-fired on the element. */
|
|
178
|
+
export interface XoImageElement extends XoElement {
|
|
179
|
+
src: string;
|
|
180
|
+
srcset: string;
|
|
181
|
+
sizes: string;
|
|
182
|
+
alt: string;
|
|
183
|
+
width: number;
|
|
184
|
+
height: number;
|
|
185
|
+
/** Aspect ratio such as `16/9` or `1.5`. */
|
|
186
|
+
ratio: string;
|
|
187
|
+
/** `cover` (default) or `contain`. */
|
|
188
|
+
fit: string;
|
|
189
|
+
position: string;
|
|
190
|
+
/** For the one image above the fold: eager, high fetch priority. */
|
|
191
|
+
priority: boolean;
|
|
192
|
+
/** `lazy` (default) or `eager`. */
|
|
193
|
+
loading: string;
|
|
194
|
+
/** A src tried once when `src` fails. */
|
|
195
|
+
fallback: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** `<xo-chart>`: a small bar or line chart with the same numbers as a table. */
|
|
199
|
+
export interface XoChartElement extends XoElement {
|
|
200
|
+
/** `bar` (default) or `line`. */
|
|
201
|
+
type: string;
|
|
202
|
+
label: string;
|
|
203
|
+
/** Appended to the values. */
|
|
204
|
+
unit: string;
|
|
205
|
+
/** Column names of the table view. */
|
|
206
|
+
heading: string;
|
|
207
|
+
valueHeading: string;
|
|
208
|
+
/** The rows as JSON text: `[{"label":"Mon","value":12}]`. */
|
|
209
|
+
data: string;
|
|
210
|
+
/** The rows. Assign numbers or `{ label, value }`; reading gives the normalised rows. Can be set again at any time. */
|
|
211
|
+
get rows(): XoChartRow[];
|
|
212
|
+
set rows(rows: readonly XoChartRowInput[] | null | undefined);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** `<xo-accordion>`: `<details>` children that move with the arrow keys; `single` keeps one open. */
|
|
216
|
+
export interface XoAccordionElement extends XoElement {
|
|
217
|
+
single: boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** `<xo-popover>`: a small panel that opens from a button. */
|
|
221
|
+
export interface XoPopoverElement extends XoElement {
|
|
222
|
+
/** A selector for the trigger; by default the element just before the popover. */
|
|
223
|
+
anchor: string;
|
|
224
|
+
label: string;
|
|
225
|
+
open: boolean;
|
|
226
|
+
/** Open it. */
|
|
227
|
+
show(): void;
|
|
228
|
+
toggle(): void;
|
|
229
|
+
/** Close it; `refocus` gives the focus back to the trigger. */
|
|
230
|
+
close(refocus?: boolean): void;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** `<xo-command>`: a command palette (Ctrl/Cmd+K by default) over its link and button children. */
|
|
234
|
+
export interface XoCommandElement extends XoElement {
|
|
235
|
+
/** The key, with Ctrl/Cmd, that opens it (default `k`). */
|
|
236
|
+
shortcut: string;
|
|
237
|
+
label: string;
|
|
238
|
+
open(): void;
|
|
239
|
+
close(): void;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** `<xo-consent>`: a cookie consent bar that remembers the choice. */
|
|
243
|
+
export interface XoConsentElement extends XoElement {
|
|
244
|
+
/** Comma separated category names (`analytics,marketing`). */
|
|
245
|
+
categories: string;
|
|
246
|
+
/** localStorage key (default `xoong-consent`). */
|
|
247
|
+
storage: string;
|
|
248
|
+
label: string;
|
|
249
|
+
/** The saved choices, or null until the visitor has chosen. */
|
|
250
|
+
readonly choices: XoConsentChoices | null;
|
|
251
|
+
/** Show the bar again (cookie settings). */
|
|
252
|
+
open(): void;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** `<xo-theme-toggle>`: chooses the light or the dark theme (`data-theme` on `<html>`). */
|
|
256
|
+
export interface XoThemeToggleElement extends XoElement {
|
|
257
|
+
/** localStorage key (default `xoong-theme`). */
|
|
258
|
+
storage: string;
|
|
259
|
+
/** The theme in effect: the chosen one, else the system's. */
|
|
260
|
+
readonly theme: 'light' | 'dark';
|
|
261
|
+
/** Forget the choice: the system setting decides again. */
|
|
262
|
+
reset(): void;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** `<xo-range-slider>`: a minimum and a maximum on one track. The value is `[min, max]`; the attribute is `value="20,70"`. */
|
|
266
|
+
export interface XoRangeSliderElement extends XoControl {
|
|
267
|
+
label: string;
|
|
268
|
+
min: number;
|
|
269
|
+
max: number;
|
|
270
|
+
step: number;
|
|
271
|
+
minLabel: string;
|
|
272
|
+
maxLabel: string;
|
|
273
|
+
value: [number, number];
|
|
274
|
+
setValue(value: [number, number] | string): void;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** `<xo-segmented>`: a group of buttons where one (or, with `multiple`, several) is chosen. */
|
|
278
|
+
export interface XoSegmentedElement extends XoControl {
|
|
279
|
+
label: string;
|
|
280
|
+
multiple: boolean;
|
|
281
|
+
clearable: boolean;
|
|
282
|
+
value: string | string[];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** `<xo-split-button>`: a main button with a menu of related actions. */
|
|
286
|
+
export interface XoSplitButtonElement extends XoElement {
|
|
287
|
+
/** Accessible name of the arrow button. */
|
|
288
|
+
more: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** `<xo-textarea>`: a textarea with a character counter and a progress bar. */
|
|
292
|
+
export interface XoTextareaElement extends XoControl {
|
|
293
|
+
label: string;
|
|
294
|
+
maxlength: number;
|
|
295
|
+
rows: number;
|
|
296
|
+
placeholder: string;
|
|
297
|
+
value: string;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** `<xo-stat>`: a KPI tile. */
|
|
301
|
+
export interface XoStatElement extends XoElement {
|
|
302
|
+
label: string;
|
|
303
|
+
value: string;
|
|
304
|
+
delta: number;
|
|
305
|
+
deltaLabel: string;
|
|
306
|
+
/** Comma-separated numbers for the sparkline. */
|
|
307
|
+
series: string;
|
|
308
|
+
/** `down-is-good` when a fall is an improvement. */
|
|
309
|
+
trend: string;
|
|
310
|
+
/** The sparkline numbers as an array (set it instead of `series`). */
|
|
311
|
+
seriesData: number[];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** `<xo-otp>`: one box per digit. The value is the digit string. */
|
|
315
|
+
export interface XoOtpElement extends XoControl {
|
|
316
|
+
label: string;
|
|
317
|
+
length: number;
|
|
318
|
+
value: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** `<xo-avatar>`: a round badge with initials or a picture. */
|
|
322
|
+
export interface XoAvatarElement extends XoElement {
|
|
323
|
+
name: string;
|
|
324
|
+
src: string;
|
|
325
|
+
size: 'sm' | 'lg' | '';
|
|
326
|
+
status: 'ok' | 'warn' | 'muted' | '';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** `<xo-avatars>`: a stack of avatars with a +N badge. */
|
|
330
|
+
export interface XoAvatarsElement extends XoElement {
|
|
331
|
+
max: number;
|
|
332
|
+
label: string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** `<xo-progress>`: a progress bar; set `value = null` for indeterminate. */
|
|
336
|
+
export interface XoProgressElement extends XoElement {
|
|
337
|
+
value: number | null;
|
|
338
|
+
max: number;
|
|
339
|
+
label: string;
|
|
340
|
+
thin: boolean;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** `<xo-spinner>`: a loading spinner (`label=""` makes it decorative). */
|
|
344
|
+
export interface XoSpinnerElement extends XoElement {
|
|
345
|
+
label: string;
|
|
346
|
+
size: 'md' | 'lg' | '';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** `<xo-steps>`: progress through a flow; the children are the step titles. */
|
|
350
|
+
export interface XoStepsElement extends XoElement {
|
|
351
|
+
/** Index of the current step (from 0). */
|
|
352
|
+
current: number;
|
|
353
|
+
label: string;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** `<xo-inline-edit>`: text renamed in place; Enter saves, Escape cancels. */
|
|
357
|
+
export interface XoInlineEditElement extends XoControl {
|
|
358
|
+
label: string;
|
|
359
|
+
value: string;
|
|
360
|
+
/** Start editing. */
|
|
361
|
+
edit(): void;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// `xo-tooltip="text"` is an attribute on any element, not an element: it has no interface.
|
|
365
|
+
|
|
366
|
+
// ---- registries ---------------------------------------------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
/** Every element of this package by tag name. Add one line per element. */
|
|
369
|
+
export interface XoControlsTagMap {
|
|
370
|
+
'xo-tabs': XoTabsElement;
|
|
371
|
+
'xo-menu': XoMenuElement;
|
|
372
|
+
'xo-combobox': XoComboboxElement;
|
|
373
|
+
'xo-tags': XoTagsElement;
|
|
374
|
+
'xo-carousel': XoCarouselElement;
|
|
375
|
+
'xo-switch': XoSwitchElement;
|
|
376
|
+
'xo-date': XoDateElement;
|
|
377
|
+
'xo-time': XoTimeElement;
|
|
378
|
+
'xo-slider': XoSliderElement;
|
|
379
|
+
'xo-stepper': XoStepperElement;
|
|
380
|
+
'xo-password': XoPasswordElement;
|
|
381
|
+
'xo-copy': XoCopyElement;
|
|
382
|
+
'xo-file': XoFileElement;
|
|
383
|
+
'xo-image': XoImageElement;
|
|
384
|
+
'xo-chart': XoChartElement;
|
|
385
|
+
'xo-accordion': XoAccordionElement;
|
|
386
|
+
'xo-popover': XoPopoverElement;
|
|
387
|
+
'xo-command': XoCommandElement;
|
|
388
|
+
'xo-consent': XoConsentElement;
|
|
389
|
+
'xo-theme-toggle': XoThemeToggleElement;
|
|
390
|
+
'xo-range-slider': XoRangeSliderElement;
|
|
391
|
+
'xo-segmented': XoSegmentedElement;
|
|
392
|
+
'xo-split-button': XoSplitButtonElement;
|
|
393
|
+
'xo-textarea': XoTextareaElement;
|
|
394
|
+
'xo-stat': XoStatElement;
|
|
395
|
+
'xo-otp': XoOtpElement;
|
|
396
|
+
'xo-avatar': XoAvatarElement;
|
|
397
|
+
'xo-avatars': XoAvatarsElement;
|
|
398
|
+
'xo-progress': XoProgressElement;
|
|
399
|
+
'xo-spinner': XoSpinnerElement;
|
|
400
|
+
'xo-steps': XoStepsElement;
|
|
401
|
+
'xo-inline-edit': XoInlineEditElement;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The events that carry data (all bubble), by name. Add one line per event. */
|
|
405
|
+
export interface XoControlsEventMap {
|
|
406
|
+
'xo-tab-change': CustomEvent<{ value: string }>;
|
|
407
|
+
'xo-menu-select': CustomEvent<{ value: string }>;
|
|
408
|
+
'xo-menu-open': CustomEvent<null>;
|
|
409
|
+
'xo-menu-close': CustomEvent<null>;
|
|
410
|
+
/** `xo-combobox` with `remote`: the person typed; set `options`. */
|
|
411
|
+
'xo-search': CustomEvent<{ query: string }>;
|
|
412
|
+
'xo-carousel-change': CustomEvent<{ index: number; count: number }>;
|
|
413
|
+
'xo-file-reject': CustomEvent<{ file: File; reason: 'type' | 'size' }>;
|
|
414
|
+
'xo-command-run': CustomEvent<{ value: string; label: string }>;
|
|
415
|
+
/** The visitor chose; the detail is the choices. */
|
|
416
|
+
'xo-consent': CustomEvent<XoConsentChoices>;
|
|
417
|
+
'xo-theme-change': CustomEvent<{ theme: 'light' | 'dark' }>;
|
|
418
|
+
'xo-popover-open': Event;
|
|
419
|
+
'xo-popover-close': Event;
|
|
420
|
+
'xo-accordion-change': CustomEvent<{ index: number; open: boolean }>;
|
|
421
|
+
/** `xo-copy`: the text was copied / the browser refused the clipboard. */
|
|
422
|
+
'copied': CustomEvent<{ text: string }>;
|
|
423
|
+
'copy-failed': CustomEvent<{ text: string }>;
|
|
424
|
+
/** `xo-otp`: every box is filled. */
|
|
425
|
+
'complete': CustomEvent<{ value: string }>;
|
|
426
|
+
/** `xo-inline-edit`. */
|
|
427
|
+
'save': CustomEvent<{ value: string }>;
|
|
428
|
+
'cancel': Event;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
declare global {
|
|
432
|
+
interface HTMLElementTagNameMap extends XoControlsTagMap {}
|
|
433
|
+
interface HTMLElementEventMap extends XoControlsEventMap {}
|
|
434
|
+
// The events bubble, so a listener on the document is typed too.
|
|
435
|
+
interface DocumentEventMap extends XoControlsEventMap {}
|
|
436
|
+
}
|
package/src/index.js
CHANGED
|
@@ -19,3 +19,13 @@ import './consent.js';
|
|
|
19
19
|
import './file.js';
|
|
20
20
|
import './command.js';
|
|
21
21
|
import './chart.js';
|
|
22
|
+
import './range-slider.js';
|
|
23
|
+
import './segmented.js';
|
|
24
|
+
import './split-button.js';
|
|
25
|
+
import './textarea.js';
|
|
26
|
+
import './stat.js';
|
|
27
|
+
import './otp.js';
|
|
28
|
+
import './avatar.js';
|
|
29
|
+
import './progress.js';
|
|
30
|
+
import './steps.js';
|
|
31
|
+
import './inline-edit.js';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* <xo-inline-edit> is a piece of text (a title, a name) that the person can rename in place.
|
|
4
|
+
*
|
|
5
|
+
* <xo-inline-edit name="title" value="Greece Golden Visa" label="Rename"></xo-inline-edit>
|
|
6
|
+
*
|
|
7
|
+
* - At rest it draws `.xo-inline-edit` with the text and a ghost icon button (a pencil) named by `label` ("Rename" by default). A click on the
|
|
8
|
+
* text or on the button starts editing: the text is replaced by an input (named by `label`), focused with everything selected.
|
|
9
|
+
* - Enter saves, Escape cancels, leaving the input (blur) saves. Focus goes back to the button after Enter and Escape.
|
|
10
|
+
* An empty text cancels (the old value stays). The text is trimmed.
|
|
11
|
+
* - `value` is the string. When the person saves a changed text: `input`, `change` and `save` (detail `{ value }`) fire. `cancel` fires when
|
|
12
|
+
* editing is abandoned (Escape, or an empty text). Saving an unchanged text just closes the editor with no event.
|
|
13
|
+
* - `edit()` starts editing from code. `disabled` and `readonly` hide the pencil's action: the text is shown and nothing can be edited.
|
|
14
|
+
*/
|
|
15
|
+
import { XoControl, define, text } from '@xoong/core';
|
|
16
|
+
|
|
17
|
+
class XoInlineEdit extends XoControl {
|
|
18
|
+
static props = { ...XoControl.props, label: String };
|
|
19
|
+
|
|
20
|
+
/** @type {HTMLElement | null} */ #root = null;
|
|
21
|
+
/** @type {HTMLElement | null} */ #text = null;
|
|
22
|
+
/** @type {HTMLButtonElement | null} */ #button = null;
|
|
23
|
+
/** @type {HTMLInputElement | null} */ #input = null;
|
|
24
|
+
|
|
25
|
+
setValue(v) { super.setValue(v == null ? '' : String(v)); this.#paintText(); }
|
|
26
|
+
reset() { super.reset(); this.#paintText(); }
|
|
27
|
+
get focusTarget() { return this.#input ?? this.#button; }
|
|
28
|
+
/** True while the input is shown. */
|
|
29
|
+
get editing() { return !!this.#input; }
|
|
30
|
+
|
|
31
|
+
connected() {
|
|
32
|
+
const root = document.createElement('span');
|
|
33
|
+
root.className = 'xo-inline-edit';
|
|
34
|
+
const label = document.createElement('span');
|
|
35
|
+
label.className = 'xo-inline-edit__text';
|
|
36
|
+
label.addEventListener('click', () => this.edit());
|
|
37
|
+
const button = document.createElement('button');
|
|
38
|
+
button.type = 'button';
|
|
39
|
+
button.className = 'ghost icon-only sm';
|
|
40
|
+
button.innerHTML = '<span class="ic" data-ic="edit" aria-hidden="true"></span>';
|
|
41
|
+
button.addEventListener('click', () => this.edit());
|
|
42
|
+
root.append(label, button);
|
|
43
|
+
this.#root = root; this.#text = label; this.#button = button;
|
|
44
|
+
this.replaceChildren(root);
|
|
45
|
+
queueMicrotask(() => this.#paintText());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
attributeChangedCallback(attr, old, value) { super.attributeChangedCallback(attr, old, value); this.#paintText(); }
|
|
49
|
+
|
|
50
|
+
/** Start editing: an input replaces the text, focused with the text selected. */
|
|
51
|
+
edit() {
|
|
52
|
+
const root = this.#root, label = this.#text, button = this.#button;
|
|
53
|
+
if (!root || !label || !button || this.#input || this.isDisabled || this.readonly) return;
|
|
54
|
+
const input = document.createElement('input');
|
|
55
|
+
input.type = 'text';
|
|
56
|
+
input.value = String(super.value);
|
|
57
|
+
input.setAttribute('aria-label', this.label || text('xoong.inlineEdit.rename', 'Rename'));
|
|
58
|
+
input.addEventListener('input', (e) => e.stopPropagation());
|
|
59
|
+
input.addEventListener('change', (e) => e.stopPropagation());
|
|
60
|
+
input.addEventListener('keydown', (e) => {
|
|
61
|
+
if (e.key === 'Enter') { e.preventDefault(); this.#finish(true, true); }
|
|
62
|
+
else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); this.#finish(false, true); }
|
|
63
|
+
});
|
|
64
|
+
input.addEventListener('blur', () => this.#finish(true, false));
|
|
65
|
+
this.#input = input;
|
|
66
|
+
root.replaceChildren(input); // removed rather than hidden: a button's own display rule would beat [hidden]
|
|
67
|
+
input.focus();
|
|
68
|
+
input.select();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @param {boolean} save @param {boolean} refocus */
|
|
72
|
+
#finish(save, refocus) {
|
|
73
|
+
const input = this.#input, label = this.#text, button = this.#button;
|
|
74
|
+
if (!input || !label || !button) return;
|
|
75
|
+
this.#input = null; // first: removing the focused input may fire blur
|
|
76
|
+
const next = input.value.trim();
|
|
77
|
+
this.#root?.replaceChildren(label, button);
|
|
78
|
+
if (save && next !== '') {
|
|
79
|
+
if (next !== super.value) {
|
|
80
|
+
this.userInput(next);
|
|
81
|
+
this.userChange();
|
|
82
|
+
this.emit('save', { value: next });
|
|
83
|
+
}
|
|
84
|
+
} else this.emit('cancel', {});
|
|
85
|
+
this.#paintText();
|
|
86
|
+
if (refocus) button.focus();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#paintText() {
|
|
90
|
+
const label = this.#text, button = this.#button;
|
|
91
|
+
if (!label || !button) return;
|
|
92
|
+
label.textContent = String(super.value);
|
|
93
|
+
const name = this.label || text('xoong.inlineEdit.rename', 'Rename');
|
|
94
|
+
button.setAttribute('aria-label', name);
|
|
95
|
+
button.title = name;
|
|
96
|
+
button.disabled = this.isDisabled || this.readonly;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
define('xo-inline-edit', XoInlineEdit);
|