html-combobox-element 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/combobox/Boolean.attribute.value.normalizer.js +11 -0
  3. package/dist/cjs/combobox/Combobox.markup.js +371 -0
  4. package/dist/cjs/combobox/HTML.combobox.element.js +365 -0
  5. package/dist/cjs/combobox/HTML.combobox.option.element.js +65 -0
  6. package/dist/cjs/combobox/HTML.combobox.tag.element.js +22 -0
  7. package/dist/cjs/combobox/index.js +17 -0
  8. package/dist/cjs/index.js +83 -0
  9. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
  10. package/dist/esm/combobox/Boolean.attribute.value.normalizer.js +8 -0
  11. package/dist/esm/combobox/Combobox.markup.js +367 -0
  12. package/dist/esm/combobox/HTML.combobox.element.js +359 -0
  13. package/dist/esm/combobox/HTML.combobox.option.element.js +59 -0
  14. package/dist/esm/combobox/HTML.combobox.tag.element.js +18 -0
  15. package/dist/esm/combobox/index.js +1 -0
  16. package/dist/esm/index.js +67 -0
  17. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
  18. package/dist/types/combobox/Boolean.attribute.value.normalizer.d.ts +3 -0
  19. package/dist/types/combobox/Boolean.attribute.value.normalizer.d.ts.map +1 -0
  20. package/dist/types/combobox/Combobox.markup.d.ts +31 -0
  21. package/dist/types/combobox/Combobox.markup.d.ts.map +1 -0
  22. package/dist/types/combobox/HTML.combobox.element.d.ts +55 -0
  23. package/dist/types/combobox/HTML.combobox.element.d.ts.map +1 -0
  24. package/dist/types/combobox/HTML.combobox.option.element.d.ts +14 -0
  25. package/dist/types/combobox/HTML.combobox.option.element.d.ts.map +1 -0
  26. package/dist/types/combobox/HTML.combobox.tag.element.d.ts +5 -0
  27. package/dist/types/combobox/HTML.combobox.tag.element.d.ts.map +1 -0
  28. package/dist/types/combobox/index.d.ts +2 -0
  29. package/dist/types/combobox/index.d.ts.map +1 -0
  30. package/dist/types/index.d.ts +214 -0
  31. package/dist/types/index.d.ts.map +1 -0
  32. package/dist/types/tsconfig.types.tsbuildinfo +1 -0
  33. package/dist/types/types.d.ts +198 -0
  34. package/package.json +45 -0
