+
-
+
-
+
-
-
-
- No results found
-
-
+
+ No results found
+
+
+
+
+
+ 0">{{ validSelectionCount }} item(s) selected
+ {{ placeholder }}
diff --git a/libs/openchallenges/ui/src/lib/search-dropdown-filter/search-dropdown-filter.component.scss b/libs/openchallenges/ui/src/lib/search-dropdown-filter/search-dropdown-filter.component.scss
index 8b9cd88dce..341112fdc8 100644
--- a/libs/openchallenges/ui/src/lib/search-dropdown-filter/search-dropdown-filter.component.scss
+++ b/libs/openchallenges/ui/src/lib/search-dropdown-filter/search-dropdown-filter.component.scss
@@ -72,13 +72,13 @@
}
}
-.loader-with-icon-container {
+.loader-with-avatar-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
- .skeleton-with-icon {
+ .skeleton-with-avatar {
display: flex;
align-items: center;
margin-top: 6px;
diff --git a/libs/openchallenges/ui/src/lib/search-dropdown-filter/search-dropdown-filter.component.ts b/libs/openchallenges/ui/src/lib/search-dropdown-filter/search-dropdown-filter.component.ts
index 48831eeb82..40eb043aec 100644
--- a/libs/openchallenges/ui/src/lib/search-dropdown-filter/search-dropdown-filter.component.ts
+++ b/libs/openchallenges/ui/src/lib/search-dropdown-filter/search-dropdown-filter.component.ts
@@ -9,6 +9,7 @@ import {
MultiSelectModule,
} from 'primeng/multiselect';
import { SkeletonModule } from 'primeng/skeleton';
+import { ScrollerOptions } from 'primeng/api';
@Component({
selector: 'openchallenges-search-dropdown-filter',
@@ -25,32 +26,41 @@ import { SkeletonModule } from 'primeng/skeleton';
})
export class SearchDropdownFilterComponent implements OnInit {
@Input({ required: true }) options!: Filter[];
+ @Input({ required: true }) optionsPerPage!: number;
@Input({ required: true }) selectedOptions!: any[];
@Input({ required: true }) placeholder = 'Search items';
@Input({ required: true }) showAvatar!: boolean | undefined;
@Input({ required: true }) filterByApiClient!: boolean | undefined;
@Input({ required: false }) lazy = true;
+ @Input({ required: false }) showLoader = false;
+ @Input({ required: false }) optionHeight = 50; // height of each option
+ @Input({ required: false }) optionSize = 10; // total number of displaying options
@Output() selectionChange = new EventEmitter
();
@Output() searchChange = new EventEmitter();
- @Output() lazyLoad = new EventEmitter();
+ @Output() pageChange = new EventEmitter();
overlayOptions = {
showTransitionOptions: '0ms',
hideTransitionOptions: '0ms',
};
- scrollOptions = {
- delay: 250,
- showLoader: true,
- };
-
searchTerm = '';
filter = true;
isLoading = false;
+ loadedPages = new Set();
+ delays = 400;
+ scrollerOptions!: ScrollerOptions;
ngOnInit(): void {
+ this.scrollerOptions = {
+ delay: this.showLoader ? this.delays : 0, // if no loader is applied, load data seamlessly
+ showLoader: this.showLoader,
+ step: this.optionsPerPage,
+ scrollHeight: `${this.optionHeight * this.optionSize + 12}px`, // dynamically set scroller height
+ };
+
this.showAvatar = this.showAvatar ? this.showAvatar : false;
if (this.filterByApiClient) {
@@ -60,14 +70,40 @@ export class SearchDropdownFilterComponent implements OnInit {
}
}
+ get validSelectionCount(): number {
+ if (!this.options) {
+ return 0;
+ }
+
+ // preparing a set for quick lookup
+ const validOptionValues = new Set(
+ this.options.map((option) => option.value)
+ );
+
+ // count how many selected values
+ // exlude the invalid selected values if they are not in the option list
+ return this.selectedOptions.filter(
+ (option) =>
+ option !== null && option !== undefined && validOptionValues.has(option)
+ ).length;
+ }
+
onLazyLoad(event: MultiSelectLazyLoadEvent) {
// note: virtual scroll needs to be set 'true' to enable lazy load
// trigger loader animation every time lazy load initiated
- this.isLoading = true;
- setTimeout(() => {
- this.isLoading = false;
- }, 250);
- this.lazyLoad.emit(event);
+ const startPage = Math.max(
+ 0,
+ Math.floor(event.first / this.optionsPerPage)
+ ); // avoid negative pages
+ const endPage = Math.floor(event.last / this.optionsPerPage);
+
+ // load next page as scrolling down
+ for (let page = startPage; page <= endPage; page++) {
+ if (!this.loadedPages.has(page)) {
+ this.loadedPages.add(page);
+ this.pageChange.emit(page);
+ }
+ }
}
onSearch(event: any): void {
@@ -75,6 +111,10 @@ export class SearchDropdownFilterComponent implements OnInit {
}
onCustomSearch(): void {
+ if (this.lazy) {
+ this.loadedPages.clear();
+ this.triggerLoading();
+ }
this.searchChange.emit(this.searchTerm);
}
@@ -83,6 +123,13 @@ export class SearchDropdownFilterComponent implements OnInit {
this.selectionChange.emit(selected);
}
+ triggerLoading(): void {
+ this.isLoading = true;
+ setTimeout(() => {
+ this.isLoading = false;
+ }, this.delays);
+ }
+
getAvatar(option: Filter): Avatar {
return {
name: option.label ?? '',