@timmo001/groundwork-ui 0.3.1 → 0.4.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/README.md +10 -1
- package/custom-elements.json +20865 -0
- package/package.json +7 -2
- package/src/events.ts +44 -0
- package/src/fields.ts +34 -0
- package/src/gw-app-bar.ts +343 -0
- package/src/gw-command-menu.ts +1 -1
- package/src/gw-dialog.ts +1 -1
- package/src/gw-expansion.ts +1 -1
- package/src/gw-menu-button.ts +1 -1
- package/src/gw-notification-center.ts +1 -1
- package/src/gw-notification.ts +1 -1
- package/src/gw-option-list.ts +51 -5
- package/src/gw-popover.ts +1 -1
- package/src/gw-segmented.ts +1 -1
- package/src/gw-select.ts +87 -9
- package/src/gw-theme-picker.ts +200 -0
- package/src/index.ts +6 -0
- package/src/on.ts +52 -0
package/src/gw-select.ts
CHANGED
|
@@ -36,9 +36,14 @@ const chevronIcon =
|
|
|
36
36
|
* with Enter. Below 600px wide the list is a bottom sheet.
|
|
37
37
|
*
|
|
38
38
|
* Pass `options`, or write `<option>` children (in `<optgroup>`s for
|
|
39
|
-
* groups, with `data-icon` and `data-description`), which it
|
|
40
|
-
* it first connects. A hidden input carries the value in
|
|
41
|
-
* `change` when the value changes.
|
|
39
|
+
* groups, with `data-icon`, `data-swatch` and `data-description`), which it
|
|
40
|
+
* reads once when it first connects. A hidden input carries the value in
|
|
41
|
+
* forms, and it fires `change` when the value changes.
|
|
42
|
+
*
|
|
43
|
+
* With `multiple` it picks several: each pick toggles an option in `values`
|
|
44
|
+
* and the list stays open, the trigger shows the first choice and how many
|
|
45
|
+
* more, and a Clear button under the list empties it. A hidden input per
|
|
46
|
+
* value carries them in forms.
|
|
42
47
|
*
|
|
43
48
|
* @element gw-select
|
|
44
49
|
* @summary A labeled picker with search, a bottom sheet on phones.
|
|
@@ -46,6 +51,8 @@ const chevronIcon =
|
|
|
46
51
|
* @attr {string} label - The field's label.
|
|
47
52
|
* @attr {string} name - The form field name.
|
|
48
53
|
* @attr {string} value - The chosen option's value.
|
|
54
|
+
* @attr {boolean} multiple - Picks several options, kept in `values`.
|
|
55
|
+
* @attr {string[]} values - The chosen options' values with `multiple`, as JSON.
|
|
49
56
|
* @attr {string} hint - One line of help under the field.
|
|
50
57
|
* @attr {string} info - Longer help, opened from an (i) button by the label.
|
|
51
58
|
* @attr {string} placeholder - What the trigger shows with nothing chosen. Defaults to `Choose`.
|
|
@@ -56,13 +63,15 @@ const chevronIcon =
|
|
|
56
63
|
* @attr {boolean} label-hidden - Hides the label but keeps it for assistive technology.
|
|
57
64
|
* @attr {boolean} inline-label - Shows the label inside the trigger, before the value, for toolbars and filters.
|
|
58
65
|
*
|
|
59
|
-
* @fires change - When the value changes.
|
|
66
|
+
* @fires {Event} change - When the value, or `values` with `multiple`, changes.
|
|
60
67
|
*/
|
|
61
68
|
export class GwSelect extends LightElement {
|
|
62
69
|
static override properties = {
|
|
63
70
|
label: {},
|
|
64
71
|
name: {},
|
|
65
72
|
value: {},
|
|
73
|
+
multiple: { type: "boolean", reflect: true },
|
|
74
|
+
values: { type: "json" },
|
|
66
75
|
hint: {},
|
|
67
76
|
info: {},
|
|
68
77
|
placeholder: {},
|
|
@@ -77,6 +86,8 @@ export class GwSelect extends LightElement {
|
|
|
77
86
|
declare label: string;
|
|
78
87
|
declare name: string;
|
|
79
88
|
declare value: string;
|
|
89
|
+
declare multiple: boolean;
|
|
90
|
+
declare values: readonly string[];
|
|
80
91
|
declare hint: string;
|
|
81
92
|
/** Longer help, opened from an (i) button by the label. */
|
|
82
93
|
declare info: string;
|
|
@@ -126,14 +137,23 @@ export class GwSelect extends LightElement {
|
|
|
126
137
|
|
|
127
138
|
readonly #input = h("input", { type: "hidden" });
|
|
128
139
|
|
|
140
|
+
readonly #inputs = h("span", { hidden: true });
|
|
141
|
+
|
|
129
142
|
readonly #hint = h("p", { class: "gw-hint", id: `${this.#id}-hint` });
|
|
130
143
|
|
|
131
144
|
readonly #list = h("gw-option-list");
|
|
132
145
|
|
|
146
|
+
readonly #clear = h(
|
|
147
|
+
"button",
|
|
148
|
+
{ type: "button", class: "gw-select-clear" },
|
|
149
|
+
"Clear",
|
|
150
|
+
);
|
|
151
|
+
|
|
133
152
|
readonly #popup = h(
|
|
134
153
|
"div",
|
|
135
154
|
{ class: "gw-select-popup", popover: "manual" },
|
|
136
155
|
this.#list,
|
|
156
|
+
this.#clear,
|
|
137
157
|
);
|
|
138
158
|
|
|
139
159
|
constructor() {
|
|
@@ -141,6 +161,8 @@ export class GwSelect extends LightElement {
|
|
|
141
161
|
this.label = "";
|
|
142
162
|
this.name = "";
|
|
143
163
|
this.value = "";
|
|
164
|
+
this.multiple = false;
|
|
165
|
+
this.values = [];
|
|
144
166
|
this.hint = "";
|
|
145
167
|
this.info = "";
|
|
146
168
|
this.placeholder = "Choose";
|
|
@@ -179,6 +201,10 @@ export class GwSelect extends LightElement {
|
|
|
179
201
|
|
|
180
202
|
if (option.dataset.icon !== undefined) item.icon = option.dataset.icon;
|
|
181
203
|
|
|
204
|
+
if (option.dataset.swatch !== undefined) {
|
|
205
|
+
item.swatch = option.dataset.swatch;
|
|
206
|
+
}
|
|
207
|
+
|
|
182
208
|
if (option.dataset.description !== undefined) {
|
|
183
209
|
item.description = option.dataset.description;
|
|
184
210
|
}
|
|
@@ -188,9 +214,15 @@ export class GwSelect extends LightElement {
|
|
|
188
214
|
return item;
|
|
189
215
|
});
|
|
190
216
|
|
|
191
|
-
const picked = children.
|
|
217
|
+
const picked = children.filter((option) => option.selected);
|
|
192
218
|
|
|
193
|
-
if (this.
|
|
219
|
+
if (this.multiple) {
|
|
220
|
+
if (this.values.length === 0 && picked.length > 0) {
|
|
221
|
+
this.values = picked.map((option) => option.value);
|
|
222
|
+
}
|
|
223
|
+
} else if (this.value === "" && picked[0] !== undefined) {
|
|
224
|
+
this.value = picked[0].value;
|
|
225
|
+
}
|
|
194
226
|
}
|
|
195
227
|
|
|
196
228
|
for (const element of this.querySelectorAll("option, optgroup")) {
|
|
@@ -294,6 +326,17 @@ export class GwSelect extends LightElement {
|
|
|
294
326
|
? String(event.detail.value)
|
|
295
327
|
: "";
|
|
296
328
|
|
|
329
|
+
if (this.multiple) {
|
|
330
|
+
const chosen = Array.isArray(this.values) ? this.values : [];
|
|
331
|
+
|
|
332
|
+
this.values = chosen.includes(value)
|
|
333
|
+
? chosen.filter((item) => item !== value)
|
|
334
|
+
: [...chosen, value];
|
|
335
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
336
|
+
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
297
340
|
this.close();
|
|
298
341
|
this.#trigger.focus();
|
|
299
342
|
|
|
@@ -302,19 +345,44 @@ export class GwSelect extends LightElement {
|
|
|
302
345
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
303
346
|
}
|
|
304
347
|
});
|
|
348
|
+
this.#clear.addEventListener("click", () => {
|
|
349
|
+
this.values = [];
|
|
350
|
+
this.close();
|
|
351
|
+
this.#trigger.focus();
|
|
352
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
353
|
+
});
|
|
305
354
|
|
|
306
355
|
root.append(
|
|
307
356
|
this.#info.heading,
|
|
308
357
|
this.#trigger,
|
|
309
358
|
this.#input,
|
|
359
|
+
this.#inputs,
|
|
310
360
|
this.#hint,
|
|
311
361
|
this.#popup,
|
|
312
362
|
);
|
|
313
363
|
}
|
|
314
364
|
|
|
365
|
+
/** What the trigger says: the choice, or with `multiple` the first and how many more. */
|
|
366
|
+
#summary(options: readonly OptionItem[]) {
|
|
367
|
+
if (!this.multiple) {
|
|
368
|
+
return options.find((option) => option.value === this.value);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const values = Array.isArray(this.values) ? this.values : [];
|
|
372
|
+
const chosen = options.filter((option) => values.includes(option.value));
|
|
373
|
+
const [first] = chosen;
|
|
374
|
+
|
|
375
|
+
if (first === undefined) return undefined;
|
|
376
|
+
|
|
377
|
+
return chosen.length === 1
|
|
378
|
+
? first
|
|
379
|
+
: { ...first, label: `${first.label} +${chosen.length - 1}` };
|
|
380
|
+
}
|
|
381
|
+
|
|
315
382
|
protected override update() {
|
|
316
383
|
const options = Array.isArray(this.options) ? this.options : [];
|
|
317
|
-
const selected = options
|
|
384
|
+
const selected = this.#summary(options);
|
|
385
|
+
const values = Array.isArray(this.values) ? this.values : [];
|
|
318
386
|
|
|
319
387
|
this.#label.textContent = this.label;
|
|
320
388
|
this.#info.update(this.label, this.info);
|
|
@@ -335,13 +403,23 @@ export class GwSelect extends LightElement {
|
|
|
335
403
|
setAttribute(trigger, "aria-expanded", this.open ? "true" : "false");
|
|
336
404
|
setAttribute(trigger, "aria-describedby", this.hint ? this.#hint.id : null);
|
|
337
405
|
|
|
338
|
-
setAttribute(this.#input, "name", this.name || null);
|
|
339
|
-
this.#input.value = this.value;
|
|
406
|
+
setAttribute(this.#input, "name", this.multiple ? null : this.name || null);
|
|
407
|
+
this.#input.value = this.multiple ? "" : this.value;
|
|
408
|
+
this.#inputs.replaceChildren(
|
|
409
|
+
...(this.multiple && this.name
|
|
410
|
+
? values.map((value) =>
|
|
411
|
+
h("input", { type: "hidden", name: this.name, value }),
|
|
412
|
+
)
|
|
413
|
+
: []),
|
|
414
|
+
);
|
|
415
|
+
this.#clear.hidden = !this.multiple || values.length === 0;
|
|
340
416
|
|
|
341
417
|
const list = this.#list;
|
|
342
418
|
|
|
343
419
|
list.options = options;
|
|
344
420
|
list.value = this.value;
|
|
421
|
+
list.multiple = this.multiple;
|
|
422
|
+
list.values = values;
|
|
345
423
|
list.label = this.label;
|
|
346
424
|
list.search =
|
|
347
425
|
this.search === "always" ||
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-theme-picker`: a button opening the page's mode, theme and import.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/actions/#theme-picker
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css, h } from "./element.js";
|
|
8
|
+
import "./gw-button.js";
|
|
9
|
+
import type { OptionItem } from "./gw-option-list.js";
|
|
10
|
+
import "./gw-popover.js";
|
|
11
|
+
import "./gw-segmented.js";
|
|
12
|
+
import "./gw-select.js";
|
|
13
|
+
import { base } from "./styles.js";
|
|
14
|
+
|
|
15
|
+
// MDI's palette
|
|
16
|
+
const paletteIcon =
|
|
17
|
+
"M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z";
|
|
18
|
+
|
|
19
|
+
const modes = [
|
|
20
|
+
{ value: "auto", label: "Auto" },
|
|
21
|
+
{ value: "light", label: "Light" },
|
|
22
|
+
{ value: "dark", label: "Dark" },
|
|
23
|
+
{ value: "darker", label: "Darker" },
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A palette button for a page's look. It opens a popover with the mode
|
|
28
|
+
* (auto, light, dark or Darker), the theme, and buttons to import a theme
|
|
29
|
+
* file (a Groundwork theme's `.json` or an Omarchy `colors.toml`) or reset.
|
|
30
|
+
*
|
|
31
|
+
* It only asks: it fires `mode`, `theme`, `import` and `reset`, and the page
|
|
32
|
+
* applies them and sets `mode`, `theme` and `themes` back. `pageTheme` from
|
|
33
|
+
* `@timmo001/groundwork-tokens/page` does all of that. Show an import's
|
|
34
|
+
* result, such as contrast misses, in `status`.
|
|
35
|
+
*
|
|
36
|
+
* @element gw-theme-picker
|
|
37
|
+
* @summary A button opening the page's mode, theme and import.
|
|
38
|
+
*
|
|
39
|
+
* @attr {"auto"|"light"|"dark"|"darker"} mode - The mode in use. Defaults to `auto`.
|
|
40
|
+
* @attr {string} theme - The theme in use, by id.
|
|
41
|
+
* @attr {OptionItem[]} themes - The themes to offer, as JSON.
|
|
42
|
+
* @attr {string} status - A line under the controls, such as an import's result.
|
|
43
|
+
* @attr {string} label - The button's accessible name and the popover's heading. Defaults to `Theme`.
|
|
44
|
+
*
|
|
45
|
+
* @fires {CustomEvent<{ mode: string }>} mode - When a mode is picked.
|
|
46
|
+
* @fires {CustomEvent<{ id: string }>} theme - When a theme is picked.
|
|
47
|
+
* @fires {CustomEvent<{ text: string, name: string }>} import - When a theme file is chosen, with its text and file name.
|
|
48
|
+
* @fires {Event} reset - When Reset is pressed.
|
|
49
|
+
*/
|
|
50
|
+
export class GwThemePicker extends GwElement {
|
|
51
|
+
static override properties = {
|
|
52
|
+
mode: { reflect: true },
|
|
53
|
+
theme: { reflect: true },
|
|
54
|
+
themes: { type: "json" },
|
|
55
|
+
status: {},
|
|
56
|
+
label: {},
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
static override styles = [
|
|
60
|
+
base,
|
|
61
|
+
css`
|
|
62
|
+
:host {
|
|
63
|
+
display: inline-block;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.controls {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: var(--gw-space-4);
|
|
70
|
+
margin-block-start: var(--gw-space-2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.status {
|
|
74
|
+
margin: 0;
|
|
75
|
+
font-size: var(--gw-font-size-s);
|
|
76
|
+
color: var(--gw-color-text-secondary);
|
|
77
|
+
}
|
|
78
|
+
`,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
declare mode: string;
|
|
82
|
+
declare theme: string;
|
|
83
|
+
declare themes: readonly OptionItem[];
|
|
84
|
+
declare status: string;
|
|
85
|
+
declare label: string;
|
|
86
|
+
|
|
87
|
+
readonly #button = h("gw-button", {
|
|
88
|
+
variant: "neutral",
|
|
89
|
+
appearance: "plain",
|
|
90
|
+
icon: paletteIcon,
|
|
91
|
+
labels: "hide",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
readonly #mode = h("gw-segmented");
|
|
95
|
+
|
|
96
|
+
readonly #theme = h("gw-select", { label: "Theme" });
|
|
97
|
+
|
|
98
|
+
readonly #status = h("p", { class: "status", role: "status" });
|
|
99
|
+
|
|
100
|
+
readonly #file = h("input", {
|
|
101
|
+
type: "file",
|
|
102
|
+
accept: ".json,.toml,application/json",
|
|
103
|
+
hidden: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
readonly #popover = h(
|
|
107
|
+
"gw-popover",
|
|
108
|
+
{},
|
|
109
|
+
h("div", { class: "controls" }, this.#mode, this.#theme, this.#status),
|
|
110
|
+
h(
|
|
111
|
+
"gw-button",
|
|
112
|
+
{
|
|
113
|
+
slot: "actions",
|
|
114
|
+
variant: "neutral",
|
|
115
|
+
appearance: "plain",
|
|
116
|
+
class: "reset",
|
|
117
|
+
},
|
|
118
|
+
"Reset",
|
|
119
|
+
),
|
|
120
|
+
h(
|
|
121
|
+
"gw-button",
|
|
122
|
+
{ slot: "actions", appearance: "outlined", class: "import" },
|
|
123
|
+
"Import",
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
constructor() {
|
|
128
|
+
super();
|
|
129
|
+
this.mode = "auto";
|
|
130
|
+
this.theme = "";
|
|
131
|
+
this.themes = [];
|
|
132
|
+
this.status = "";
|
|
133
|
+
this.label = "Theme";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Opens the popover under the button. */
|
|
137
|
+
show() {
|
|
138
|
+
this.#popover.show(this.#button);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#detail<Detail>(type: string, detail: Detail) {
|
|
142
|
+
return new CustomEvent(type, { detail, bubbles: true, composed: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected override create(root: ShadowRoot) {
|
|
146
|
+
this.#mode.options = modes;
|
|
147
|
+
this.#button.addEventListener("click", () => {
|
|
148
|
+
this.show();
|
|
149
|
+
});
|
|
150
|
+
this.#mode.addEventListener("change", (event) => {
|
|
151
|
+
event.stopPropagation();
|
|
152
|
+
this.mode = this.#mode.value;
|
|
153
|
+
this.dispatchEvent(this.#detail("mode", { mode: this.mode }));
|
|
154
|
+
});
|
|
155
|
+
this.#theme.addEventListener("change", (event) => {
|
|
156
|
+
event.stopPropagation();
|
|
157
|
+
this.theme = this.#theme.value;
|
|
158
|
+
this.dispatchEvent(this.#detail("theme", { id: this.theme }));
|
|
159
|
+
});
|
|
160
|
+
this.#popover.querySelector(".import")?.addEventListener("click", () => {
|
|
161
|
+
this.#file.click();
|
|
162
|
+
});
|
|
163
|
+
this.#popover.querySelector(".reset")?.addEventListener("click", () => {
|
|
164
|
+
this.dispatchEvent(new Event("reset", { bubbles: true, composed: true }));
|
|
165
|
+
});
|
|
166
|
+
this.#file.addEventListener("change", async () => {
|
|
167
|
+
const file = this.#file.files?.[0];
|
|
168
|
+
|
|
169
|
+
this.#file.value = "";
|
|
170
|
+
|
|
171
|
+
if (file === undefined) return;
|
|
172
|
+
|
|
173
|
+
const text = await file.text();
|
|
174
|
+
|
|
175
|
+
this.dispatchEvent(this.#detail("import", { text, name: file.name }));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
root.append(this.#button, this.#popover, this.#file);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
protected override update() {
|
|
182
|
+
this.#button.label = this.label;
|
|
183
|
+
this.#button.textContent = this.label;
|
|
184
|
+
this.#popover.heading = this.label;
|
|
185
|
+
this.#mode.label = "Mode";
|
|
186
|
+
this.#mode.value = this.mode;
|
|
187
|
+
this.#theme.options = Array.isArray(this.themes) ? this.themes : [];
|
|
188
|
+
this.#theme.value = this.theme;
|
|
189
|
+
this.#status.textContent = this.status;
|
|
190
|
+
this.#status.hidden = !this.status;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
GwThemePicker.define("gw-theme-picker");
|
|
195
|
+
|
|
196
|
+
declare global {
|
|
197
|
+
interface HTMLElementTagNameMap {
|
|
198
|
+
"gw-theme-picker": GwThemePicker;
|
|
199
|
+
}
|
|
200
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,8 +15,12 @@ export * from "./fields.js";
|
|
|
15
15
|
|
|
16
16
|
export * from "./layout.js";
|
|
17
17
|
|
|
18
|
+
export * from "./on.js";
|
|
19
|
+
|
|
18
20
|
export * from "./gw-alert.js";
|
|
19
21
|
|
|
22
|
+
export * from "./gw-app-bar.js";
|
|
23
|
+
|
|
20
24
|
export * from "./gw-badge.js";
|
|
21
25
|
|
|
22
26
|
export * from "./gw-banner.js";
|
|
@@ -73,4 +77,6 @@ export * from "./gw-skeleton.js";
|
|
|
73
77
|
|
|
74
78
|
export * from "./gw-spinner.js";
|
|
75
79
|
|
|
80
|
+
export * from "./gw-theme-picker.js";
|
|
81
|
+
|
|
76
82
|
export * from "./gw-toast.js";
|
package/src/on.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `on()`: listens for a `gw-` element's own events, typed from the
|
|
3
|
+
* generated `GwEventMap`, so a handler gets `CustomEvent<{ value: string }>`
|
|
4
|
+
* for `pick` with no cast.
|
|
5
|
+
* Docs: https://groundwork.timmo.dev/components/#using-with-lit
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { GwEventMap } from "./events.js";
|
|
10
|
+
|
|
11
|
+
export type { GwEventMap } from "./events.js";
|
|
12
|
+
|
|
13
|
+
type EventTag = keyof GwEventMap;
|
|
14
|
+
|
|
15
|
+
/** A `gw-` element that fires events of its own. */
|
|
16
|
+
export type GwEventTarget = HTMLElementTagNameMap[EventTag];
|
|
17
|
+
|
|
18
|
+
/** The events an element fires, found from the tag its class is defined for. */
|
|
19
|
+
export type GwEventsOf<E extends GwEventTarget> = {
|
|
20
|
+
[T in EventTag]: E extends HTMLElementTagNameMap[T] ? GwEventMap[T] : never;
|
|
21
|
+
}[EventTag];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A handler for one event. Declared as a method so it's checked bivariantly
|
|
25
|
+
* and can be handed to `addEventListener`, which only knows it as an `Event`.
|
|
26
|
+
*/
|
|
27
|
+
type Listener<T extends Event> = {
|
|
28
|
+
handle(event: T): void;
|
|
29
|
+
}["handle"];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Adds a listener for one of the element's own events, typed from its
|
|
33
|
+
* `@fires` docs, and returns a function that removes it.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* on(menu, "pick", (event) => open(event.detail.value));
|
|
37
|
+
*/
|
|
38
|
+
export function on<
|
|
39
|
+
E extends GwEventTarget,
|
|
40
|
+
K extends keyof GwEventsOf<E> & string,
|
|
41
|
+
>(
|
|
42
|
+
element: E,
|
|
43
|
+
type: K,
|
|
44
|
+
listener: Listener<GwEventsOf<E>[K] & Event>,
|
|
45
|
+
options?: AddEventListenerOptions,
|
|
46
|
+
): () => void {
|
|
47
|
+
const handler: Listener<Event> = listener;
|
|
48
|
+
|
|
49
|
+
element.addEventListener(type, handler, options);
|
|
50
|
+
|
|
51
|
+
return () => element.removeEventListener(type, handler, options);
|
|
52
|
+
}
|