@@ -0,0 +1,367 @@
1
+ import { HTMLComboboxTagElement } from './HTML.combobox.tag.element.js';
2
+ export class ComboboxMarkup {
3
+ #shadowRoot;
4
+ #internals;
5
+ tagsContainer = null;
6
+ optionsContainer = null;
7
+ clearAllButton = null;
8
+ dropdown = null;
9
+ placeholder = null;
10
+ searchInput = null;
11
+ tagTemplate = null;
12
+ options;
13
+ connected = false;
14
+ constructor(shadowRoot, internals) {
15
+ this.#shadowRoot = shadowRoot;
16
+ this.#internals = internals;
17
+ this.#shadowRoot.host.addEventListener('focus', this.showDropdown);
18
+ this.#shadowRoot.host.addEventListener('blur', this.hideDropdown);
19
+ this.#shadowRoot.host.addEventListener('click', this.showDropdown);
20
+ document.addEventListener('click', this.hideDropdown);
21
+ document.addEventListener('scroll', this.onPageScroll);
22
+ }
23
+ connect() {
24
+ const placeholder = this.#shadowRoot.host.getAttribute('placeholder') || '';
25
+ this.tagsContainer = this.#shadowRoot.querySelector('#tags');
26
+ this.optionsContainer = this.#shadowRoot.querySelector('[part*="options"]');
27
+ this.clearAllButton = this.#shadowRoot.querySelector('[part*="clear-all"]');
28
+ this.dropdown = this.#shadowRoot.querySelector('#dropdown');
29
+ this.placeholder = this.#shadowRoot.querySelector('#placeholder');
30
+ this.placeholder.innerText = placeholder;
31
+ this.searchInput = this.#shadowRoot.querySelector('[part*="search-input"]');
32
+ this.searchInput.value = this.#shadowRoot.host.getAttribute('query');
33
+ this.searchInput.placeholder = placeholder;
34
+ const innerTemplate = this.#shadowRoot.querySelector('#tag-template');
35
+ const doc = document.importNode(innerTemplate.content, true);
36
+ this.tagTemplate = doc.querySelector('box-tag');
37
+ this.connected = true;
38
+ }
39
+ invalidateOptionsCache() {
40
+ this.options = this.optionsContainer.querySelectorAll('box-option');
41
+ }
42
+ #timer = undefined;
43
+ sort(query) {
44
+ clearTimeout(this.#timer);
45
+ this.#timer = setTimeout(() => {
46
+ const regex = new RegExp(query.trim(), 'i');
47
+ this.options.forEach((option) => {
48
+ if (!regex.test(option.textContent)) {
49
+ option.style.display = "none";
50
+ }
51
+ else {
52
+ option.style.display = "flex";
53
+ }
54
+ });
55
+ this.dropdown.scrollTo({ top: 0, behavior: 'smooth' });
56
+ }, 200);
57
+ }
58
+ disconnect() {
59
+ this.#shadowRoot.host.removeEventListener('focus', this.showDropdown);
60
+ this.#shadowRoot.host.removeEventListener('blur', this.hideDropdown);
61
+ this.#shadowRoot.host.removeEventListener('click', this.showDropdown);
62
+ document.removeEventListener('click', this.hideDropdown);
63
+ document.removeEventListener('scroll', this.onPageScroll);
64
+ this.connected = false;
65
+ }
66
+ onPageScroll = () => {
67
+ if (this.dropdown.matches(':popover-open')) {
68
+ this.setDropdownPosition(this.#shadowRoot.host.getBoundingClientRect());
69
+ }
70
+ };
71
+ setDropdownPosition(rect) {
72
+ const dropdown = this.dropdown;
73
+ const vh = window.innerHeight;
74
+ const sh = rect.height;
75
+ const sy = rect.top;
76
+ if (sy < (vh - vh / 3)) {
77
+ dropdown.style.top = sh + sy + 'px';
78
+ dropdown.style.bottom = 'unset';
79
+ dropdown.style.transform = `unset`;
80
+ }
81
+ else {
82
+ dropdown.style.top = sh + sy + 'px';
83
+ dropdown.style.bottom = 'unset';
84
+ dropdown.style.transform = `translateY(calc(-1 * calc(100% + ${sh}px)))`;
85
+ }
86
+ dropdown.style.left = rect.left + 'px';
87
+ dropdown.style.width = rect.width + 'px';
88
+ dropdown.style.maxHeight = '50vh';
89
+ }
90
+ showDropdown = () => {
91
+ try {
92
+ this.setDropdownPosition(this.#shadowRoot.host.getBoundingClientRect());
93
+ this.dropdown.style.display = 'flex';
94
+ this.dropdown.showPopover();
95
+ this.#internals.ariaExpanded = "true";
96
+ if (this.tagsContainer?.children.length === 0) {
97
+ this.searchInput?.focus();
98
+ }
99
+ this.placeholder.innerText = '';
100
+ }
101
+ catch {
102
+ this.#internals.ariaExpanded = "false";
103
+ }
104
+ };
105
+ closeDropdown() {
106
+ try {
107
+ this.dropdown.hidePopover();
108
+ this.dropdown.style.display = 'none';
109
+ this.#internals.ariaExpanded = "false";
110
+ this.placeholder.innerText = this.#shadowRoot.host.getAttribute('placeholder');
111
+ }
112
+ catch (e) {
113
+ this.#internals.ariaExpanded = "true";
114
+ }
115
+ }
116
+ hideDropdown = (event) => {
117
+ if (event.composedPath().includes(this.#shadowRoot.host))
118
+ return;
119
+ this.closeDropdown();
120
+ };
121
+ createAndAppendTag(option) {
122
+ const value = option.value;
123
+ const userTagTemplate = this.#shadowRoot.host.firstElementChild;
124
+ let tag;
125
+ let button;
126
+ if (userTagTemplate && userTagTemplate instanceof HTMLComboboxTagElement) {
127
+ tag = userTagTemplate.cloneNode(true);
128
+ tag.querySelectorAll('slot')
129
+ .forEach(node => {
130
+ const relatedNode = option.querySelector(`[slot="${node.name}"]`);
131
+ if (relatedNode) {
132
+ const clone = relatedNode.cloneNode(true);
133
+ clone.part.remove(...clone.part.values());
134
+ clone.part.add(...node.part.values());
135
+ clone.classList.add(...node.classList.values());
136
+ tag.replaceChild(clone, node);
137
+ }
138
+ });
139
+ tag.part.add(...option.part.values());
140
+ tag.part.remove('option');
141
+ button = tag.querySelector('[part*="clear-tag"]');
142
+ }
143
+ else {
144
+ const template = this.tagTemplate;
145
+ tag = template.cloneNode(true);
146
+ const label = tag.querySelector('[part="tag-label"]');
147
+ label.textContent = option.textContent;
148
+ button = tag.querySelector('[part="clear-tag"]');
149
+ }
150
+ button?.setAttribute('value', value);
151
+ tag.setAttribute('value', value);
152
+ this.tagsContainer.appendChild(tag);
153
+ return button;
154
+ }
155
+ getTagByValue(value) {
156
+ return this.tagsContainer.querySelector(`box-tag[value="${value}"]`);
157
+ }
158
+ getOptionByValue(value) {
159
+ return this.optionsContainer.querySelector(`box-option[value="${value}"]`);
160
+ }
161
+ get selectedOptions() {
162
+ return this.optionsContainer
163
+ .querySelectorAll('box-option[selected]');
164
+ }
165
+ static importCSS(urls) {
166
+ ComboboxMarkup.template = ComboboxMarkup.template.replace(':host {', `
167
+ ${urls.map(url => `@import "${url}";`)}
168
+
169
+ :host {
170
+ `);
171
+ }
172
+ static template = `
173
+ <style>
174
+ :host {
175
+ font-size: inherit;
176
+ font-family: inherit;
177
+ display: grid;
178
+ grid-template-columns: minmax(0, max-content) 1fr;
179
+ align-items: center;
180
+ gap: 1px;
181
+ position: relative;
182
+ }
183
+
184
+ :host([multiple]) {
185
+ grid-template-columns: minmax(0, max-content) 1fr auto;
186
+ }
187
+
188
+ #dropdown {
189
+ inset: unset;
190
+ margin: 0;
191
+ box-sizing: border-box;
192
+ overflow-y: scroll;
193
+ flex-direction: column;
194
+ border-radius: inherit;
195
+ border-color: ButtonFace;
196
+ border-width: inherit;
197
+ }
198
+
199
+ [part="options"] {
200
+ display: flex;
201
+ flex-direction: column;
202
+ justify-content: start;
203
+ gap: 2px;
204
+ padding-block: .5rem;
205
+ border-radius: inherit;
206
+ }
207
+
208
+ box-option {
209
+ display: flex;
210
+ border-radius: inherit;
211
+ content-visibility: auto;
212
+ cursor: pointer;
213
+ padding-inline: 2px;
214
+ padding-block: 1px;
215
+ }
216
+
217
+ box-option:hover {
218
+ background-color: color-mix(in srgb, Highlight, transparent 70%);
219
+ }
220
+
221
+ box-option[selected] {
222
+ background-color: Highlight;
223
+ cursor: not-allowed;
224
+ pointer-events: none;
225
+ }
226
+
227
+ [part="search-input"] {
228
+ display: none;
229
+ position: sticky;
230
+ top: 0;
231
+ z-index: 2;
232
+ border-radius: inherit;
233
+ border-style: inherit;
234
+ border-width: inherit;
235
+ border-color: inherit;
236
+ padding: inherit;
237
+ }
238
+
239
+ :host([searchable]) [part="search-input"],
240
+ :host([filterable]) [part="search-input"] {
241
+ display: flex;
242
+ }
243
+
244
+ #placeholder {
245
+ text-align: left;
246
+ overflow: hidden;
247
+ padding-inline-start: 2px;
248
+ font-size: smaller;
249
+ color: dimgrey;
250
+ }
251
+
252
+ #tags:not(:empty) + #placeholder {
253
+ display: none;
254
+ }
255
+
256
+ #tags:not(:empty) {
257
+ grid-column: 1 / span 2;
258
+ width: 100%;
259
+ }
260
+
261
+ #tags {
262
+ display: flex;
263
+ flex-wrap: wrap;
264
+ overflow: hidden;
265
+ gap: 2px;
266
+ border-radius: inherit;
267
+ }
268
+
269
+ box-tag {
270
+ width: 100%;
271
+ justify-self: start;
272
+ box-sizing: border-box;
273
+ display: flex;
274
+ align-items: center;
275
+ border-radius: inherit;
276
+ padding-inline-start: 0.2lh;
277
+ padding-inline-end: .2rem;
278
+ background-color: transparent;
279
+ gap: 5px;
280
+ font-size: medium;
281
+ text-transform: uppercase;
282
+ }
283
+
284
+ :host([multiple]) box-tag {
285
+ background-color: Highlight;
286
+ width: fit-content;
287
+ max-width: 100%;
288
+ }
289
+
290
+ box-tag [part*="tag-label"] {
291
+ white-space: nowrap;
292
+ text-overflow: ellipsis;
293
+ overflow: hidden;
294
+ user-select: none;
295
+ font-size: 95%;
296
+ flex-grow: 1;
297
+ }
298
+
299
+ :host([multiple]) box-tag [part*="tag-label"] {
300
+ flex-grow: unset;
301
+ }
302
+
303
+ [part*="clear-tag"], [part*="clear-all"] {
304
+ border-radius: 100%;
305
+ border: none;
306
+ aspect-ratio: 1;
307
+ line-height: 0;
308
+ padding: 0!important;
309
+ user-select: none;
310
+ background-color: transparent;
311
+ }
312
+
313
+ [part*="clear-tag"] {
314
+ inline-size: 1em;
315
+ block-size: 1em;
316
+ font-size: 80%;
317
+ display: none;
318
+ }
319
+
320
+ :host([multiple]) [part*="clear-tag"],
321
+ :host([clearable]) [part*="clear-all"] {
322
+ display: block;
323
+ }
324
+
325
+ [part*="clear-all"] {
326
+ font-size: inherit;
327
+ inline-size: 1.2em;
328
+ block-size: 1.2em;
329
+ display: none;
330
+ }
331
+
332
+ :host([multiple]) [part*="clear-all"] {
333
+ display: block;
334
+ }
335
+
336
+ [part*="clear-all"]:hover,
337
+ [part*="clear-tag"]:hover {
338
+ color: ActiveText;
339
+ cursor: pointer;
340
+ }
341
+
342
+ [part*="clear-all"]:hover {
343
+ background-color: ButtonFace;
344
+ }
345
+
346
+ :host:has(#tags:empty) [part*="clear-all"] {
347
+ pointer-events: none;
348
+ color: darkgrey;
349
+ }
350
+
351
+ </style>
352
+
353
+ <div id="tags"></div>
354
+ <div id="placeholder">&nbsp;</div>
355
+ <button part="clear-all">✕</button>
356
+ <div id="dropdown" popover="manual">
357
+ <input name="search-input" part="search-input" />
358
+ <div part="options"></div>
359
+ </div>
360
+ <template id='tag-template'>
361
+ <box-tag>
362
+ <span part="tag-label"></span>
363
+ <button part="clear-tag">✕</button>
364
+ </box-tag>
365
+ </template>
366
+ `;
367
+ }