html-combobox-element 0.0.2-beta.1 → 0.0.2-beta.10

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.
@@ -12,40 +12,84 @@ class ComboboxMarkup {
12
12
  placeholder = null;
13
13
  searchInput = null;
14
14
  tagTemplate = null;
15
- options;
16
- connected = false;
15
+ customTag = null;
16
+ #rso;
17
+ #hostWidth = 0;
18
+ #hostHeight = 0;
19
+ #hostTop = 0;
17
20
  constructor(shadowRoot, internals) {
18
21
  this.#shadowRoot = shadowRoot;
19
22
  this.#internals = internals;
20
- this.#shadowRoot.host.addEventListener('focus', this.showDropdown);
21
- this.#shadowRoot.host.addEventListener('blur', this.hideDropdown);
22
- this.#shadowRoot.host.addEventListener('click', this.showDropdown);
23
- document.addEventListener('click', this.hideDropdown);
24
- document.addEventListener('scroll', this.onPageScroll);
23
+ window.addEventListener('click', this.onClickOutside);
24
+ this.#shadowRoot.host.addEventListener('focusin', this.onFocusIn);
25
+ this.#shadowRoot.host.addEventListener('focusout', this.onFocusOut);
26
+ this.#rso = new ResizeObserver(this.#onDimensionsChanges);
25
27
  }
26
28
  connect() {
27
- const placeholder = this.#shadowRoot.host.getAttribute('placeholder') || '';
28
- this.tagsContainer = this.#shadowRoot.querySelector('#tags');
29
- this.optionsContainer = this.#shadowRoot.querySelector('[part*="options"]');
30
- this.clearAllButton = this.#shadowRoot.querySelector('[part*="clear-all"]');
31
- this.dropdown = this.#shadowRoot.querySelector('#dropdown');
32
- this.placeholder = this.#shadowRoot.querySelector('#placeholder');
29
+ const placeholder = this.#shadowRoot.host.getAttribute('placeholder') || `Выбрать`;
30
+ this.tagsContainer = this.#shadowRoot.querySelector('#cbtags');
31
+ this.#rso.observe(this.#shadowRoot.host, { box: 'border-box' });
32
+ this.optionsContainer = this.#shadowRoot.querySelector('#cboptions');
33
+ this.clearAllButton = this.#shadowRoot.querySelector('#cbclear');
34
+ this.clearAllButton?.addEventListener('focusout', this.onClearAllFocusOut);
35
+ this.dropdown = this.#shadowRoot.querySelector('#cbdropdown');
36
+ this.placeholder = this.#shadowRoot.querySelector('#cbplaceholder');
33
37
  this.placeholder.innerText = placeholder;
34
- this.searchInput = this.#shadowRoot.querySelector('[part*="search-input"]');
38
+ this.searchInput = this.#shadowRoot.querySelector('#cbfilter');
35
39
  this.searchInput.value = this.#shadowRoot.host.getAttribute('query');
36
- this.searchInput.placeholder = placeholder;
40
+ this.searchInput.style.setProperty('--search-input-placeholder-color', this.placeholder.style.color);
37
41
  const innerTemplate = this.#shadowRoot.querySelector('#tag-template');
38
42
  const doc = document.importNode(innerTemplate.content, true);
39
43
  this.tagTemplate = doc.querySelector('box-tag');
40
- this.connected = true;
41
44
  }
42
- invalidateOptionsCache() {
43
- this.options = this.optionsContainer.querySelectorAll('box-option');
45
+ onHostConnected() {
46
+ const host = this.#shadowRoot.host;
47
+ const clearable = host.hasAttribute('clearable') || host.hasAttribute('multiple');
48
+ this.tagTemplate.toggleAttribute('visible', clearable);
49
+ const userTagTemplate = this.#shadowRoot.host.firstElementChild;
50
+ if (userTagTemplate instanceof HTML_combobox_tag_element_js_1.HTMLComboboxTagElement) {
51
+ this.customTag = userTagTemplate;
52
+ }
53
+ }
54
+ disconnect() {
55
+ this.#shadowRoot.host.removeEventListener('focusin', this.onFocusIn);
56
+ this.#shadowRoot.host.removeEventListener('focusout', this.onFocusOut);
57
+ this.clearAllButton?.removeEventListener('focusout', this.onClearAllFocusOut);
58
+ window.removeEventListener('click', this.onClickOutside);
59
+ this.#rso.unobserve(this.#shadowRoot.host);
60
+ }
61
+ setPlaceholder(value) {
62
+ if (this.placeholder) {
63
+ this.placeholder.innerText = value.trim() || `Выбрать`;
64
+ }
65
+ }
66
+ setInputValue(value) {
67
+ if (this.searchInput) {
68
+ this.searchInput.value = value;
69
+ }
44
70
  }
71
+ onClearAllFocusOut = (event) => {
72
+ event.preventDefault();
73
+ event.stopPropagation();
74
+ if (event.relatedTarget == null) {
75
+ const host = this.#shadowRoot.host;
76
+ if (!host.hasAttribute('searchable') && !host.hasAttribute('filterable')) {
77
+ this.closeDropdown();
78
+ }
79
+ }
80
+ };
81
+ onClickOutside = (ev) => {
82
+ if (this.#internals.ariaExpanded !== 'true')
83
+ return;
84
+ if (ev.target === this.#shadowRoot.host)
85
+ return;
86
+ this.closeDropdown();
87
+ };
45
88
  #timer = undefined;
46
89
  sort(query) {
47
90
  clearTimeout(this.#timer);
48
91
  this.#timer = setTimeout(() => {
92
+ // console.log(this.options)
49
93
  const regex = new RegExp(query.trim(), 'i');
50
94
  this.options.forEach((option) => {
51
95
  if (!regex.test(option.textContent)) {
@@ -56,78 +100,77 @@ class ComboboxMarkup {
56
100
  }
57
101
  });
58
102
  this.dropdown.scrollTo({ top: 0, behavior: 'smooth' });
59
- }, 200);
103
+ }, 400);
60
104
  }
61
- disconnect() {
62
- this.#shadowRoot.host.removeEventListener('focus', this.showDropdown);
63
- this.#shadowRoot.host.removeEventListener('blur', this.hideDropdown);
64
- this.#shadowRoot.host.removeEventListener('click', this.showDropdown);
65
- document.removeEventListener('click', this.hideDropdown);
66
- document.removeEventListener('scroll', this.onPageScroll);
67
- this.connected = false;
105
+ get options() {
106
+ return this.optionsContainer.querySelectorAll('box-option');
107
+ }
108
+ getTagControl(tag) {
109
+ return tag.querySelector('[part*="clear-tag"]');
68
110
  }
69
- onPageScroll = () => {
70
- if (this.dropdown.matches(':popover-open')) {
71
- this.setDropdownPosition(this.#shadowRoot.host.getBoundingClientRect());
111
+ #onDimensionsChanges = (entries) => {
112
+ const entry = entries[0];
113
+ if (entry) {
114
+ this.#hostWidth = entry.borderBoxSize[0].inlineSize;
115
+ this.#hostHeight = entry.borderBoxSize[0].blockSize;
72
116
  }
117
+ this.setDropdownPosition();
73
118
  };
74
- setDropdownPosition(rect) {
119
+ setDropdownPosition() {
75
120
  const dropdown = this.dropdown;
76
121
  const vh = window.innerHeight;
77
- const sh = rect.height;
78
- const sy = rect.top;
79
- if (sy < (vh - vh / 3)) {
80
- dropdown.style.top = sh + sy + 'px';
81
- dropdown.style.bottom = 'unset';
82
- dropdown.style.transform = `unset`;
83
- }
84
- else {
85
- dropdown.style.top = sh + sy + 'px';
86
- dropdown.style.bottom = 'unset';
87
- dropdown.style.transform = `translateY(calc(-1 * calc(100% + ${sh}px)))`;
88
- }
89
- dropdown.style.left = rect.left + 'px';
90
- dropdown.style.width = rect.width + 'px';
91
- dropdown.style.maxHeight = '50vh';
122
+ const sh = this.#hostHeight;
123
+ const sy = this.#hostTop;
124
+ // Calculate available space below and above
125
+ const spaceBelow = vh - (sy + sh);
126
+ const spaceAbove = sy;
127
+ // Determine if dropdown should go below or above
128
+ const shouldGoBelow = spaceBelow >= spaceAbove;
129
+ // Set dropdown to max 50vh or available space, whichever is smaller
130
+ const maxHeight = Math.min(50 * vh / 100, shouldGoBelow ? spaceBelow - 10 : spaceAbove - 10);
131
+ dropdown.style.width = this.#hostWidth + 'px';
132
+ dropdown.style.maxHeight = `${Math.max(100, maxHeight)}px`;
133
+ }
134
+ showDropdown() {
135
+ this.onFocusIn();
92
136
  }
93
- showDropdown = () => {
137
+ onFocusIn = () => {
138
+ if (this.#internals.ariaExpanded === "true")
139
+ return;
140
+ this.#hostTop = this.#shadowRoot.host.getBoundingClientRect().top;
94
141
  try {
95
- this.setDropdownPosition(this.#shadowRoot.host.getBoundingClientRect());
96
- this.dropdown.style.display = 'flex';
142
+ // this.dropdown!.style.display = 'flex';
143
+ // @ts-ignore
97
144
  this.dropdown.showPopover();
98
145
  this.#internals.ariaExpanded = "true";
99
- if (this.tagsContainer?.children.length === 0) {
100
- this.searchInput?.focus();
101
- }
102
- this.placeholder.innerText = '';
103
146
  }
104
- catch {
147
+ catch (e) {
105
148
  this.#internals.ariaExpanded = "false";
106
149
  }
107
150
  };
108
151
  closeDropdown() {
109
152
  try {
153
+ // @ts-ignore
110
154
  this.dropdown.hidePopover();
111
- this.dropdown.style.display = 'none';
155
+ // this.dropdown!.style.display = 'none';
112
156
  this.#internals.ariaExpanded = "false";
113
- this.placeholder.innerText = this.#shadowRoot.host.getAttribute('placeholder');
114
157
  }
115
158
  catch (e) {
116
159
  this.#internals.ariaExpanded = "true";
117
160
  }
118
161
  }
119
- hideDropdown = (event) => {
120
- if (event.composedPath().includes(this.#shadowRoot.host))
162
+ onFocusOut = () => {
163
+ if (this.#internals.ariaExpanded !== "true")
121
164
  return;
122
165
  this.closeDropdown();
123
166
  };
124
167
  createAndAppendTag(option) {
125
168
  const value = option.value;
126
- const userTagTemplate = this.#shadowRoot.host.firstElementChild;
169
+ const userTagTemplate = this.customTag;
127
170
  let tag;
128
171
  let button;
129
172
  if (userTagTemplate && userTagTemplate instanceof HTML_combobox_tag_element_js_1.HTMLComboboxTagElement) {
130
- tag = userTagTemplate.cloneNode(true);
173
+ tag = document.importNode(userTagTemplate, true);
131
174
  tag.querySelectorAll('slot')
132
175
  .forEach(node => {
133
176
  const relatedNode = option.querySelector(`[slot="${node.name}"]`);
@@ -145,25 +188,29 @@ class ComboboxMarkup {
145
188
  }
146
189
  else {
147
190
  const template = this.tagTemplate;
191
+ // console.log(this)
148
192
  tag = template.cloneNode(true);
149
- const label = tag.querySelector('[part="tag-label"]');
150
- label.textContent = option.textContent;
151
- button = tag.querySelector('[part="clear-tag"]');
193
+ // console.log(template)
194
+ const label = tag.querySelector('.cb-tag-label');
195
+ label.textContent = option.label || option.textContent;
196
+ button = tag.querySelector('.cb-clear-tag');
152
197
  }
153
198
  button?.setAttribute('value', value);
199
+ // button.addEventListener('blur', e => e.stopPropagation());
154
200
  tag.setAttribute('value', value);
155
201
  this.tagsContainer.appendChild(tag);
202
+ this.clearAllButton?.removeAttribute('disabled');
156
203
  return button;
157
204
  }
158
205
  getTagByValue(value) {
206
+ // console.log(this.tagsContainer)
159
207
  return this.tagsContainer.querySelector(`box-tag[value="${value}"]`);
160
208
  }
161
209
  getOptionByValue(value) {
162
210
  return this.optionsContainer.querySelector(`box-option[value="${value}"]`);
163
211
  }
164
212
  get selectedOptions() {
165
- return this.optionsContainer
166
- .querySelectorAll('box-option[selected]');
213
+ return this.optionsContainer.querySelectorAll('box-option[selected]');
167
214
  }
168
215
  static importCSS(urls) {
169
216
  ComboboxMarkup.template = ComboboxMarkup.template.replace(':host {', `
@@ -177,35 +224,65 @@ ${urls.map(url => `@import "${url}";`)}
177
224
  :host {
178
225
  font-size: inherit;
179
226
  font-family: inherit;
227
+ display: flow-root;
228
+ position: relative;
229
+ anchor-name: --position;
230
+ }
231
+
232
+ #cbwrapper {
233
+ min-block-size: 1lh;
180
234
  display: grid;
181
235
  grid-template-columns: minmax(0, max-content) 1fr;
182
236
  align-items: center;
183
237
  gap: 1px;
184
- position: relative;
238
+ border-radius: inherit;
185
239
  }
186
240
 
187
- :host([multiple]) {
241
+ :host([multiple]) #cbwrapper {
188
242
  grid-template-columns: minmax(0, max-content) 1fr auto;
189
243
  }
190
244
 
191
- #dropdown {
245
+ #cbdropdown {
246
+ contain: layout style paint;
192
247
  inset: unset;
193
248
  margin: 0;
249
+ /*position: absolute;*/
194
250
  box-sizing: border-box;
195
251
  overflow-y: scroll;
196
252
  flex-direction: column;
197
253
  border-radius: inherit;
198
254
  border-color: ButtonFace;
199
255
  border-width: inherit;
256
+ background-color: Canvas;
257
+ outline: none;
258
+ display: none;
259
+ top: anchor(bottom);
260
+ left: anchor(left);
261
+ position-anchor: --position;
262
+ position-try: normal flip-block;
263
+ }
264
+
265
+ #cbdropdown:popover-open {
266
+ display: flex;
267
+ }
268
+
269
+ #cbdropdown::-webkit-scrollbar {
270
+ display: none;
200
271
  }
201
272
 
202
- [part="options"] {
273
+ #cboptions {
203
274
  display: flex;
204
275
  flex-direction: column;
205
276
  justify-content: start;
206
277
  gap: 2px;
207
- padding-block: .5rem;
278
+ /*padding: inherit;*/
279
+ /*padding-block: .5rem;*/
208
280
  border-radius: inherit;
281
+
282
+ }
283
+
284
+ :host([filterable]) #cboptions, :host([searchable]) #cboptions {
285
+ padding-top: .5rem;
209
286
  }
210
287
 
211
288
  box-option {
@@ -213,8 +290,10 @@ ${urls.map(url => `@import "${url}";`)}
213
290
  border-radius: inherit;
214
291
  content-visibility: auto;
215
292
  cursor: pointer;
216
- padding-inline: 2px;
217
- padding-block: 1px;
293
+ padding: inherit;
294
+ padding-inline: .5lh;
295
+ /*padding-inline: 2px;*/
296
+ /*padding-block: 1px;*/
218
297
  }
219
298
 
220
299
  box-option:hover {
@@ -227,46 +306,56 @@ ${urls.map(url => `@import "${url}";`)}
227
306
  pointer-events: none;
228
307
  }
229
308
 
230
- [part="search-input"] {
309
+ #cbfilter {
231
310
  display: none;
232
311
  position: sticky;
233
312
  top: 0;
234
313
  z-index: 2;
235
- border-radius: inherit;
236
- border-style: inherit;
237
- border-width: inherit;
238
- border-color: inherit;
239
- padding: inherit;
314
+ /*border-radius: inherit;*/
315
+ /*border-style: inherit;*/
316
+ /*border-width: inherit;*/
317
+ /*border-color: inherit;*/
318
+ /*padding: inherit;*/
319
+ /*padding-inline: .4lh;*/
320
+ font-size: inherit;
321
+ outline: inherit;
322
+ /*&::placeholder {*/
323
+ /* color: var(--search-input-placeholder-color);*/
324
+ /*} */
240
325
  }
241
326
 
242
- :host([searchable]) [part="search-input"],
243
- :host([filterable]) [part="search-input"] {
244
- display: flex;
327
+ :host([searchable]) #cbfilter,
328
+ :host([filterable]) #cbfilter {
329
+ display: flow-root;
245
330
  }
246
331
 
247
- #placeholder {
332
+ #cbplaceholder {
248
333
  text-align: left;
249
334
  overflow: hidden;
250
335
  padding-inline-start: 2px;
251
- font-size: smaller;
336
+ /*font-size: smaller;*/
252
337
  color: dimgrey;
338
+ outline: none;
339
+ user-select: none;
253
340
  }
254
341
 
255
- #tags:not(:empty) + #placeholder {
342
+ #cbtags:not(:empty) + #cbplaceholder {
256
343
  display: none;
257
344
  }
258
345
 
259
- #tags:not(:empty) {
346
+ #cbtags:not(:empty) {
260
347
  grid-column: 1 / span 2;
261
348
  width: 100%;
262
349
  }
350
+
351
+
263
352
 
264
- #tags {
353
+ #cbtags {
265
354
  display: flex;
266
355
  flex-wrap: wrap;
267
356
  overflow: hidden;
268
357
  gap: 2px;
269
- border-radius: inherit;
358
+ /*border-radius: inherit;*/
270
359
  }
271
360
 
272
361
  box-tag {
@@ -275,13 +364,14 @@ ${urls.map(url => `@import "${url}";`)}
275
364
  box-sizing: border-box;
276
365
  display: flex;
277
366
  align-items: center;
278
- border-radius: inherit;
279
- padding-inline-start: 0.2lh;
280
- padding-inline-end: .2rem;
367
+ /*border-radius: inherit;*/
368
+ padding-inline-start: 0.4lh;
369
+ padding-inline-end: 0.2lh;
281
370
  background-color: transparent;
282
371
  gap: 5px;
283
- font-size: medium;
372
+ /*font-size: medium;*/
284
373
  text-transform: uppercase;
374
+ border-radius: 1lh;
285
375
  }
286
376
 
287
377
  :host([multiple]) box-tag {
@@ -290,80 +380,100 @@ ${urls.map(url => `@import "${url}";`)}
290
380
  max-width: 100%;
291
381
  }
292
382
 
293
- box-tag [part*="tag-label"] {
383
+ .cb-tag-label {
294
384
  white-space: nowrap;
295
385
  text-overflow: ellipsis;
296
386
  overflow: hidden;
297
387
  user-select: none;
298
- font-size: 95%;
388
+ /*font-size: 95%;*/
299
389
  flex-grow: 1;
300
390
  }
301
391
 
302
- :host([multiple]) box-tag[part*="tag-label"] {
392
+ :host([multiple]) .cb-tag-label {
303
393
  flex-grow: unset;
304
394
  }
305
395
 
306
- [part*="clear-tag"], [part*="clear-all"] {
396
+ .cb-clear-tag, #cbclear {
307
397
  border-radius: 100%;
308
398
  border: none;
309
399
  aspect-ratio: 1;
310
- line-height: 0;
400
+ /*line-height: 0;*/
311
401
  padding: 0!important;
312
402
  user-select: none;
313
403
  background-color: transparent;
314
404
  }
315
405
 
316
- [part*="clear-tag"] {
317
- inline-size: 1em;
318
- block-size: 1em;
319
- font-size: 80%;
406
+ .cb-clear-tag {
407
+ /*inline-size: 1lh;*/
408
+ block-size: 1lh;
409
+ font-size: .7lh;
410
+ text-transform: uppercase;
320
411
  display: none;
321
412
  }
413
+
414
+ .cb-clear-tag:focus-visible, .cb-clear-tag:focus {
415
+ outline-style: solid;
416
+ outline-width: 1px;
417
+ }
322
418
 
323
- :host([multiple]) [part*="clear-all"] {
419
+ :host([multiple]) #cbclear {
324
420
  display: block;
325
421
  }
326
422
 
327
- :host([clearable]) [part*="clear-tag"] {
328
- display: block;
423
+ :host([clearable]) #cbclear,
424
+ :host([multiple]) .cb-clear-tag {
425
+ display: grid;
426
+ place-content: center;
329
427
  }
330
428
 
331
- [part*="clear-all"] {
429
+ #cbclear {
332
430
  font-size: inherit;
333
- inline-size: 1.2em;
334
- block-size: 1.2em;
431
+ text-transform: uppercase;
432
+ /*inline-size: 1lh;*/
433
+ block-size: 1lh;
434
+ /*inline-size: 1.2em;*/
435
+ /*block-size: 1.2em;*/
335
436
  display: none;
336
437
  }
337
438
 
338
- [part*="clear-all"]:hover,
339
- [part*="clear-tag"]:hover {
439
+ #cbclear:hover, .cb-clear-tag:hover {
340
440
  color: ActiveText;
341
441
  cursor: pointer;
342
442
  }
343
443
 
344
- [part*="clear-all"]:hover {
444
+ #cbclear:hover {
345
445
  background-color: ButtonFace;
346
446
  }
347
447
 
348
- :host:has(#tags:empty) [part*="clear-all"] {
448
+ #cbtags:empty + #cbclear {
349
449
  pointer-events: none;
350
- color: darkgrey;
450
+ outline: none;
351
451
  }
352
452
  </style>
353
453
 
354
- <div id="tags"></div>
355
- <div id="placeholder">&nbsp;</div>
356
- <button part="clear-all">✕</button>
357
- <div id="dropdown" popover="manual">
358
- <input name="search-input" part="search-input" />
359
- <div part="options"></div>
454
+ <div id='cbwrapper' part='internal'>
455
+ <div id="cbtags" part='tags'></div>
456
+ <div id="cbplaceholder" part='placeholder' tabindex='0'>Выбрать</div>
457
+ <button id="cbclear" part="clear-all">✕</button>
360
458
  </div>
459
+
460
+ <div id="cbdropdown" popover="manual" part='dropdown'>
461
+ <input id="cbfilter" placeholder='Поиск' name="filter" part="search-input" inputmode='search' autofocus />
462
+ <div part="options" id='cboptions'></div>
463
+ </div>
464
+
361
465
  <template id='tag-template'>
362
466
  <box-tag>
363
- <span part="tag-label"></span>
364
- <button part="clear-tag">✕</button>
467
+ <span class="cb-tag-label" part="tag-label"></span>
468
+ <div class="cb-clear-tag" part="clear-tag" role='button' tabindex='0'>✕</div>
365
469
  </box-tag>
366
470
  </template>
367
471
  `;
472
+ static markup;
473
+ static {
474
+ const template = document.createElement('template');
475
+ template.innerHTML = ComboboxMarkup.template;
476
+ ComboboxMarkup.markup = template;
477
+ }
368
478
  }
369
479
  exports.ComboboxMarkup = ComboboxMarkup;