@f-ewald/components 0.6.0 → 1.0.0
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 +101 -44
- package/custom-elements.json +2570 -877
- package/dist/calendar-entry.d.ts +33 -0
- package/dist/calendar-entry.d.ts.map +1 -0
- package/dist/calendar-entry.js +64 -0
- package/dist/calendar-entry.js.map +1 -0
- package/dist/calendar-month.d.ts +67 -0
- package/dist/calendar-month.d.ts.map +1 -0
- package/dist/calendar-month.js +455 -0
- package/dist/calendar-month.js.map +1 -0
- package/dist/calendar-year.d.ts +43 -0
- package/dist/calendar-year.d.ts.map +1 -0
- package/dist/calendar-year.js +136 -0
- package/dist/calendar-year.js.map +1 -0
- package/dist/dropdown-button.d.ts +37 -0
- package/dist/dropdown-button.d.ts.map +1 -0
- package/dist/dropdown-button.js +219 -0
- package/dist/dropdown-button.js.map +1 -0
- package/dist/form-select.d.ts +27 -2
- package/dist/form-select.d.ts.map +1 -1
- package/dist/form-select.js +440 -27
- package/dist/form-select.js.map +1 -1
- package/dist/frame-box.d.ts +22 -0
- package/dist/frame-box.d.ts.map +1 -0
- package/dist/frame-box.js +68 -0
- package/dist/frame-box.js.map +1 -0
- package/dist/icon-button.d.ts +27 -0
- package/dist/icon-button.d.ts.map +1 -0
- package/dist/icon-button.js +83 -0
- package/dist/icon-button.js.map +1 -0
- package/dist/icons.d.ts +1 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +1 -0
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/map-circle.d.ts +3 -0
- package/dist/map-circle.d.ts.map +1 -1
- package/dist/map-circle.js +3 -0
- package/dist/map-circle.js.map +1 -1
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +82 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/popover-panel.d.ts +3 -0
- package/dist/popover-panel.d.ts.map +1 -1
- package/dist/popover-panel.js +11 -0
- package/dist/popover-panel.js.map +1 -1
- package/dist/tokens.css +4 -0
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +3 -0
- package/dist/tokens.js.map +1 -1
- package/dist/utils/calendar.d.ts +77 -0
- package/dist/utils/calendar.d.ts.map +1 -0
- package/dist/utils/calendar.js +135 -0
- package/dist/utils/calendar.js.map +1 -0
- package/docs/calendar-entry.md +53 -0
- package/docs/calendar-month.md +77 -0
- package/docs/calendar-year.md +53 -0
- package/docs/dropdown-button.md +61 -0
- package/docs/form-select.md +21 -3
- package/docs/frame-box.md +43 -0
- package/docs/icon-button.md +52 -0
- package/docs/map-circle.md +4 -0
- package/docs/mcp-evaluation.md +18 -11
- package/docs/popover-panel.md +5 -1
- package/llms.txt +194 -28
- package/package.json +19 -4
- package/dist/map-point.d.ts +0 -31
- package/dist/map-point.d.ts.map +0 -1
- package/dist/map-point.js +0 -83
- package/dist/map-point.js.map +0 -1
- package/docs/map-point.md +0 -44
package/dist/form-select.js
CHANGED
|
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { LitElement, css, html, nothing } from "lit";
|
|
7
|
+
import { LitElement, css, html, nothing, } from "lit";
|
|
8
8
|
import { customElement, property, state } from "lit/decorators.js";
|
|
9
9
|
import { iconChevronRight } from "./icons.js";
|
|
10
10
|
import { tokens } from "./tokens.js";
|
|
@@ -15,6 +15,19 @@ import { tokens } from "./tokens.js";
|
|
|
15
15
|
* `change` event carrying `{ value }` are wanted (e.g. a task's status
|
|
16
16
|
* picker).
|
|
17
17
|
*
|
|
18
|
+
* The trigger fills its host's width (`justify-content: space-between`
|
|
19
|
+
* pushes the chevron to the far edge), but the host itself stays
|
|
20
|
+
* `display: inline-block` — so usages that never size the host (a filter
|
|
21
|
+
* bar, a status picker) keep shrink-to-fit auto-width unchanged. To make an
|
|
22
|
+
* instance full-width, size the host itself: `form-select { width: 100%; }`.
|
|
23
|
+
*
|
|
24
|
+
* Set `searchable` to replace the button trigger with an editable combobox
|
|
25
|
+
* that filters the predefined options by case-insensitive label infix. Typed
|
|
26
|
+
* text is only a query: `value` changes exclusively when an actual option is
|
|
27
|
+
* selected, and an uncommitted query is discarded when the list closes.
|
|
28
|
+
* Each option may also provide a pre-rendered `icon` and square `iconSize`;
|
|
29
|
+
* iconless options reserve no leading space.
|
|
30
|
+
*
|
|
18
31
|
* @element form-select
|
|
19
32
|
* @fires change - Fired with `{ value: string }` when a different option is picked.
|
|
20
33
|
*/
|
|
@@ -29,11 +42,25 @@ let FormSelect = class FormSelect extends LitElement {
|
|
|
29
42
|
this.label = "";
|
|
30
43
|
/** Disables the trigger, preventing the popover from opening. */
|
|
31
44
|
this.disabled = false;
|
|
45
|
+
/**
|
|
46
|
+
* Enables editable, case-insensitive infix filtering by option label.
|
|
47
|
+
* Typed text never becomes the selected `value`.
|
|
48
|
+
*/
|
|
49
|
+
this.searchable = false;
|
|
32
50
|
this._open = false;
|
|
33
51
|
this._activeIndex = -1;
|
|
52
|
+
this._query = null;
|
|
53
|
+
this.#listboxPointerDown = false;
|
|
54
|
+
this.#suppressNextBlur = false;
|
|
55
|
+
this.#restoringSearchFocus = false;
|
|
56
|
+
this.#searchSelection = null;
|
|
57
|
+
this.#isComposing = false;
|
|
58
|
+
this.#compositionJustEnded = false;
|
|
59
|
+
this.#compositionEndTimer = null;
|
|
60
|
+
this.#blurTimer = null;
|
|
34
61
|
this.#onWindowMousedown = (e) => {
|
|
35
62
|
if (!e.composedPath().includes(this))
|
|
36
|
-
this
|
|
63
|
+
this.#close();
|
|
37
64
|
};
|
|
38
65
|
}
|
|
39
66
|
static { this.styles = [
|
|
@@ -48,7 +75,10 @@ let FormSelect = class FormSelect extends LitElement {
|
|
|
48
75
|
button.trigger {
|
|
49
76
|
display: flex;
|
|
50
77
|
align-items: center;
|
|
78
|
+
justify-content: space-between;
|
|
51
79
|
gap: 0.35rem;
|
|
80
|
+
width: 100%;
|
|
81
|
+
box-sizing: border-box;
|
|
52
82
|
font: inherit;
|
|
53
83
|
color: var(--ui-text, #0f172a);
|
|
54
84
|
background: var(--ui-surface, #fff);
|
|
@@ -69,12 +99,56 @@ let FormSelect = class FormSelect extends LitElement {
|
|
|
69
99
|
border-color: var(--ui-primary, #4f46e5);
|
|
70
100
|
box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));
|
|
71
101
|
}
|
|
102
|
+
.search-trigger {
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: space-between;
|
|
106
|
+
gap: 0.35rem;
|
|
107
|
+
width: 100%;
|
|
108
|
+
box-sizing: border-box;
|
|
109
|
+
color: var(--ui-text, #0f172a);
|
|
110
|
+
background: var(--ui-surface, #fff);
|
|
111
|
+
border: 1px solid var(--ui-border, #e2e8f0);
|
|
112
|
+
border-radius: var(--ui-radius-sm, 0.25rem);
|
|
113
|
+
cursor: text;
|
|
114
|
+
}
|
|
115
|
+
.search-trigger:hover:not(.disabled) {
|
|
116
|
+
background: var(--ui-surface-muted, #f8fafc);
|
|
117
|
+
}
|
|
118
|
+
.search-trigger:focus-within {
|
|
119
|
+
border-color: var(--ui-primary, #4f46e5);
|
|
120
|
+
box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));
|
|
121
|
+
}
|
|
122
|
+
.search-trigger.disabled {
|
|
123
|
+
cursor: not-allowed;
|
|
124
|
+
opacity: 0.6;
|
|
125
|
+
}
|
|
126
|
+
.search-trigger.has-icon input.search-input {
|
|
127
|
+
padding-left: 0;
|
|
128
|
+
}
|
|
129
|
+
input.search-input {
|
|
130
|
+
min-width: 0;
|
|
131
|
+
flex: 1;
|
|
132
|
+
box-sizing: border-box;
|
|
133
|
+
padding: 0.3rem 0 0.3rem 0.5rem;
|
|
134
|
+
color: inherit;
|
|
135
|
+
background: transparent;
|
|
136
|
+
border: 0;
|
|
137
|
+
outline: 0;
|
|
138
|
+
font: inherit;
|
|
139
|
+
}
|
|
72
140
|
.chevron {
|
|
73
141
|
display: flex;
|
|
142
|
+
flex: 0 0 auto;
|
|
143
|
+
margin-right: 0.5rem;
|
|
74
144
|
color: var(--ui-text-muted, #64748b);
|
|
145
|
+
pointer-events: none;
|
|
75
146
|
transform: rotate(90deg);
|
|
76
147
|
transition: transform 0.1s ease;
|
|
77
148
|
}
|
|
149
|
+
button.trigger .chevron {
|
|
150
|
+
margin-right: 0;
|
|
151
|
+
}
|
|
78
152
|
:host([open]) .chevron {
|
|
79
153
|
transform: rotate(-90deg);
|
|
80
154
|
}
|
|
@@ -86,15 +160,26 @@ let FormSelect = class FormSelect extends LitElement {
|
|
|
86
160
|
min-width: 100%;
|
|
87
161
|
max-height: 40vh;
|
|
88
162
|
overflow-y: auto;
|
|
163
|
+
scrollbar-gutter: stable;
|
|
89
164
|
margin: 2px 0 0;
|
|
90
165
|
padding: 4px 0;
|
|
91
166
|
list-style: none;
|
|
92
167
|
white-space: nowrap;
|
|
168
|
+
color: var(--ui-text, #0f172a);
|
|
93
169
|
background: var(--ui-surface, #fff);
|
|
94
170
|
border: 1px solid var(--ui-border, #e2e8f0);
|
|
95
171
|
border-radius: var(--ui-radius-sm, 0.25rem);
|
|
96
172
|
box-shadow: var(--ui-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));
|
|
97
173
|
}
|
|
174
|
+
ul.options::-webkit-scrollbar {
|
|
175
|
+
width: 0.75rem;
|
|
176
|
+
}
|
|
177
|
+
ul.options::-webkit-scrollbar-thumb {
|
|
178
|
+
background: var(--ui-border, #e2e8f0);
|
|
179
|
+
border: 0.2rem solid transparent;
|
|
180
|
+
border-radius: 999px;
|
|
181
|
+
background-clip: padding-box;
|
|
182
|
+
}
|
|
98
183
|
li {
|
|
99
184
|
padding: 0.35rem 0.6rem;
|
|
100
185
|
cursor: pointer;
|
|
@@ -107,34 +192,136 @@ let FormSelect = class FormSelect extends LitElement {
|
|
|
107
192
|
font-weight: 600;
|
|
108
193
|
color: var(--ui-primary, #4f46e5);
|
|
109
194
|
}
|
|
195
|
+
.option-content {
|
|
196
|
+
display: inline-flex;
|
|
197
|
+
min-width: 0;
|
|
198
|
+
align-items: center;
|
|
199
|
+
gap: 0.4rem;
|
|
200
|
+
}
|
|
201
|
+
li .option-content {
|
|
202
|
+
display: flex;
|
|
203
|
+
width: 100%;
|
|
204
|
+
}
|
|
205
|
+
.option-icon {
|
|
206
|
+
display: inline-flex;
|
|
207
|
+
width: var(--form-select-icon-size);
|
|
208
|
+
height: var(--form-select-icon-size);
|
|
209
|
+
flex: 0 0 var(--form-select-icon-size);
|
|
210
|
+
align-items: center;
|
|
211
|
+
justify-content: center;
|
|
212
|
+
}
|
|
213
|
+
.option-icon > svg {
|
|
214
|
+
width: 100%;
|
|
215
|
+
height: 100%;
|
|
216
|
+
}
|
|
217
|
+
.selected-icon {
|
|
218
|
+
margin-left: 0.5rem;
|
|
219
|
+
}
|
|
220
|
+
.option-label {
|
|
221
|
+
min-width: 0;
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
text-overflow: ellipsis;
|
|
224
|
+
white-space: nowrap;
|
|
225
|
+
}
|
|
226
|
+
li.no-options {
|
|
227
|
+
padding: 0.35rem 0.6rem;
|
|
228
|
+
color: var(--ui-text-muted, #64748b);
|
|
229
|
+
cursor: default;
|
|
230
|
+
font-style: italic;
|
|
231
|
+
}
|
|
110
232
|
`,
|
|
111
233
|
]; }
|
|
234
|
+
#listboxPointerDown;
|
|
235
|
+
#suppressNextBlur;
|
|
236
|
+
#restoringSearchFocus;
|
|
237
|
+
#searchSelection;
|
|
238
|
+
#isComposing;
|
|
239
|
+
#compositionJustEnded;
|
|
240
|
+
#compositionEndTimer;
|
|
241
|
+
#blurTimer;
|
|
112
242
|
disconnectedCallback() {
|
|
113
243
|
super.disconnectedCallback();
|
|
114
244
|
window.removeEventListener("mousedown", this.#onWindowMousedown, true);
|
|
245
|
+
if (this.#blurTimer)
|
|
246
|
+
clearTimeout(this.#blurTimer);
|
|
247
|
+
if (this.#compositionEndTimer)
|
|
248
|
+
clearTimeout(this.#compositionEndTimer);
|
|
249
|
+
this.#listboxPointerDown = false;
|
|
250
|
+
this.#suppressNextBlur = false;
|
|
251
|
+
this.#searchSelection = null;
|
|
252
|
+
this.#isComposing = false;
|
|
253
|
+
this.#compositionJustEnded = false;
|
|
254
|
+
this.#close();
|
|
115
255
|
}
|
|
116
256
|
updated(changed) {
|
|
117
|
-
if (
|
|
257
|
+
if (changed.has("_open")) {
|
|
258
|
+
this.toggleAttribute("open", this._open);
|
|
259
|
+
if (this._open) {
|
|
260
|
+
window.addEventListener("mousedown", this.#onWindowMousedown, true);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
window.removeEventListener("mousedown", this.#onWindowMousedown, true);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if ((changed.has("disabled") && this.disabled) || changed.has("searchable")) {
|
|
267
|
+
this.#close();
|
|
118
268
|
return;
|
|
119
|
-
this.toggleAttribute("open", this._open);
|
|
120
|
-
if (this._open) {
|
|
121
|
-
window.addEventListener("mousedown", this.#onWindowMousedown, true);
|
|
122
269
|
}
|
|
123
|
-
|
|
124
|
-
|
|
270
|
+
if (changed.has("options") && this._open) {
|
|
271
|
+
this._activeIndex = this.#initialActiveIndex();
|
|
272
|
+
}
|
|
273
|
+
else if (changed.has("value") && this._open && this._query === null) {
|
|
274
|
+
this._activeIndex = this.#initialActiveIndex();
|
|
275
|
+
}
|
|
276
|
+
if ((changed.has("_activeIndex") || changed.has("_query") || changed.has("options")) &&
|
|
277
|
+
this._open &&
|
|
278
|
+
this._activeIndex >= 0) {
|
|
279
|
+
this.renderRoot
|
|
280
|
+
.querySelector(`#form-select-option-${this._activeIndex}`)
|
|
281
|
+
?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
125
282
|
}
|
|
126
283
|
}
|
|
127
284
|
#onWindowMousedown;
|
|
128
285
|
#toggle() {
|
|
129
286
|
if (this.disabled)
|
|
130
287
|
return;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
288
|
+
if (this._open)
|
|
289
|
+
this.#close();
|
|
290
|
+
else
|
|
291
|
+
this.#open();
|
|
135
292
|
}
|
|
136
|
-
#
|
|
293
|
+
#open() {
|
|
294
|
+
if (this.disabled)
|
|
295
|
+
return;
|
|
296
|
+
this._query = null;
|
|
297
|
+
this._open = true;
|
|
298
|
+
this._activeIndex = this.#initialActiveIndex();
|
|
299
|
+
}
|
|
300
|
+
#close() {
|
|
137
301
|
this._open = false;
|
|
302
|
+
this._query = null;
|
|
303
|
+
this._activeIndex = -1;
|
|
304
|
+
this.#searchSelection = null;
|
|
305
|
+
}
|
|
306
|
+
#initialActiveIndex() {
|
|
307
|
+
const options = this.#visibleOptions();
|
|
308
|
+
if (options.length === 0)
|
|
309
|
+
return -1;
|
|
310
|
+
if (this._query !== null)
|
|
311
|
+
return 0;
|
|
312
|
+
const selectedIndex = options.findIndex((option) => option.value === this.value);
|
|
313
|
+
return selectedIndex >= 0 ? selectedIndex : 0;
|
|
314
|
+
}
|
|
315
|
+
#visibleOptions() {
|
|
316
|
+
if (!this.searchable || this._query === null)
|
|
317
|
+
return this.options;
|
|
318
|
+
const needle = this._query.trim().toLocaleLowerCase();
|
|
319
|
+
if (!needle)
|
|
320
|
+
return this.options;
|
|
321
|
+
return this.options.filter((option) => option.label.toLocaleLowerCase().includes(needle));
|
|
322
|
+
}
|
|
323
|
+
#select(option) {
|
|
324
|
+
this.#close();
|
|
138
325
|
if (option.value === this.value)
|
|
139
326
|
return;
|
|
140
327
|
this.value = option.value;
|
|
@@ -156,43 +343,253 @@ let FormSelect = class FormSelect extends LitElement {
|
|
|
156
343
|
}
|
|
157
344
|
else if (e.key === "Escape" && this._open) {
|
|
158
345
|
e.preventDefault();
|
|
159
|
-
this
|
|
346
|
+
this.#close();
|
|
160
347
|
}
|
|
161
348
|
}
|
|
162
349
|
#moveActive(delta) {
|
|
163
|
-
|
|
350
|
+
const options = this.#visibleOptions();
|
|
351
|
+
if (options.length === 0)
|
|
164
352
|
return;
|
|
165
|
-
const n =
|
|
353
|
+
const n = options.length;
|
|
166
354
|
this._activeIndex = (this._activeIndex + delta + n) % n;
|
|
167
355
|
}
|
|
168
356
|
#confirmActive() {
|
|
169
|
-
const
|
|
357
|
+
const options = this.#visibleOptions();
|
|
358
|
+
const option = options[this._activeIndex] ?? options[0];
|
|
170
359
|
if (option)
|
|
171
360
|
this.#select(option);
|
|
172
361
|
}
|
|
362
|
+
#onSearchFocus(e) {
|
|
363
|
+
if (this.disabled)
|
|
364
|
+
return;
|
|
365
|
+
if (this.#blurTimer) {
|
|
366
|
+
clearTimeout(this.#blurTimer);
|
|
367
|
+
this.#blurTimer = null;
|
|
368
|
+
}
|
|
369
|
+
if (!this._open)
|
|
370
|
+
this.#open();
|
|
371
|
+
if (!this.#restoringSearchFocus)
|
|
372
|
+
e.currentTarget.select();
|
|
373
|
+
}
|
|
374
|
+
#onSearchClick(e) {
|
|
375
|
+
if (!this._open)
|
|
376
|
+
this.#open();
|
|
377
|
+
if (this._query === null)
|
|
378
|
+
e.currentTarget.select();
|
|
379
|
+
}
|
|
380
|
+
#onSearchMousedown(e) {
|
|
381
|
+
if (e.button !== 0)
|
|
382
|
+
e.preventDefault();
|
|
383
|
+
}
|
|
384
|
+
#onSearchInput(e) {
|
|
385
|
+
this._query = e.currentTarget.value;
|
|
386
|
+
this._open = true;
|
|
387
|
+
const options = this.#visibleOptions();
|
|
388
|
+
this._activeIndex = options.length > 0 ? 0 : -1;
|
|
389
|
+
}
|
|
390
|
+
#onSearchKeydown(e) {
|
|
391
|
+
if (e.isComposing || e.keyCode === 229 || this.#isComposing || this.#compositionJustEnded) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
395
|
+
e.preventDefault();
|
|
396
|
+
if (!this._open) {
|
|
397
|
+
this.#open();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this.#moveActive(e.key === "ArrowDown" ? 1 : -1);
|
|
401
|
+
}
|
|
402
|
+
else if (e.key === "Enter" && this._open) {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
this.#confirmActive();
|
|
405
|
+
}
|
|
406
|
+
else if (e.key === "Escape" && this._open) {
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
this.#close();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
#onSearchBlur() {
|
|
412
|
+
if (this.#blurTimer)
|
|
413
|
+
clearTimeout(this.#blurTimer);
|
|
414
|
+
this.#blurTimer = setTimeout(() => {
|
|
415
|
+
this.#blurTimer = null;
|
|
416
|
+
if (!this._open)
|
|
417
|
+
return;
|
|
418
|
+
if (!this.#listboxPointerDown && !this.#suppressNextBlur)
|
|
419
|
+
this.#close();
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
#focusSearchInput(e) {
|
|
423
|
+
if (e.button !== 0 ||
|
|
424
|
+
this.disabled ||
|
|
425
|
+
e.composedPath()[0] instanceof HTMLInputElement) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
e.preventDefault();
|
|
429
|
+
const input = this.renderRoot.querySelector("input.search-input");
|
|
430
|
+
if (!this._open)
|
|
431
|
+
this.#open();
|
|
432
|
+
input?.focus();
|
|
433
|
+
input?.select();
|
|
434
|
+
}
|
|
435
|
+
#onCompositionStart() {
|
|
436
|
+
if (this.#compositionEndTimer)
|
|
437
|
+
clearTimeout(this.#compositionEndTimer);
|
|
438
|
+
this.#compositionEndTimer = null;
|
|
439
|
+
this.#compositionJustEnded = false;
|
|
440
|
+
this.#isComposing = true;
|
|
441
|
+
}
|
|
442
|
+
#onCompositionEnd() {
|
|
443
|
+
this.#isComposing = false;
|
|
444
|
+
this.#compositionJustEnded = true;
|
|
445
|
+
if (this.#compositionEndTimer)
|
|
446
|
+
clearTimeout(this.#compositionEndTimer);
|
|
447
|
+
this.#compositionEndTimer = setTimeout(() => {
|
|
448
|
+
this.#compositionJustEnded = false;
|
|
449
|
+
this.#compositionEndTimer = null;
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
#onListboxMousedown(e) {
|
|
453
|
+
if (e.button !== 0)
|
|
454
|
+
return;
|
|
455
|
+
this.#listboxPointerDown = true;
|
|
456
|
+
this.#suppressNextBlur = true;
|
|
457
|
+
const input = this.renderRoot.querySelector("input.search-input");
|
|
458
|
+
this.#searchSelection = input
|
|
459
|
+
? [input.selectionStart, input.selectionEnd]
|
|
460
|
+
: null;
|
|
461
|
+
window.addEventListener("mouseup", () => {
|
|
462
|
+
this.#listboxPointerDown = false;
|
|
463
|
+
if (this._open) {
|
|
464
|
+
const searchInput = this.renderRoot.querySelector("input.search-input");
|
|
465
|
+
if (searchInput) {
|
|
466
|
+
this.#restoringSearchFocus = true;
|
|
467
|
+
searchInput.focus({ preventScroll: true });
|
|
468
|
+
this.#restoringSearchFocus = false;
|
|
469
|
+
const [start, end] = this.#searchSelection ?? [null, null];
|
|
470
|
+
if (start !== null && end !== null)
|
|
471
|
+
searchInput.setSelectionRange(start, end);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
this.#searchSelection = null;
|
|
475
|
+
setTimeout(() => {
|
|
476
|
+
this.#suppressNextBlur = false;
|
|
477
|
+
});
|
|
478
|
+
}, { capture: true, once: true });
|
|
479
|
+
}
|
|
480
|
+
#onOptionMousedown(e, option) {
|
|
481
|
+
if (e.button !== 0)
|
|
482
|
+
return;
|
|
483
|
+
const optionElement = e.currentTarget;
|
|
484
|
+
const listbox = optionElement.parentElement;
|
|
485
|
+
if (listbox) {
|
|
486
|
+
const scrollbarWidth = listbox.offsetWidth - listbox.clientWidth;
|
|
487
|
+
const bounds = listbox.getBoundingClientRect();
|
|
488
|
+
const isRtl = getComputedStyle(listbox).direction === "rtl";
|
|
489
|
+
const inScrollbarGutter = isRtl
|
|
490
|
+
? e.clientX <= bounds.left + scrollbarWidth
|
|
491
|
+
: e.clientX >= bounds.right - scrollbarWidth;
|
|
492
|
+
if (scrollbarWidth > 0 && inScrollbarGutter)
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
e.preventDefault();
|
|
496
|
+
this.#select(option);
|
|
497
|
+
}
|
|
498
|
+
#optionIconSize(option) {
|
|
499
|
+
const size = option.iconSize ?? 16;
|
|
500
|
+
return Number.isFinite(size) && size > 0 ? size : 16;
|
|
501
|
+
}
|
|
502
|
+
#renderOptionIcon(option, className = "option-icon") {
|
|
503
|
+
if (!option.icon)
|
|
504
|
+
return nothing;
|
|
505
|
+
return html `
|
|
506
|
+
<span
|
|
507
|
+
class=${className}
|
|
508
|
+
style=${`--form-select-icon-size: ${this.#optionIconSize(option)}px`}
|
|
509
|
+
aria-hidden="true"
|
|
510
|
+
>
|
|
511
|
+
${option.icon}
|
|
512
|
+
</span>
|
|
513
|
+
`;
|
|
514
|
+
}
|
|
515
|
+
#renderOptionContent(option) {
|
|
516
|
+
return html `
|
|
517
|
+
<span class="option-content">
|
|
518
|
+
${this.#renderOptionIcon(option)}
|
|
519
|
+
<span class="option-label">${option.label}</span>
|
|
520
|
+
</span>
|
|
521
|
+
`;
|
|
522
|
+
}
|
|
173
523
|
renderListbox() {
|
|
174
524
|
if (!this._open)
|
|
175
525
|
return nothing;
|
|
526
|
+
const options = this.#visibleOptions();
|
|
176
527
|
return html `
|
|
177
|
-
<ul
|
|
178
|
-
|
|
528
|
+
<ul
|
|
529
|
+
id="form-select-listbox"
|
|
530
|
+
class="options"
|
|
531
|
+
role="listbox"
|
|
532
|
+
@mousedown=${(e) => this.#onListboxMousedown(e)}
|
|
533
|
+
>
|
|
534
|
+
${options.map((o, i) => html `
|
|
179
535
|
<li
|
|
536
|
+
id=${`form-select-option-${i}`}
|
|
180
537
|
role="option"
|
|
181
538
|
aria-selected=${o.value === this.value}
|
|
182
539
|
class=${i === this._activeIndex ? "active" : ""}
|
|
183
|
-
@mousedown=${(e) =>
|
|
184
|
-
e.preventDefault();
|
|
185
|
-
this.#select(o);
|
|
186
|
-
}}
|
|
540
|
+
@mousedown=${(e) => this.#onOptionMousedown(e, o)}
|
|
187
541
|
>
|
|
188
|
-
${o
|
|
542
|
+
${this.#renderOptionContent(o)}
|
|
189
543
|
</li>
|
|
190
544
|
`)}
|
|
545
|
+
${options.length === 0
|
|
546
|
+
? html `<li class="no-options" role="presentation"><span role="status">No options found</span></li>`
|
|
547
|
+
: nothing}
|
|
191
548
|
</ul>
|
|
192
549
|
`;
|
|
193
550
|
}
|
|
194
|
-
|
|
195
|
-
const
|
|
551
|
+
renderSearchTrigger(current) {
|
|
552
|
+
const displayValue = this._query ?? current?.label ?? this.value;
|
|
553
|
+
const showSelectedIcon = this._query === null && Boolean(current?.icon);
|
|
554
|
+
const activeDescendant = this._open && this._activeIndex >= 0
|
|
555
|
+
? `form-select-option-${this._activeIndex}`
|
|
556
|
+
: nothing;
|
|
557
|
+
return html `
|
|
558
|
+
<div
|
|
559
|
+
class="search-trigger ${this.disabled ? "disabled" : ""} ${showSelectedIcon
|
|
560
|
+
? "has-icon"
|
|
561
|
+
: ""}"
|
|
562
|
+
@mousedown=${(e) => this.#focusSearchInput(e)}
|
|
563
|
+
>
|
|
564
|
+
${showSelectedIcon && current
|
|
565
|
+
? this.#renderOptionIcon(current, "option-icon selected-icon")
|
|
566
|
+
: nothing}
|
|
567
|
+
<input
|
|
568
|
+
type="text"
|
|
569
|
+
class="search-input"
|
|
570
|
+
role="combobox"
|
|
571
|
+
aria-autocomplete="list"
|
|
572
|
+
aria-expanded=${this._open}
|
|
573
|
+
aria-controls="form-select-listbox"
|
|
574
|
+
aria-activedescendant=${activeDescendant}
|
|
575
|
+
aria-label=${this.label || "Select an option"}
|
|
576
|
+
autocomplete="off"
|
|
577
|
+
?disabled=${this.disabled}
|
|
578
|
+
.value=${displayValue}
|
|
579
|
+
@mousedown=${(e) => this.#onSearchMousedown(e)}
|
|
580
|
+
@focus=${(e) => this.#onSearchFocus(e)}
|
|
581
|
+
@click=${(e) => this.#onSearchClick(e)}
|
|
582
|
+
@input=${(e) => this.#onSearchInput(e)}
|
|
583
|
+
@compositionstart=${() => this.#onCompositionStart()}
|
|
584
|
+
@compositionend=${() => this.#onCompositionEnd()}
|
|
585
|
+
@keydown=${(e) => this.#onSearchKeydown(e)}
|
|
586
|
+
@blur=${() => this.#onSearchBlur()}
|
|
587
|
+
/>
|
|
588
|
+
<span class="chevron">${iconChevronRight(14)}</span>
|
|
589
|
+
</div>
|
|
590
|
+
`;
|
|
591
|
+
}
|
|
592
|
+
renderButtonTrigger(current) {
|
|
196
593
|
return html `
|
|
197
594
|
<button
|
|
198
595
|
type="button"
|
|
@@ -204,9 +601,19 @@ let FormSelect = class FormSelect extends LitElement {
|
|
|
204
601
|
@click=${() => this.#toggle()}
|
|
205
602
|
@keydown=${(e) => this.#onTriggerKeydown(e)}
|
|
206
603
|
>
|
|
207
|
-
|
|
604
|
+
${current
|
|
605
|
+
? this.#renderOptionContent(current)
|
|
606
|
+
: html `<span class="option-label">${this.value}</span>`}
|
|
208
607
|
<span class="chevron">${iconChevronRight(14)}</span>
|
|
209
608
|
</button>
|
|
609
|
+
`;
|
|
610
|
+
}
|
|
611
|
+
render() {
|
|
612
|
+
const current = this.options.find((o) => o.value === this.value);
|
|
613
|
+
return html `
|
|
614
|
+
${this.searchable
|
|
615
|
+
? this.renderSearchTrigger(current)
|
|
616
|
+
: this.renderButtonTrigger(current)}
|
|
210
617
|
${this.renderListbox()}
|
|
211
618
|
`;
|
|
212
619
|
}
|
|
@@ -223,12 +630,18 @@ __decorate([
|
|
|
223
630
|
__decorate([
|
|
224
631
|
property({ type: Boolean })
|
|
225
632
|
], FormSelect.prototype, "disabled", void 0);
|
|
633
|
+
__decorate([
|
|
634
|
+
property({ type: Boolean, reflect: true })
|
|
635
|
+
], FormSelect.prototype, "searchable", void 0);
|
|
226
636
|
__decorate([
|
|
227
637
|
state()
|
|
228
638
|
], FormSelect.prototype, "_open", void 0);
|
|
229
639
|
__decorate([
|
|
230
640
|
state()
|
|
231
641
|
], FormSelect.prototype, "_activeIndex", void 0);
|
|
642
|
+
__decorate([
|
|
643
|
+
state()
|
|
644
|
+
], FormSelect.prototype, "_query", void 0);
|
|
232
645
|
FormSelect = __decorate([
|
|
233
646
|
customElement("form-select")
|
|
234
647
|
], FormSelect);
|