@aquera/nile-elements 1.7.2 → 1.7.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 +4 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +387 -265
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js +1 -1
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js.map +1 -1
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.esm.js +8 -6
- package/dist/nile-combobox/group-utils.cjs.js +2 -0
- package/dist/nile-combobox/group-utils.cjs.js.map +1 -0
- package/dist/nile-combobox/group-utils.esm.js +1 -0
- package/dist/nile-combobox/index.cjs.js +1 -1
- package/dist/nile-combobox/index.esm.js +1 -1
- package/dist/nile-combobox/nile-combobox.cjs.js +1 -1
- package/dist/nile-combobox/nile-combobox.cjs.js.map +1 -1
- package/dist/nile-combobox/nile-combobox.css.cjs.js +1 -1
- package/dist/nile-combobox/nile-combobox.css.cjs.js.map +1 -1
- package/dist/nile-combobox/nile-combobox.css.esm.js +77 -4
- package/dist/nile-combobox/nile-combobox.esm.js +13 -8
- package/dist/nile-combobox/renderer.cjs.js +1 -1
- package/dist/nile-combobox/renderer.cjs.js.map +1 -1
- package/dist/nile-combobox/renderer.esm.js +84 -42
- package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js +4 -2
- package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js.map +1 -1
- package/dist/src/nile-combobox/group-utils.d.ts +26 -0
- package/dist/src/nile-combobox/group-utils.js +140 -0
- package/dist/src/nile-combobox/group-utils.js.map +1 -0
- package/dist/src/nile-combobox/nile-combobox.css.js +77 -4
- package/dist/src/nile-combobox/nile-combobox.css.js.map +1 -1
- package/dist/src/nile-combobox/nile-combobox.d.ts +33 -0
- package/dist/src/nile-combobox/nile-combobox.js +171 -34
- package/dist/src/nile-combobox/nile-combobox.js.map +1 -1
- package/dist/src/nile-combobox/renderer.d.ts +4 -0
- package/dist/src/nile-combobox/renderer.js +71 -2
- package/dist/src/nile-combobox/renderer.js.map +1 -1
- package/dist/src/nile-combobox/types.d.ts +30 -0
- package/dist/src/nile-combobox/types.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/nile-breadcrumb-item/nile-breadcrumb-item.ts +4 -2
- package/src/nile-combobox/group-utils.ts +157 -0
- package/src/nile-combobox/nile-combobox.css.ts +77 -4
- package/src/nile-combobox/nile-combobox.ts +223 -70
- package/src/nile-combobox/renderer.ts +119 -2
- package/src/nile-combobox/types.ts +36 -0
- package/vscode-html-custom-data.json +6 -1
|
@@ -38,12 +38,14 @@ import type {
|
|
|
38
38
|
ComboboxTagLayout,
|
|
39
39
|
ComboboxSize,
|
|
40
40
|
ComboboxPlacement,
|
|
41
|
+
ComboboxRow,
|
|
41
42
|
NileRemoveEvent,
|
|
42
43
|
} from './types.js';
|
|
43
44
|
import { ComboboxSelectionManager } from './selection-manager.js';
|
|
44
45
|
import { ComboboxSearchManager } from './search-manager.js';
|
|
45
46
|
import { ComboboxRenderer } from './renderer.js';
|
|
46
47
|
import { ComboboxPortalManager } from './portal-manager.js';
|
|
48
|
+
import { hasGroups, flattenRows, filterRows, getOptionRows } from './group-utils.js';
|
|
47
49
|
import { VisibilityManager } from '../utilities/visibility-manager.js';
|
|
48
50
|
|
|
49
51
|
/**
|
|
@@ -149,6 +151,15 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
149
151
|
/** The items displayed after filtering. Renderer reads from this. */
|
|
150
152
|
@state() filteredData: any[] = [];
|
|
151
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Mixed (header + option) row list, only populated when `data` contains
|
|
156
|
+
* group entries (`type: 'group'`). When non-empty, the listbox renders from
|
|
157
|
+
* this instead of `filteredData`. `filteredData` stays in sync as the
|
|
158
|
+
* option-only projection so existing select-all / strict-match / etc. logic
|
|
159
|
+
* keeps working unchanged.
|
|
160
|
+
*/
|
|
161
|
+
@state() private filteredRows: ComboboxRow[] = [];
|
|
162
|
+
|
|
152
163
|
/** The complete unfiltered dataset (preserved for re-filtering). */
|
|
153
164
|
@state() private originalData: any[] = [];
|
|
154
165
|
|
|
@@ -159,6 +170,13 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
159
170
|
@state() private selectAllChecked = false;
|
|
160
171
|
@state() private selectAllIndeterminate = false;
|
|
161
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Index into `filteredRows` of the group header that should be pinned at
|
|
175
|
+
* the top of the (virtualized) listbox right now. -1 means none.
|
|
176
|
+
* Recomputed on scroll.
|
|
177
|
+
*/
|
|
178
|
+
@state() private stickyHeaderIndex = -1;
|
|
179
|
+
|
|
162
180
|
// ── Public properties ──
|
|
163
181
|
|
|
164
182
|
@property() name = '';
|
|
@@ -230,6 +248,14 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
230
248
|
*/
|
|
231
249
|
@property({ type: Boolean, reflect: true, attribute: 'select-all-enabled' }) selectAllEnabled = false;
|
|
232
250
|
|
|
251
|
+
/**
|
|
252
|
+
* When true (default), data-driven group headers stick to the top of the
|
|
253
|
+
* listbox while scrolling through that group's options (Atlassian-style).
|
|
254
|
+
* Works in both plain and virtualized rendering modes. Set to false for
|
|
255
|
+
* inline-only headers that scroll away with their options.
|
|
256
|
+
*/
|
|
257
|
+
@property({ type: Boolean, reflect: true, attribute: 'sticky-group-header' }) stickyGroupHeader = true;
|
|
258
|
+
|
|
233
259
|
@property({ type: Boolean, reflect: true }) portal = false;
|
|
234
260
|
|
|
235
261
|
@property({ type: Boolean }) hoist = false;
|
|
@@ -359,6 +385,48 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
359
385
|
return this.gridRows > 0 && this.gridColumns <= 1;
|
|
360
386
|
}
|
|
361
387
|
|
|
388
|
+
/** True when the source data contains at least one group entry. */
|
|
389
|
+
private get hasGroupedData(): boolean {
|
|
390
|
+
const base = this.originalData.length > 0 ? this.originalData : this.data;
|
|
391
|
+
return hasGroups(base);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Walk filteredRows and find the index of the deepest group header whose
|
|
396
|
+
* virtual position is at or above `scrollTop`. That's the header that
|
|
397
|
+
* should be pinned at the top of the listbox right now.
|
|
398
|
+
*/
|
|
399
|
+
private updateStickyHeader(scrollTop: number): void {
|
|
400
|
+
if (!this.stickyGroupHeader || !this.hasGroupedData) {
|
|
401
|
+
if (this.stickyHeaderIndex !== -1) this.stickyHeaderIndex = -1;
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
let offset = 0;
|
|
405
|
+
let stuck = -1;
|
|
406
|
+
for (let i = 0; i < this.filteredRows.length; i++) {
|
|
407
|
+
const row = this.filteredRows[i];
|
|
408
|
+
const size = row.kind === 'header' ? 32 : 38;
|
|
409
|
+
if (offset > scrollTop) break;
|
|
410
|
+
if (row.kind === 'header') stuck = i;
|
|
411
|
+
offset += size;
|
|
412
|
+
}
|
|
413
|
+
if (stuck !== this.stickyHeaderIndex) this.stickyHeaderIndex = stuck;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Recursively keep only options whose value is in `selectedSet`; drop empty groups. */
|
|
417
|
+
private pruneTreeBySelection(items: any[], selectedSet: Set<string>): any[] {
|
|
418
|
+
const out: any[] = [];
|
|
419
|
+
for (const item of items) {
|
|
420
|
+
if (item && typeof item === 'object' && item.type === 'group' && Array.isArray(item.options)) {
|
|
421
|
+
const kept = this.pruneTreeBySelection(item.options, selectedSet);
|
|
422
|
+
if (kept.length > 0) out.push({ ...item, options: kept });
|
|
423
|
+
} else if (selectedSet.has(String(this.getItemValue(item)))) {
|
|
424
|
+
out.push(item);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return out;
|
|
428
|
+
}
|
|
429
|
+
|
|
362
430
|
private get hasActiveFilter(): boolean {
|
|
363
431
|
return !!this.searchValue || this.showSelectedOnly;
|
|
364
432
|
}
|
|
@@ -378,6 +446,9 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
378
446
|
if (this.gridColumns > 1) {
|
|
379
447
|
return Math.ceil(this.filteredData.length / this.gridColumns);
|
|
380
448
|
}
|
|
449
|
+
if (this.hasGroupedData) {
|
|
450
|
+
return this.filteredRows.length;
|
|
451
|
+
}
|
|
381
452
|
return this.filteredData.length;
|
|
382
453
|
}
|
|
383
454
|
|
|
@@ -400,16 +471,26 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
400
471
|
} else {
|
|
401
472
|
const virtualizer = this.virtualizerCtrl.getVirtualizer();
|
|
402
473
|
const count = this.virtualRowCount;
|
|
403
|
-
|
|
474
|
+
const grouped = this.hasGroupedData;
|
|
475
|
+
const countChanged = virtualizer.options.count !== count;
|
|
476
|
+
const modeChanged = this.lastVirtualizerGrouped !== grouped;
|
|
477
|
+
if (countChanged || modeChanged) {
|
|
478
|
+
const estimateSize = grouped
|
|
479
|
+
? (index: number) => (this.filteredRows[index]?.kind === 'header' ? 32 : 38)
|
|
480
|
+
: () => 38;
|
|
404
481
|
virtualizer.setOptions({
|
|
405
482
|
...virtualizer.options,
|
|
406
483
|
count,
|
|
484
|
+
estimateSize,
|
|
407
485
|
});
|
|
408
486
|
virtualizer.measure();
|
|
487
|
+
this.lastVirtualizerGrouped = grouped;
|
|
409
488
|
}
|
|
410
489
|
}
|
|
411
490
|
}
|
|
412
491
|
|
|
492
|
+
private lastVirtualizerGrouped = false;
|
|
493
|
+
|
|
413
494
|
// ── Data helpers ──
|
|
414
495
|
|
|
415
496
|
private getDisplayText(item: any): string {
|
|
@@ -442,7 +523,10 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
442
523
|
// ── Selection ──
|
|
443
524
|
|
|
444
525
|
private syncSelection(): void {
|
|
445
|
-
const
|
|
526
|
+
const baseData = this.originalData.length > 0 ? this.originalData : this.data;
|
|
527
|
+
const items = this.hasGroupedData
|
|
528
|
+
? getOptionRows(flattenRows(baseData)).map(r => r.item)
|
|
529
|
+
: baseData;
|
|
446
530
|
this.selectedOptions = ComboboxSelectionManager.createOptionsFromValues(
|
|
447
531
|
this.value,
|
|
448
532
|
items,
|
|
@@ -785,19 +869,38 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
785
869
|
|
|
786
870
|
private filterOptions(search: string, preserveScroll = false): void {
|
|
787
871
|
const baseData = this.originalData.length > 0 ? this.originalData : this.data;
|
|
788
|
-
|
|
789
|
-
if (this.
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
872
|
+
|
|
873
|
+
if (this.hasGroupedData) {
|
|
874
|
+
// Grouped path: filter the tree, derive options-only projection.
|
|
875
|
+
let tree = baseData;
|
|
876
|
+
if (this.showSelectedOnly) {
|
|
877
|
+
const selectedValues = Array.isArray(this.value) ? this.value : [this.value];
|
|
878
|
+
const selectedSet = new Set(selectedValues.map(v => String(v)));
|
|
879
|
+
tree = this.pruneTreeBySelection(baseData, selectedSet);
|
|
880
|
+
}
|
|
881
|
+
const { rows } = filterRows(tree, search, this.getSearchText.bind(this));
|
|
882
|
+
this.filteredRows = rows;
|
|
883
|
+
this.filteredData = getOptionRows(rows).map(r => r.item);
|
|
884
|
+
this.showNoResults = this.filteredData.length === 0;
|
|
885
|
+
// Recompute sticky header at current scroll (defaults to top on filter).
|
|
886
|
+
const st = this.scrollElementRef.value?.scrollTop ?? 0;
|
|
887
|
+
this.updateStickyHeader(st);
|
|
888
|
+
} else {
|
|
889
|
+
let source = baseData;
|
|
890
|
+
if (this.showSelectedOnly) {
|
|
891
|
+
const selectedValues = Array.isArray(this.value) ? this.value : [this.value];
|
|
892
|
+
const selectedSet = new Set(selectedValues.map(v => String(v)));
|
|
893
|
+
source = baseData.filter((item: any) => selectedSet.has(String(this.getItemValue(item))));
|
|
894
|
+
}
|
|
895
|
+
const { filteredItems, showNoResults } = this.searchManager.filter(
|
|
896
|
+
search,
|
|
897
|
+
source,
|
|
898
|
+
this.getSearchText.bind(this),
|
|
899
|
+
);
|
|
900
|
+
this.filteredData = filteredItems;
|
|
901
|
+
this.filteredRows = [];
|
|
902
|
+
this.showNoResults = showNoResults;
|
|
793
903
|
}
|
|
794
|
-
const { filteredItems, showNoResults } = this.searchManager.filter(
|
|
795
|
-
search,
|
|
796
|
-
source,
|
|
797
|
-
this.getSearchText.bind(this),
|
|
798
|
-
);
|
|
799
|
-
this.filteredData = filteredItems;
|
|
800
|
-
this.showNoResults = showNoResults;
|
|
801
904
|
|
|
802
905
|
this.portalManager.resetMeasuredHeight();
|
|
803
906
|
if (!preserveScroll) {
|
|
@@ -829,7 +932,10 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
829
932
|
|
|
830
933
|
if (!this.multiple) {
|
|
831
934
|
if (this.strict) {
|
|
832
|
-
const
|
|
935
|
+
const baseData = this.originalData.length > 0 ? this.originalData : this.data;
|
|
936
|
+
const allItems = this.hasGroupedData
|
|
937
|
+
? getOptionRows(flattenRows(baseData)).map(r => r.item)
|
|
938
|
+
: baseData;
|
|
833
939
|
const match = allItems.find(
|
|
834
940
|
(item: any) => this.getDisplayText(item).toLowerCase() === this.searchValue.toLowerCase(),
|
|
835
941
|
);
|
|
@@ -975,28 +1081,14 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
975
1081
|
if (this.selectedOptions.length === 0) return;
|
|
976
1082
|
|
|
977
1083
|
this.showSelectedOnly = !this.showSelectedOnly;
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
this.searchValue = '';
|
|
981
|
-
const selectedValues = Array.isArray(this.value) ? this.value : [this.value];
|
|
982
|
-
this.filteredData = this.originalData.filter((item: any) => {
|
|
983
|
-
const iv = this.getItemValue(item);
|
|
984
|
-
return selectedValues.some(val => String(val) === String(iv));
|
|
985
|
-
});
|
|
986
|
-
} else {
|
|
987
|
-
this.filteredData = [...this.originalData];
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
this.portalManager.resetMeasuredHeight();
|
|
991
|
-
this.resetScrollPosition();
|
|
992
|
-
this.updateSelectAllState();
|
|
993
|
-
this.requestUpdate();
|
|
1084
|
+
if (this.showSelectedOnly) this.searchValue = '';
|
|
1085
|
+
this.filterOptions(this.searchValue);
|
|
994
1086
|
}
|
|
995
1087
|
|
|
996
1088
|
private clearAll(): void {
|
|
997
1089
|
this.showSelectedOnly = false;
|
|
998
1090
|
this.value = this.multiple ? [] : '';
|
|
999
|
-
this.
|
|
1091
|
+
this.filterOptions('');
|
|
1000
1092
|
this.syncSelection();
|
|
1001
1093
|
this.emit('nile-change', { value: this.value, name: this.name });
|
|
1002
1094
|
this.emit('nile-clear', { value: this.value, name: this.name });
|
|
@@ -1006,8 +1098,9 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
1006
1098
|
// ── Scroll ──
|
|
1007
1099
|
|
|
1008
1100
|
private onScroll(e: Event): void {
|
|
1009
|
-
if (this.showSelectedOnly) return;
|
|
1010
1101
|
const target = e.target as HTMLElement;
|
|
1102
|
+
this.updateStickyHeader(target.scrollTop);
|
|
1103
|
+
if (this.showSelectedOnly) return;
|
|
1011
1104
|
|
|
1012
1105
|
this.emit('nile-scroll', { scrollTop: target.scrollTop, scrollLeft: target.scrollLeft, name: this.name });
|
|
1013
1106
|
|
|
@@ -1067,8 +1160,13 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
1067
1160
|
if (this.originalData.length === 0 && this.data.length > 0) {
|
|
1068
1161
|
this.originalData = [...this.data];
|
|
1069
1162
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1163
|
+
if (this.hasGroupedData) {
|
|
1164
|
+
this.filterOptions(this.searchValue);
|
|
1165
|
+
} else {
|
|
1166
|
+
this.filteredData = [...(this.originalData.length > 0 ? this.originalData : this.data)];
|
|
1167
|
+
this.filteredRows = [];
|
|
1168
|
+
this.showNoResults = this.filteredData.length === 0;
|
|
1169
|
+
}
|
|
1072
1170
|
|
|
1073
1171
|
await stopAnimations(this);
|
|
1074
1172
|
|
|
@@ -1137,7 +1235,12 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
1137
1235
|
if (this.data.length > 0 && !this.showSelectedOnly) {
|
|
1138
1236
|
this.originalData = [...this.data];
|
|
1139
1237
|
}
|
|
1140
|
-
this.
|
|
1238
|
+
if (this.hasGroupedData) {
|
|
1239
|
+
this.filterOptions(this.searchValue);
|
|
1240
|
+
} else {
|
|
1241
|
+
this.filteredData = [...this.data];
|
|
1242
|
+
this.filteredRows = [];
|
|
1243
|
+
}
|
|
1141
1244
|
this.syncSelection();
|
|
1142
1245
|
this.updateSelectAllState();
|
|
1143
1246
|
|
|
@@ -1404,6 +1507,19 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
1404
1507
|
`;
|
|
1405
1508
|
}
|
|
1406
1509
|
|
|
1510
|
+
private renderStickyHeaderOverlay(grouped: boolean, useVirtual: boolean): TemplateResult {
|
|
1511
|
+
if (!this.stickyGroupHeader || !grouped || !useVirtual) return html``;
|
|
1512
|
+
const idx = this.stickyHeaderIndex;
|
|
1513
|
+
if (idx < 0 || idx >= this.filteredRows.length) return html``;
|
|
1514
|
+
const row = this.filteredRows[idx];
|
|
1515
|
+
if (row.kind !== 'header') return html``;
|
|
1516
|
+
return html`
|
|
1517
|
+
<div class="combobox__group-sticky-overlay">
|
|
1518
|
+
${ComboboxRenderer.renderGroupHeader(row)}
|
|
1519
|
+
</div>
|
|
1520
|
+
`;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1407
1523
|
private renderListbox(): TemplateResult {
|
|
1408
1524
|
const showAddOption = this.allowCustomValue
|
|
1409
1525
|
&& this.searchValue.trim()
|
|
@@ -1421,7 +1537,10 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
1421
1537
|
}
|
|
1422
1538
|
|
|
1423
1539
|
const isGrid = this.gridColumns > 1 || this.isBidirectionalGrid;
|
|
1424
|
-
const
|
|
1540
|
+
const grouped = this.hasGroupedData && !isGrid;
|
|
1541
|
+
const useVirtual = grouped
|
|
1542
|
+
? this.filteredRows.length >= 5
|
|
1543
|
+
: ComboboxRenderer.shouldUseVirtualizer(this.filteredData, this.gridColumns);
|
|
1425
1544
|
const virtualizer = this.virtualizerCtrl.getVirtualizer();
|
|
1426
1545
|
const virtualItems = (useVirtual || isGrid) ? virtualizer.getVirtualItems() : [];
|
|
1427
1546
|
const totalSize = (useVirtual || isGrid) ? virtualizer.getTotalSize() : 0;
|
|
@@ -1443,6 +1562,7 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
1443
1562
|
>
|
|
1444
1563
|
${this.renderLoader()}
|
|
1445
1564
|
${this.renderSelectAll()}
|
|
1565
|
+
${this.renderStickyHeaderOverlay(grouped, useVirtual)}
|
|
1446
1566
|
${this.showNoResults && !this.optionsLoading && !this.loading
|
|
1447
1567
|
? this.renderEmptyState()
|
|
1448
1568
|
: isGrid
|
|
@@ -1462,39 +1582,72 @@ export class NileCombobox extends NileElement implements NileFormControl {
|
|
|
1462
1582
|
this.renderItemConfig?.getSuffix ? this.getItemSuffix.bind(this) : undefined,
|
|
1463
1583
|
this.isBidirectionalGrid ? this.gridColumnWidth : undefined,
|
|
1464
1584
|
)
|
|
1465
|
-
:
|
|
1466
|
-
?
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1585
|
+
: grouped
|
|
1586
|
+
? (useVirtual
|
|
1587
|
+
? ComboboxRenderer.renderRowsVirtualized(
|
|
1588
|
+
virtualItems,
|
|
1589
|
+
totalSize,
|
|
1590
|
+
this.filteredRows,
|
|
1591
|
+
this.value,
|
|
1592
|
+
this.multiple,
|
|
1593
|
+
this.getDisplayText.bind(this),
|
|
1594
|
+
this.getItemValue.bind(this),
|
|
1595
|
+
this.optionsLoading || this.loading,
|
|
1596
|
+
this.allowHtmlLabel,
|
|
1597
|
+
this.renderItemConfig?.getDescription ? this.getItemDescription.bind(this) : undefined,
|
|
1598
|
+
this.renderItemConfig?.getPrefix ? this.getItemPrefix.bind(this) : undefined,
|
|
1599
|
+
this.renderItemConfig?.getSuffix ? this.getItemSuffix.bind(this) : undefined,
|
|
1600
|
+
)
|
|
1601
|
+
: ComboboxRenderer.renderRowsPlain(
|
|
1602
|
+
this.filteredRows,
|
|
1603
|
+
this.value,
|
|
1604
|
+
this.multiple,
|
|
1605
|
+
this.getDisplayText.bind(this),
|
|
1606
|
+
this.getItemValue.bind(this),
|
|
1607
|
+
this.showNoResults,
|
|
1608
|
+
this.noResultsMessage,
|
|
1609
|
+
this.optionsLoading || this.loading,
|
|
1610
|
+
this.onScroll.bind(this),
|
|
1611
|
+
this.allowHtmlLabel,
|
|
1612
|
+
this.renderItemConfig?.getDescription ? this.getItemDescription.bind(this) : undefined,
|
|
1613
|
+
this.renderItemConfig?.getPrefix ? this.getItemPrefix.bind(this) : undefined,
|
|
1614
|
+
this.renderItemConfig?.getSuffix ? this.getItemSuffix.bind(this) : undefined,
|
|
1615
|
+
undefined,
|
|
1616
|
+
this.hasActiveFilter ? this.noResultsSubtitle : undefined,
|
|
1617
|
+
))
|
|
1618
|
+
: useVirtual
|
|
1619
|
+
? ComboboxRenderer.renderVirtualizedOptions(
|
|
1620
|
+
virtualItems,
|
|
1621
|
+
totalSize,
|
|
1622
|
+
this.filteredData,
|
|
1623
|
+
this.value,
|
|
1624
|
+
this.multiple,
|
|
1625
|
+
this.getDisplayText.bind(this),
|
|
1626
|
+
this.getItemValue.bind(this),
|
|
1627
|
+
this.optionsLoading || this.loading,
|
|
1628
|
+
this.allowHtmlLabel,
|
|
1629
|
+
virtualizer.measureElement,
|
|
1630
|
+
this.renderItemConfig?.getDescription ? this.getItemDescription.bind(this) : undefined,
|
|
1631
|
+
this.renderItemConfig?.getPrefix ? this.getItemPrefix.bind(this) : undefined,
|
|
1632
|
+
this.renderItemConfig?.getSuffix ? this.getItemSuffix.bind(this) : undefined,
|
|
1633
|
+
)
|
|
1634
|
+
: ComboboxRenderer.renderPlainOptions(
|
|
1635
|
+
this.filteredData,
|
|
1636
|
+
this.value,
|
|
1637
|
+
this.multiple,
|
|
1638
|
+
this.getDisplayText.bind(this),
|
|
1639
|
+
this.getItemValue.bind(this),
|
|
1640
|
+
this.showNoResults,
|
|
1641
|
+
this.noResultsMessage,
|
|
1642
|
+
this.optionsLoading || this.loading,
|
|
1643
|
+
this.onScroll.bind(this),
|
|
1644
|
+
this.allowHtmlLabel,
|
|
1645
|
+
this.renderItemConfig?.getDescription ? this.getItemDescription.bind(this) : undefined,
|
|
1646
|
+
this.renderItemConfig?.getPrefix ? this.getItemPrefix.bind(this) : undefined,
|
|
1647
|
+
this.renderItemConfig?.getSuffix ? this.getItemSuffix.bind(this) : undefined,
|
|
1648
|
+
undefined,
|
|
1649
|
+
this.hasActiveFilter ? this.noResultsSubtitle : undefined,
|
|
1650
|
+
)
|
|
1498
1651
|
}
|
|
1499
1652
|
${showAddOption ? html`
|
|
1500
1653
|
<div @mouseup=${(e: MouseEvent) => { e.stopPropagation(); this.addCustomValue(this.searchValue.trim()); }}>
|
|
@@ -10,9 +10,127 @@ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
|
10
10
|
import { classMap } from 'lit/directives/class-map.js';
|
|
11
11
|
import { repeat } from 'lit/directives/repeat.js';
|
|
12
12
|
import type { VirtualItem } from '@tanstack/virtual-core';
|
|
13
|
+
import type { ComboboxRow, ComboboxHeaderRow } from './types';
|
|
13
14
|
|
|
14
15
|
export class ComboboxRenderer {
|
|
15
16
|
|
|
17
|
+
static renderGroupHeader(row: ComboboxHeaderRow): TemplateResult {
|
|
18
|
+
return html`
|
|
19
|
+
<div
|
|
20
|
+
part="group-header"
|
|
21
|
+
class="combobox__group-header"
|
|
22
|
+
role="presentation"
|
|
23
|
+
data-group-id=${row.id}
|
|
24
|
+
style=${`--group-depth:${row.depth}`}
|
|
25
|
+
>
|
|
26
|
+
${row.prefix
|
|
27
|
+
? html`<nile-icon
|
|
28
|
+
class="combobox__group-prefix"
|
|
29
|
+
name=${row.prefix}
|
|
30
|
+
size="14"
|
|
31
|
+
method="fill"
|
|
32
|
+
></nile-icon>`
|
|
33
|
+
: ''}
|
|
34
|
+
<span class="combobox__group-label">${row.label}</span>
|
|
35
|
+
</div>
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static renderRowsPlain(
|
|
40
|
+
rows: ComboboxRow[],
|
|
41
|
+
value: string | string[],
|
|
42
|
+
multiple: boolean,
|
|
43
|
+
getDisplayText: (item: any) => string,
|
|
44
|
+
getItemValue: (item: any) => string,
|
|
45
|
+
showNoResults: boolean,
|
|
46
|
+
noResultsMessage: string,
|
|
47
|
+
isLoading: boolean,
|
|
48
|
+
onScroll: (e: Event) => void,
|
|
49
|
+
allowHtmlLabel: boolean,
|
|
50
|
+
getItemDescription?: (item: any) => string,
|
|
51
|
+
getItemPrefix?: (item: any) => string,
|
|
52
|
+
getItemSuffix?: (item: any) => string,
|
|
53
|
+
enableDescription?: boolean,
|
|
54
|
+
noResultsSubtitle?: string,
|
|
55
|
+
): TemplateResult {
|
|
56
|
+
if (showNoResults && !isLoading && rows.length === 0) {
|
|
57
|
+
return ComboboxRenderer.renderNoResults(noResultsMessage, noResultsSubtitle);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return html`
|
|
61
|
+
<div
|
|
62
|
+
part="select-options"
|
|
63
|
+
class="combobox__options ${isLoading ? 'loading' : ''}"
|
|
64
|
+
>
|
|
65
|
+
<div class="combobox__options-plain" @scroll=${onScroll}>
|
|
66
|
+
${rows.map((row) =>
|
|
67
|
+
row.kind === 'header'
|
|
68
|
+
? ComboboxRenderer.renderGroupHeader(row)
|
|
69
|
+
: ComboboxRenderer.renderItem(
|
|
70
|
+
row.item, value, multiple, getDisplayText, getItemValue,
|
|
71
|
+
allowHtmlLabel, getItemDescription, getItemPrefix,
|
|
72
|
+
getItemSuffix, enableDescription,
|
|
73
|
+
),
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static renderRowsVirtualized(
|
|
81
|
+
virtualItems: VirtualItem[],
|
|
82
|
+
totalSize: number,
|
|
83
|
+
rows: ComboboxRow[],
|
|
84
|
+
value: string | string[],
|
|
85
|
+
multiple: boolean,
|
|
86
|
+
getDisplayText: (item: any) => string,
|
|
87
|
+
getItemValue: (item: any) => string,
|
|
88
|
+
isLoading: boolean,
|
|
89
|
+
allowHtmlLabel: boolean,
|
|
90
|
+
getItemDescription?: (item: any) => string,
|
|
91
|
+
getItemPrefix?: (item: any) => string,
|
|
92
|
+
getItemSuffix?: (item: any) => string,
|
|
93
|
+
enableDescription?: boolean,
|
|
94
|
+
): TemplateResult {
|
|
95
|
+
return html`
|
|
96
|
+
<div
|
|
97
|
+
part="select-options"
|
|
98
|
+
class="combobox__options ${isLoading ? 'loading' : ''}"
|
|
99
|
+
>
|
|
100
|
+
<div style="position:relative;height:${totalSize}px;width:100%;">
|
|
101
|
+
${repeat(
|
|
102
|
+
virtualItems,
|
|
103
|
+
(vItem) => vItem.key,
|
|
104
|
+
(vItem) => {
|
|
105
|
+
const row = rows[vItem.index];
|
|
106
|
+
if (!row) return html``;
|
|
107
|
+
const posStyle =
|
|
108
|
+
`position:absolute;top:0;left:0;right:0;` +
|
|
109
|
+
`transform:translateY(${vItem.start}px);` +
|
|
110
|
+
`height:${vItem.size}px;`;
|
|
111
|
+
if (row.kind === 'header') {
|
|
112
|
+
return html`
|
|
113
|
+
<div style=${posStyle} class="combobox__group-header-slot">
|
|
114
|
+
${ComboboxRenderer.renderGroupHeader(row)}
|
|
115
|
+
</div>
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
return html`
|
|
119
|
+
<div style=${posStyle}>
|
|
120
|
+
${ComboboxRenderer.renderItem(
|
|
121
|
+
row.item, value, multiple, getDisplayText, getItemValue,
|
|
122
|
+
allowHtmlLabel, getItemDescription, getItemPrefix,
|
|
123
|
+
getItemSuffix, enableDescription,
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
`;
|
|
127
|
+
},
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
|
|
16
134
|
static renderVirtualizedOptions(
|
|
17
135
|
virtualItems: VirtualItem[],
|
|
18
136
|
totalSize: number,
|
|
@@ -45,7 +163,7 @@ export class ComboboxRenderer {
|
|
|
45
163
|
const item = data[vItem.index];
|
|
46
164
|
return ComboboxRenderer.renderMeasuredItem(
|
|
47
165
|
item, vItem.index, value, multiple, getDisplayText, getItemValue,
|
|
48
|
-
allowHtmlLabel,
|
|
166
|
+
allowHtmlLabel, getItemDescription, getItemPrefix,
|
|
49
167
|
getItemSuffix, enableDescription,
|
|
50
168
|
);
|
|
51
169
|
},
|
|
@@ -130,7 +248,6 @@ export class ComboboxRenderer {
|
|
|
130
248
|
getDisplayText: (item: any) => string,
|
|
131
249
|
getItemValue: (item: any) => string,
|
|
132
250
|
allowHtmlLabel: boolean,
|
|
133
|
-
measureElement: (el: Element | null) => void,
|
|
134
251
|
getItemDescription?: (item: any) => string,
|
|
135
252
|
getItemPrefix?: (item: any) => string,
|
|
136
253
|
getItemSuffix?: (item: any) => string,
|
|
@@ -25,3 +25,39 @@ export interface ComboboxRenderItemConfig {
|
|
|
25
25
|
export type ComboboxTagLayout = 'single-line' | 'wrap' | 'fixed-height';
|
|
26
26
|
export type ComboboxSize = 'small' | 'medium' | 'large';
|
|
27
27
|
export type ComboboxPlacement = 'top' | 'bottom';
|
|
28
|
+
|
|
29
|
+
export interface ComboboxGroupItem {
|
|
30
|
+
type: 'group';
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
prefix?: string;
|
|
34
|
+
options: ComboboxDataItem[];
|
|
35
|
+
collapsible?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ComboboxOptionItem {
|
|
39
|
+
type?: 'option';
|
|
40
|
+
value: string;
|
|
41
|
+
label?: string;
|
|
42
|
+
[key: string]: any;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ComboboxDataItem = ComboboxGroupItem | ComboboxOptionItem;
|
|
46
|
+
|
|
47
|
+
export interface ComboboxHeaderRow {
|
|
48
|
+
kind: 'header';
|
|
49
|
+
id: string;
|
|
50
|
+
label: string;
|
|
51
|
+
prefix?: string;
|
|
52
|
+
depth: number;
|
|
53
|
+
optionCount: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ComboboxOptionRow {
|
|
57
|
+
kind: 'option';
|
|
58
|
+
item: ComboboxOptionItem;
|
|
59
|
+
depth: number;
|
|
60
|
+
parentIds: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type ComboboxRow = ComboboxHeaderRow | ComboboxOptionRow;
|