html-combobox-element 0.0.2-beta.9 → 0.0.3
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 +122 -0
- package/dist/cjs/combobox/Combobox.markup.js +34 -62
- package/dist/cjs/combobox/HTML.combobox.element.js +131 -25
- package/dist/cjs/combobox/HTML.combobox.option.element.js +3 -4
- package/dist/cjs/combobox/HTML.combobox.tag.element.js +1 -1
- package/dist/cjs/index.js +0 -4
- package/dist/esm/combobox/Combobox.markup.js +34 -62
- package/dist/esm/combobox/HTML.combobox.element.js +131 -25
- package/dist/esm/combobox/HTML.combobox.option.element.js +3 -4
- package/dist/esm/combobox/HTML.combobox.tag.element.js +1 -1
- package/dist/esm/index.js +0 -4
- package/dist/types/combobox/Combobox.markup.d.ts +2 -0
- package/dist/types/combobox/Combobox.markup.d.ts.map +1 -1
- package/dist/types/combobox/HTML.combobox.element.d.ts +8 -4
- package/dist/types/combobox/HTML.combobox.element.d.ts.map +1 -1
- package/dist/types/combobox/HTML.combobox.option.element.d.ts.map +1 -1
- package/dist/types/index.d.ts +102 -61
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +9 -2
- package/combobox-skill/SKILL.md +0 -46
- package/combobox-skill/custom-box-tag.md +0 -12
- package/combobox-skill/customization.md +0 -23
- package/combobox-skill/legacy-react-restrictions.md +0 -13
- package/combobox-skill/share-styles.md +0 -17
- package/dist/types/types.d.ts +0 -198
package/README.md
CHANGED
|
@@ -1 +1,123 @@
|
|
|
1
1
|
# html-combobox-element
|
|
2
|
+
|
|
3
|
+
The `<combo-box>` is a web component that provides dropdown/combobox functionality through `<select>` fully compatible API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
```sh
|
|
7
|
+
npm install html-combobox-element
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
// app entry point
|
|
12
|
+
import 'html-combobox-element';
|
|
13
|
+
```
|
|
14
|
+
This registers the `<combo-box>`, `<box-option>` and `<box-tag>` custom elements with full TS support for plain DOM/vanilla-JS usage.
|
|
15
|
+
|
|
16
|
+
### TypeScript + JSX
|
|
17
|
+
Using `<combo-box>` in JSX or a Vue template needs one extra step, due to a TypeScript limitation: JSX/template type-checking only picks up custom element typings from a
|
|
18
|
+
file in your own project - not from a package's shipped `.d.ts`.
|
|
19
|
+
|
|
20
|
+
This isn't specific to this package; it affects any library adding custom elements to `JSX.IntrinsicElements` under the modern automatic JSX runtime.
|
|
21
|
+
|
|
22
|
+
So, in addition to `import 'html-combobox-element'`, create and include one small file tsconfig.json, with the snippet for your framework:
|
|
23
|
+
|
|
24
|
+
<details>
|
|
25
|
+
<summary><strong>React</strong> (<code>jsx: "react-jsx"</code>)</summary>
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// combo-box.d.ts
|
|
29
|
+
declare module 'react/jsx-runtime' {
|
|
30
|
+
namespace JSX {
|
|
31
|
+
interface IntrinsicElements {
|
|
32
|
+
'combo-box': ComboboxElementAttributes;
|
|
33
|
+
'box-option': ComboboxOptionElementAttributes;
|
|
34
|
+
'box-tag': ComboboxTagElementAttributes;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
declare module 'react/jsx-dev-runtime' {
|
|
39
|
+
namespace JSX {
|
|
40
|
+
interface IntrinsicElements {
|
|
41
|
+
'combo-box': ComboboxElementAttributes;
|
|
42
|
+
'box-option': ComboboxOptionElementAttributes;
|
|
43
|
+
'box-tag': ComboboxTagElementAttributes;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
</details>
|
|
50
|
+
|
|
51
|
+
<details>
|
|
52
|
+
<summary><strong>Preact</strong> (<code>jsx: "react-jsx"</code>, <code>jsxImportSource: "preact"</code>)</summary>
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// combo-box.d.ts
|
|
56
|
+
declare module 'preact/jsx-runtime' {
|
|
57
|
+
namespace JSX {
|
|
58
|
+
interface IntrinsicElements {
|
|
59
|
+
'combo-box': ComboboxElementAttributes;
|
|
60
|
+
'box-option': ComboboxOptionElementAttributes;
|
|
61
|
+
'box-tag': ComboboxTagElementAttributes;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
</details>
|
|
68
|
+
|
|
69
|
+
<details>
|
|
70
|
+
<summary><strong>Solid</strong> (<code>jsx: "preserve"</code>, <code>jsxImportSource: "solid-js"</code>)</summary>
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// combo-box.d.ts
|
|
74
|
+
declare module 'solid-js/jsx-runtime' {
|
|
75
|
+
namespace JSX {
|
|
76
|
+
interface IntrinsicElements {
|
|
77
|
+
'combo-box': ComboboxElementAttributes;
|
|
78
|
+
'box-option': ComboboxOptionElementAttributes;
|
|
79
|
+
'box-tag': ComboboxTagElementAttributes;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
</details>
|
|
86
|
+
|
|
87
|
+
<details>
|
|
88
|
+
<summary><strong>Vue</strong> (SFC <code><template></code>, checked via <code>vue-tsc</code>)</summary>
|
|
89
|
+
|
|
90
|
+
Vue templates aren't JSX, so they're type-checked through `GlobalComponents` instead:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// combo-box.d.ts
|
|
94
|
+
declare module 'vue' {
|
|
95
|
+
export interface GlobalComponents {
|
|
96
|
+
'combo-box': new () => { $props: ComboboxElementAttributes };
|
|
97
|
+
'box-option': new () => { $props: ComboboxOptionElementAttributes };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This checks required props and value types for props you pass; like any native custom
|
|
104
|
+
element, Vue still allows extra/unrecognized attributes through (that's expected Vue
|
|
105
|
+
behavior for custom elements, not a gap in this typing). The trailing `export {};` may be
|
|
106
|
+
required here (without it the file is treated as a script, not a module, and the
|
|
107
|
+
augmentation breaks Vue's own types).
|
|
108
|
+
|
|
109
|
+
</details>
|
|
110
|
+
|
|
111
|
+
### Exported types
|
|
112
|
+
|
|
113
|
+
- `HTMLComboboxElement`, `HTMLComboboxOptionElement`, `HTMLComboboxTagElement` - element
|
|
114
|
+
interfaces, also usable via `document.querySelector('combo-box')` etc.
|
|
115
|
+
- `ComboboxEvent`, `ComboboxInputEvent`, `ComboboxFocusEvent` - typed event objects
|
|
116
|
+
(`event.target` is a properly typed `HTMLComboboxElement`).
|
|
117
|
+
- `ComboboxJsxAttributes`, `ComboboxOptionJsxAttributes` - the component's own props
|
|
118
|
+
(`value`, `multiple`, `placeholder`, `clearable`, `searchable`, `filterable`,
|
|
119
|
+
`required`, `disabled`, etc.), framework-independent.
|
|
120
|
+
- `ComboboxElementAttributes`, `ComboboxOptionElementAttributes`,
|
|
121
|
+
`ComboboxTagElementAttributes` - the above plus common passthrough attributes
|
|
122
|
+
(`class`/`className`, `style`, `ref`, `children`, `data-*`, `aria-*`, `on*`). These
|
|
123
|
+
are what the snippets above use for `'combo-box'` / `'box-option'` / `'box-tag'`.
|
|
@@ -22,10 +22,7 @@ class ComboboxMarkup {
|
|
|
22
22
|
window.addEventListener('click', this.onClickOutside);
|
|
23
23
|
this.#shadowRoot.host.addEventListener('focusin', this.onFocusIn);
|
|
24
24
|
this.#shadowRoot.host.addEventListener('focusout', this.onFocusOut);
|
|
25
|
-
// document.addEventListener('scroll', this.#onDimensionsChanges);
|
|
26
|
-
// document.addEventListener('resize', this.#onDimensionsChanges);
|
|
27
25
|
this.#rso = new ResizeObserver(this.#onDimensionsChanges);
|
|
28
|
-
// document.addEventListener('scrollend', this.onConnected);
|
|
29
26
|
}
|
|
30
27
|
connect() {
|
|
31
28
|
const placeholder = this.#shadowRoot.host.getAttribute('placeholder') || `Выбрать`;
|
|
@@ -44,6 +41,11 @@ class ComboboxMarkup {
|
|
|
44
41
|
const doc = document.importNode(innerTemplate.content, true);
|
|
45
42
|
this.tagTemplate = doc.querySelector('box-tag');
|
|
46
43
|
}
|
|
44
|
+
onHostConnected() {
|
|
45
|
+
const host = this.#shadowRoot.host;
|
|
46
|
+
const clearable = host.hasAttribute('clearable') || host.hasAttribute('multiple');
|
|
47
|
+
this.tagTemplate.toggleAttribute('visible', clearable);
|
|
48
|
+
}
|
|
47
49
|
disconnect() {
|
|
48
50
|
this.#shadowRoot.host.removeEventListener('focusin', this.onFocusIn);
|
|
49
51
|
this.#shadowRoot.host.removeEventListener('focusout', this.onFocusOut);
|
|
@@ -82,7 +84,6 @@ class ComboboxMarkup {
|
|
|
82
84
|
sort(query) {
|
|
83
85
|
clearTimeout(this.#timer);
|
|
84
86
|
this.#timer = setTimeout(() => {
|
|
85
|
-
// console.log(this.options)
|
|
86
87
|
const regex = new RegExp(query.trim(), 'i');
|
|
87
88
|
this.options.forEach((option) => {
|
|
88
89
|
if (!regex.test(option.textContent)) {
|
|
@@ -132,8 +133,6 @@ class ComboboxMarkup {
|
|
|
132
133
|
return;
|
|
133
134
|
this.#hostTop = this.#shadowRoot.host.getBoundingClientRect().top;
|
|
134
135
|
try {
|
|
135
|
-
this.dropdown.style.display = 'flex';
|
|
136
|
-
// @ts-ignore
|
|
137
136
|
this.dropdown.showPopover();
|
|
138
137
|
this.#internals.ariaExpanded = "true";
|
|
139
138
|
}
|
|
@@ -143,9 +142,7 @@ class ComboboxMarkup {
|
|
|
143
142
|
};
|
|
144
143
|
closeDropdown() {
|
|
145
144
|
try {
|
|
146
|
-
// @ts-ignore
|
|
147
145
|
this.dropdown.hidePopover();
|
|
148
|
-
this.dropdown.style.display = 'none';
|
|
149
146
|
this.#internals.ariaExpanded = "false";
|
|
150
147
|
}
|
|
151
148
|
catch (e) {
|
|
@@ -181,14 +178,12 @@ class ComboboxMarkup {
|
|
|
181
178
|
}
|
|
182
179
|
else {
|
|
183
180
|
const template = this.tagTemplate;
|
|
184
|
-
// console.log(this)
|
|
185
181
|
tag = template.cloneNode(true);
|
|
186
182
|
const label = tag.querySelector('[part="tag-label"]');
|
|
187
183
|
label.textContent = option.label || option.textContent;
|
|
188
184
|
button = tag.querySelector('[part="clear-tag"]');
|
|
189
185
|
}
|
|
190
186
|
button?.setAttribute('value', value);
|
|
191
|
-
// button.addEventListener('blur', e => e.stopPropagation());
|
|
192
187
|
tag.setAttribute('value', value);
|
|
193
188
|
this.tagsContainer.appendChild(tag);
|
|
194
189
|
this.clearAllButton?.removeAttribute('disabled');
|
|
@@ -236,6 +231,7 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
236
231
|
}
|
|
237
232
|
|
|
238
233
|
#dropdown {
|
|
234
|
+
contain: layout style paint;
|
|
239
235
|
inset: unset;
|
|
240
236
|
margin: 0;
|
|
241
237
|
/*position: absolute;*/
|
|
@@ -247,29 +243,30 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
247
243
|
border-width: inherit;
|
|
248
244
|
background-color: Canvas;
|
|
249
245
|
outline: none;
|
|
250
|
-
|
|
251
|
-
top: anchor(bottom);
|
|
246
|
+
display: none;
|
|
247
|
+
top: calc(anchor(bottom) + 2px);
|
|
252
248
|
left: anchor(left);
|
|
253
249
|
position-anchor: --position;
|
|
254
250
|
position-try: normal flip-block;
|
|
255
251
|
}
|
|
256
252
|
|
|
253
|
+
#dropdown:popover-open {
|
|
254
|
+
display: flex;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
257
|
#dropdown::-webkit-scrollbar {
|
|
258
258
|
display: none;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
|
|
261
|
+
#options {
|
|
262
262
|
display: flex;
|
|
263
263
|
flex-direction: column;
|
|
264
264
|
justify-content: start;
|
|
265
265
|
gap: 2px;
|
|
266
|
-
|
|
267
|
-
/*padding-block: .5rem;*/
|
|
268
|
-
border-radius: inherit;
|
|
269
|
-
|
|
266
|
+
border-radius: inherit;
|
|
270
267
|
}
|
|
271
268
|
|
|
272
|
-
:host([filterable])
|
|
269
|
+
:host([filterable]) #options, :host([searchable]) #options {
|
|
273
270
|
padding-top: .5rem;
|
|
274
271
|
}
|
|
275
272
|
|
|
@@ -280,8 +277,6 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
280
277
|
cursor: pointer;
|
|
281
278
|
padding: inherit;
|
|
282
279
|
padding-inline: .5lh;
|
|
283
|
-
/*padding-inline: 2px;*/
|
|
284
|
-
/*padding-block: 1px;*/
|
|
285
280
|
}
|
|
286
281
|
|
|
287
282
|
box-option:hover {
|
|
@@ -294,26 +289,17 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
294
289
|
pointer-events: none;
|
|
295
290
|
}
|
|
296
291
|
|
|
297
|
-
[part="search-input"] {
|
|
292
|
+
#dropdown > [part="search-input"] {
|
|
298
293
|
display: none;
|
|
299
294
|
position: sticky;
|
|
300
295
|
top: 0;
|
|
301
296
|
z-index: 2;
|
|
302
|
-
/*border-radius: inherit;*/
|
|
303
|
-
/*border-style: inherit;*/
|
|
304
|
-
/*border-width: inherit;*/
|
|
305
|
-
/*border-color: inherit;*/
|
|
306
|
-
/*padding: inherit;*/
|
|
307
|
-
/*padding-inline: .4lh;*/
|
|
308
297
|
font-size: inherit;
|
|
309
298
|
outline: inherit;
|
|
310
|
-
&::placeholder {
|
|
311
|
-
color: var(--search-input-placeholder-color);
|
|
312
|
-
}
|
|
313
299
|
}
|
|
314
300
|
|
|
315
|
-
:host([searchable]) [part="search-input"],
|
|
316
|
-
:host([filterable]) [part="search-input"] {
|
|
301
|
+
:host([searchable]) #dropdown > [part="search-input"],
|
|
302
|
+
:host([filterable]) #dropdown > [part="search-input"] {
|
|
317
303
|
display: flow-root;
|
|
318
304
|
}
|
|
319
305
|
|
|
@@ -321,7 +307,6 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
321
307
|
text-align: left;
|
|
322
308
|
overflow: hidden;
|
|
323
309
|
padding-inline-start: 2px;
|
|
324
|
-
/*font-size: smaller;*/
|
|
325
310
|
color: dimgrey;
|
|
326
311
|
outline: none;
|
|
327
312
|
user-select: none;
|
|
@@ -335,15 +320,12 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
335
320
|
grid-column: 1 / span 2;
|
|
336
321
|
width: 100%;
|
|
337
322
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
323
|
|
|
341
324
|
#tags {
|
|
342
325
|
display: flex;
|
|
343
326
|
flex-wrap: wrap;
|
|
344
327
|
overflow: hidden;
|
|
345
328
|
gap: 2px;
|
|
346
|
-
/*border-radius: inherit;*/
|
|
347
329
|
}
|
|
348
330
|
|
|
349
331
|
box-tag {
|
|
@@ -352,13 +334,10 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
352
334
|
box-sizing: border-box;
|
|
353
335
|
display: flex;
|
|
354
336
|
align-items: center;
|
|
355
|
-
/*border-radius: inherit;*/
|
|
356
337
|
padding-inline-start: 0.4lh;
|
|
357
338
|
padding-inline-end: 0.2lh;
|
|
358
339
|
background-color: transparent;
|
|
359
340
|
gap: 5px;
|
|
360
|
-
/*font-size: medium;*/
|
|
361
|
-
text-transform: uppercase;
|
|
362
341
|
border-radius: 1lh;
|
|
363
342
|
}
|
|
364
343
|
|
|
@@ -368,31 +347,28 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
368
347
|
max-width: 100%;
|
|
369
348
|
}
|
|
370
349
|
|
|
371
|
-
|
|
350
|
+
[part*="tag-label"] {
|
|
372
351
|
white-space: nowrap;
|
|
373
352
|
text-overflow: ellipsis;
|
|
374
353
|
overflow: hidden;
|
|
375
354
|
user-select: none;
|
|
376
|
-
/*font-size: 95%;*/
|
|
377
355
|
flex-grow: 1;
|
|
378
356
|
}
|
|
379
357
|
|
|
380
|
-
:host([multiple])
|
|
358
|
+
:host([multiple]) [part*="tag-label"] {
|
|
381
359
|
flex-grow: unset;
|
|
382
360
|
}
|
|
383
361
|
|
|
384
|
-
[part*="clear-tag"], [part*="clear-all"] {
|
|
362
|
+
[part*="clear-tag"], #internal > [part*="clear-all"] {
|
|
385
363
|
border-radius: 100%;
|
|
386
364
|
border: none;
|
|
387
365
|
aspect-ratio: 1;
|
|
388
|
-
/*line-height: 0;*/
|
|
389
366
|
padding: 0!important;
|
|
390
367
|
user-select: none;
|
|
391
368
|
background-color: transparent;
|
|
392
369
|
}
|
|
393
370
|
|
|
394
371
|
[part*="clear-tag"] {
|
|
395
|
-
/*inline-size: 1lh;*/
|
|
396
372
|
block-size: 1lh;
|
|
397
373
|
font-size: .7lh;
|
|
398
374
|
text-transform: uppercase;
|
|
@@ -404,7 +380,7 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
404
380
|
outline-width: 1px;
|
|
405
381
|
}
|
|
406
382
|
|
|
407
|
-
:host([multiple]) [part*="clear-all"] {
|
|
383
|
+
:host([multiple]) #internal > [part*="clear-all"] {
|
|
408
384
|
display: block;
|
|
409
385
|
}
|
|
410
386
|
|
|
@@ -414,13 +390,10 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
414
390
|
place-content: center;
|
|
415
391
|
}
|
|
416
392
|
|
|
417
|
-
[part*="clear-all"] {
|
|
393
|
+
#internal > [part*="clear-all"] {
|
|
418
394
|
font-size: inherit;
|
|
419
395
|
text-transform: uppercase;
|
|
420
|
-
/*inline-size: 1lh;*/
|
|
421
396
|
block-size: 1lh;
|
|
422
|
-
/*inline-size: 1.2em;*/
|
|
423
|
-
/*block-size: 1.2em;*/
|
|
424
397
|
display: none;
|
|
425
398
|
}
|
|
426
399
|
|
|
@@ -428,28 +401,20 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
428
401
|
outline: none;
|
|
429
402
|
}
|
|
430
403
|
|
|
431
|
-
[part*="clear-all"]:hover,
|
|
404
|
+
#internal > [part*="clear-all"]:hover,
|
|
432
405
|
[part*="clear-tag"]:hover {
|
|
433
406
|
color: ActiveText;
|
|
434
407
|
cursor: pointer;
|
|
435
408
|
}
|
|
436
409
|
|
|
437
|
-
[part*="clear-all"]:hover {
|
|
410
|
+
#internal >[part*="clear-all"]:hover {
|
|
438
411
|
background-color: ButtonFace;
|
|
439
412
|
}
|
|
440
413
|
|
|
441
|
-
|
|
414
|
+
#tags:empty + [part*="clear-all"] {
|
|
442
415
|
pointer-events: none;
|
|
443
416
|
outline: none;
|
|
444
417
|
}
|
|
445
|
-
|
|
446
|
-
[part*="not-found"] {
|
|
447
|
-
visibility: visible;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/*:host:has(#dropdown:popover-open) #dropdown {*/
|
|
451
|
-
/* border-top-left-radius: 0;*/
|
|
452
|
-
/*}*/
|
|
453
418
|
|
|
454
419
|
</style>
|
|
455
420
|
<div id='internal' part='internal'>
|
|
@@ -460,8 +425,9 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
460
425
|
|
|
461
426
|
<div id="dropdown" popover="manual" part='dropdown' >
|
|
462
427
|
<input placeholder='Поиск' name="search-input" part="search-input" inputmode='search' autofocus />
|
|
463
|
-
<div part="options"></div>
|
|
428
|
+
<div part="options" id='options'></div>
|
|
464
429
|
</div>
|
|
430
|
+
|
|
465
431
|
<template id='tag-template'>
|
|
466
432
|
<box-tag>
|
|
467
433
|
<span part="tag-label"></span>
|
|
@@ -469,5 +435,11 @@ ${urls.map(url => `@import "${url}";`)}
|
|
|
469
435
|
</box-tag>
|
|
470
436
|
</template>
|
|
471
437
|
`;
|
|
438
|
+
static markup;
|
|
439
|
+
static {
|
|
440
|
+
const template = document.createElement('template');
|
|
441
|
+
template.innerHTML = ComboboxMarkup.template;
|
|
442
|
+
ComboboxMarkup.markup = template;
|
|
443
|
+
}
|
|
472
444
|
}
|
|
473
445
|
exports.ComboboxMarkup = ComboboxMarkup;
|
|
@@ -7,7 +7,7 @@ const HTML_combobox_option_element_js_1 = require("./HTML.combobox.option.elemen
|
|
|
7
7
|
const Boolean_attribute_value_normalizer_js_1 = require("./Boolean.attribute.value.normalizer.js");
|
|
8
8
|
class HTMLComboboxElement extends HTMLElement {
|
|
9
9
|
static OWN_IDL = new Set(['required', 'disabled', 'clearable', 'multiple', 'filterable', 'searchable', 'value', 'placeholder', 'query']);
|
|
10
|
-
static observerOptions = { childList: true
|
|
10
|
+
static observerOptions = { childList: true };
|
|
11
11
|
static styleSheet = [new CSSStyleSheet];
|
|
12
12
|
static formAssociated = true;
|
|
13
13
|
shadowRoot;
|
|
@@ -24,32 +24,51 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
24
24
|
this.#internals.ariaHasPopup = "dialog";
|
|
25
25
|
this.shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true }); //delegatesFocus: true
|
|
26
26
|
this.#markup = new Combobox_markup_js_1.ComboboxMarkup(this.shadowRoot, this.#internals);
|
|
27
|
-
this.shadowRoot.
|
|
27
|
+
this.shadowRoot.appendChild(Combobox_markup_js_1.ComboboxMarkup.markup.content.cloneNode(true));
|
|
28
28
|
this.shadowRoot.adoptedStyleSheets = _a.styleSheet;
|
|
29
29
|
this.#observer = new MutationObserver(this.#onOptionsChanges);
|
|
30
30
|
this.#markup.connect();
|
|
31
31
|
}
|
|
32
32
|
// Lifecycle callbacks
|
|
33
33
|
connectedCallback() {
|
|
34
|
-
|
|
34
|
+
for (const child of Array.from(this.children)) {
|
|
35
|
+
if (_a.#isOptionLike(child)) {
|
|
36
|
+
this.#markup.optionsContainer?.appendChild(child);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this.#markup.onHostConnected();
|
|
35
40
|
this.shadowRoot.addEventListener('optionselectedstatechange', this.#onOptionSelectedStateChanges);
|
|
36
41
|
this.shadowRoot.addEventListener('tagcleared', this.#onClearTag);
|
|
37
|
-
this.#onOptionsChanges([{ addedNodes: Array.from(this.children) }]);
|
|
38
42
|
this.#initialAttributesSynchronization();
|
|
39
|
-
this.#observer.observe(this, _a.observerOptions);
|
|
43
|
+
this.#observer.observe(this.#markup.optionsContainer, _a.observerOptions);
|
|
40
44
|
this.addEventListener('keydown', this.#onKeyDown);
|
|
41
45
|
this.#markup.clearAllButton.addEventListener('click', this.#onClearAll);
|
|
42
46
|
this.#markup.searchInput.addEventListener('input', this.#onInput);
|
|
43
47
|
this.#markup.dropdown.addEventListener('toggle', this.#onPickerToggle);
|
|
44
48
|
this.#setValidityAndFormValue();
|
|
45
|
-
|
|
49
|
+
// Options/optgroups can end up as direct light-DOM children of <combo-box> without ever
|
|
50
|
+
// going through insertBefore/appendChild below: parsed straight from static HTML markup,
|
|
51
|
+
// or - notably - Solid's compiler, which renders static JSX subtrees via a single
|
|
52
|
+
// <template> + cloneNode(true), never calling any child-mutation method at all. Sweep
|
|
53
|
+
// whatever is already here once on connect and move it into the real options container.
|
|
54
|
+
}
|
|
55
|
+
static #isOptionLike(node) {
|
|
56
|
+
return node instanceof HTML_combobox_option_element_js_1.HTMLComboboxOptionElement || node instanceof HTMLOptGroupElement;
|
|
57
|
+
}
|
|
58
|
+
// Overwritten child-mutation methods.
|
|
59
|
+
//
|
|
60
|
+
// <box-option>/<optgroup> children don't actually live under <combo-box> itself - they're
|
|
61
|
+
// redirected into #markup.optionsContainer inside the shadow root, which is what the
|
|
62
|
+
// dropdown renders from and what #observer watches. Every native method a framework's
|
|
63
|
+
// renderer or plain user code might use to add/move/remove children needs to funnel through
|
|
64
|
+
// the same redirect, since browsers implement each of these independently (overriding one,
|
|
65
|
+
// e.g. insertBefore, does not make the others call it under the hood):
|
|
66
|
+
// - React calls appendChild for a plain append and insertBefore only when reordering.
|
|
67
|
+
// - Vue and Preact always call insertBefore, with a null reference for a plain append.
|
|
68
|
+
// - Solid mostly bypasses all of them for static subtrees (see connectedCallback above),
|
|
69
|
+
// but dynamic option lists still go through insert()/insertBefore().
|
|
46
70
|
insertBefore = (node, referenceNode) => {
|
|
47
|
-
|
|
48
|
-
// next time, when someone is calling this(combo-box).insertBefore, an error will be thrown:
|
|
49
|
-
// Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
|
|
50
|
-
// Here we overwrite this method, and if new node is HTMLComboboxOptionElement,
|
|
51
|
-
// we just delegate this job to the real option parent: optionsContainer
|
|
52
|
-
if (node instanceof HTML_combobox_option_element_js_1.HTMLComboboxOptionElement || node instanceof HTMLOptGroupElement) {
|
|
71
|
+
if (_a.#isOptionLike(node)) {
|
|
53
72
|
this.#markup.optionsContainer?.insertBefore(node, referenceNode);
|
|
54
73
|
}
|
|
55
74
|
else {
|
|
@@ -57,6 +76,52 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
57
76
|
}
|
|
58
77
|
return node;
|
|
59
78
|
};
|
|
79
|
+
appendChild = (node) => {
|
|
80
|
+
if (_a.#isOptionLike(node)) {
|
|
81
|
+
this.#markup.optionsContainer?.appendChild(node);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
super.appendChild(node);
|
|
85
|
+
}
|
|
86
|
+
return node;
|
|
87
|
+
};
|
|
88
|
+
append = (...nodes) => {
|
|
89
|
+
nodes.forEach(node => this.appendChild(typeof node === 'string' ? new Text(node) : node));
|
|
90
|
+
};
|
|
91
|
+
prepend = (...nodes) => {
|
|
92
|
+
[...nodes].reverse().forEach(node => {
|
|
93
|
+
const n = typeof node === 'string' ? new Text(node) : node;
|
|
94
|
+
if (_a.#isOptionLike(n)) {
|
|
95
|
+
this.#markup.optionsContainer?.insertBefore(n, this.#markup.optionsContainer.firstChild);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
super.insertBefore(n, this.firstChild);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
replaceChildren = (...nodes) => {
|
|
103
|
+
this.#markup.optionsContainer?.replaceChildren();
|
|
104
|
+
super.replaceChildren();
|
|
105
|
+
this.append(...nodes);
|
|
106
|
+
};
|
|
107
|
+
removeChild = (child) => {
|
|
108
|
+
if (child.parentNode === this.#markup.optionsContainer) {
|
|
109
|
+
this.#markup.optionsContainer.removeChild(child);
|
|
110
|
+
}
|
|
111
|
+
else if (child.parentNode === this) {
|
|
112
|
+
super.removeChild(child);
|
|
113
|
+
}
|
|
114
|
+
return child;
|
|
115
|
+
};
|
|
116
|
+
replaceChild = (newChild, oldChild) => {
|
|
117
|
+
if (oldChild.parentNode === this.#markup.optionsContainer) {
|
|
118
|
+
this.#markup.optionsContainer.replaceChild(newChild, oldChild);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
super.replaceChild(newChild, oldChild);
|
|
122
|
+
}
|
|
123
|
+
return oldChild;
|
|
124
|
+
};
|
|
60
125
|
get changed() {
|
|
61
126
|
return this.#changed;
|
|
62
127
|
}
|
|
@@ -72,6 +137,7 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
72
137
|
}
|
|
73
138
|
formResetCallback() {
|
|
74
139
|
this.selectedOptions.forEach(option => option.selected = false);
|
|
140
|
+
this.#markup.tagsContainer?.replaceChildren();
|
|
75
141
|
this.dispatchEvent(new Event('reset'));
|
|
76
142
|
requestAnimationFrame(() => this.#markup.searchInput?.focus());
|
|
77
143
|
}
|
|
@@ -184,11 +250,14 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
184
250
|
set value(value) {
|
|
185
251
|
if (this.value === value || typeof value !== 'string')
|
|
186
252
|
return;
|
|
187
|
-
const
|
|
188
|
-
this.#values = new Set(values);
|
|
253
|
+
const nextValues = new Set(value.split(',').filter(Boolean));
|
|
189
254
|
this.#markup.options.forEach(option => {
|
|
190
|
-
const
|
|
191
|
-
|
|
255
|
+
const optionValue = option.value;
|
|
256
|
+
// toggling `selected` drives #onOptionSelectedStateChanges, which is the
|
|
257
|
+
// single source of truth for #values and tags - don't pre-assign #values
|
|
258
|
+
// here, or the dedup logic there ends up deselecting the option it's in
|
|
259
|
+
// the middle of selecting (see: stale tag left behind on next selection)
|
|
260
|
+
option.selected = nextValues.has(optionValue);
|
|
192
261
|
});
|
|
193
262
|
}
|
|
194
263
|
// Instance methods
|
|
@@ -237,9 +306,24 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
237
306
|
// Internal
|
|
238
307
|
#onOptionsChanges = (records) => {
|
|
239
308
|
records.forEach(record => {
|
|
240
|
-
record.
|
|
241
|
-
if (
|
|
242
|
-
|
|
309
|
+
record.removedNodes.forEach(option => {
|
|
310
|
+
if (option instanceof HTML_combobox_option_element_js_1.HTMLComboboxOptionElement) {
|
|
311
|
+
const value = option.value;
|
|
312
|
+
// mode is searchable,
|
|
313
|
+
// user type "a", select a result,
|
|
314
|
+
// then types "b". Here previous options will be replaced with new ones,
|
|
315
|
+
// this.#values may contain "a", but there is no relevant option yet,
|
|
316
|
+
// so just wait
|
|
317
|
+
if (this.searchable && this.#internals.ariaExpanded === 'true') {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// options was removed, but values still include this value,
|
|
321
|
+
// just clear, this is like a reset was called
|
|
322
|
+
if (this.#values.has(value) && !this.#markup.optionsContainer?.querySelector(`[value="${value}"]`)) {
|
|
323
|
+
this.#values.delete(value);
|
|
324
|
+
const control = this.#markup.getTagByValue(value);
|
|
325
|
+
control?.remove();
|
|
326
|
+
}
|
|
243
327
|
}
|
|
244
328
|
});
|
|
245
329
|
});
|
|
@@ -251,11 +335,17 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
251
335
|
if (!this.multiple) {
|
|
252
336
|
this.#values.forEach(value => {
|
|
253
337
|
const prevOption = this.#markup.getOptionByValue(value);
|
|
254
|
-
if (prevOption)
|
|
338
|
+
if (prevOption) {
|
|
255
339
|
prevOption.selected = false;
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
const tag = this.#markup.getTagByValue(value);
|
|
343
|
+
this.#values.delete(value);
|
|
344
|
+
tag?.remove();
|
|
345
|
+
}
|
|
256
346
|
});
|
|
257
347
|
}
|
|
258
|
-
if (!this.#values.has(value)) {
|
|
348
|
+
if (!this.#values.has(value) || !this.#markup.getTagByValue(value)) {
|
|
259
349
|
this.#values.add(value);
|
|
260
350
|
this.#markup.createAndAppendTag(option);
|
|
261
351
|
this.#setValidityAndFormValue();
|
|
@@ -270,14 +360,24 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
270
360
|
this.#setValidityAndFormValue();
|
|
271
361
|
this.dispatchEvent(new Event('change'));
|
|
272
362
|
}
|
|
363
|
+
else {
|
|
364
|
+
const tag = this.#markup.getTagByValue(value);
|
|
365
|
+
tag?.remove();
|
|
366
|
+
}
|
|
273
367
|
}
|
|
274
368
|
};
|
|
275
369
|
#onClearTag = (event) => {
|
|
276
370
|
const target = event.target;
|
|
277
371
|
if (target) {
|
|
278
372
|
const option = this.#markup.getOptionByValue(target.value);
|
|
279
|
-
if (option)
|
|
373
|
+
if (option) {
|
|
280
374
|
option.selected = false;
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
const tag = this.#markup.getTagByValue(target.value);
|
|
378
|
+
this.#values.delete(target.value);
|
|
379
|
+
tag?.remove();
|
|
380
|
+
}
|
|
281
381
|
requestAnimationFrame(() => {
|
|
282
382
|
if (this.#values.size) {
|
|
283
383
|
this.focus();
|
|
@@ -319,6 +419,7 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
319
419
|
if (activeElement === this.#markup.searchInput && event.key === 'Enter') {
|
|
320
420
|
event.stopPropagation();
|
|
321
421
|
if (!this.searchable && this.filterable) {
|
|
422
|
+
console.log('sss');
|
|
322
423
|
this.#markup.sort(this.#markup.searchInput.value);
|
|
323
424
|
}
|
|
324
425
|
if (this.searchable) {
|
|
@@ -332,8 +433,14 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
332
433
|
return;
|
|
333
434
|
}
|
|
334
435
|
}
|
|
335
|
-
if (event.key === 'Enter' && activeElement
|
|
436
|
+
if (event.key === 'Enter' && activeElement.part.contains('clear-tag')) {
|
|
437
|
+
// @ts-ignore
|
|
438
|
+
// this.#onClearTag(event);
|
|
439
|
+
// event.stopPropagation();
|
|
440
|
+
// event.preventDefault()
|
|
441
|
+
// @ts-ignore
|
|
336
442
|
activeElement.click();
|
|
443
|
+
// event.stopPropagation();
|
|
337
444
|
return;
|
|
338
445
|
}
|
|
339
446
|
if (event.key === 'Escape' || event.key === 'Enter') {
|
|
@@ -366,7 +473,6 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
366
473
|
}
|
|
367
474
|
#initialAttributesSynchronization() {
|
|
368
475
|
for (const key of _a.OWN_IDL) {
|
|
369
|
-
// @ts-ignore
|
|
370
476
|
this[key] = this.getAttribute(key);
|
|
371
477
|
}
|
|
372
478
|
}
|
|
@@ -395,7 +501,7 @@ class HTMLComboboxElement extends HTMLElement {
|
|
|
395
501
|
}
|
|
396
502
|
exports.HTMLComboboxElement = HTMLComboboxElement;
|
|
397
503
|
_a = HTMLComboboxElement;
|
|
398
|
-
document.addEventListener('keypress', (
|
|
504
|
+
document.addEventListener('keypress', () => {
|
|
399
505
|
if (document.activeElement instanceof HTMLComboboxElement) {
|
|
400
506
|
const maybeHost = document.activeElement.shadowRoot.activeElement;
|
|
401
507
|
if (maybeHost instanceof HTML_combobox_option_element_js_1.HTMLComboboxOptionElement) {
|