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