-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Use infinite scroll select #11991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use infinite scroll select #11991
Changes from 4 commits
10b60c2
c7d282c
f317e9c
99dca08
4fc90c1
4676fb1
b069057
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,8 +41,10 @@ | |
| - optionValueKey (String, optional): Property to use as the value for options (e.g., 'name'). Default is 'id' | ||
| - optionLabelKey (String, optional): Property to use as the label for options (e.g., 'name'). Default is 'name' | ||
| - defaultOption (Object, optional): Preselected object to include initially | ||
| - allowClear (Boolean, optional): Whether to allow clearing the selection. Default is false | ||
| - showIcon (Boolean, optional): Whether to show icon for the options. Default is true | ||
| - defaultIcon (String, optional): Icon to be shown when there is no resource icon for the option. Default is 'cloud-outlined' | ||
| - selectFirstOption (Boolean, optional): Whether to automatically select the first option when options are loaded. Default is false | ||
|
|
||
| Events: | ||
| - @change-option-value (Function): Emits the selected option value(s) when value(s) changes. Do not use @change as it will give warnings and may not work | ||
|
|
@@ -58,6 +60,7 @@ | |
| :filter-option="false" | ||
| :loading="loading" | ||
| show-search | ||
| :allowClear="allowClear" | ||
| placeholder="Select" | ||
| @search="onSearchTimed" | ||
| @popupScroll="onScroll" | ||
|
|
@@ -75,9 +78,9 @@ | |
| </div> | ||
| </div> | ||
| </template> | ||
| <a-select-option v-for="option in options" :key="option.id" :value="option[optionValueKey]"> | ||
| <a-select-option v-for="option in selectableOptions" :key="option.id" :value="option[optionValueKey]"> | ||
| <span> | ||
| <span v-if="showIcon"> | ||
| <span v-if="showIcon && option.id !== null && option.id !== undefined"> | ||
| <resource-icon v-if="option.icon && option.icon.base64image" :image="option.icon.base64image" size="1x" style="margin-right: 5px"/> | ||
| <render-icon v-else :icon="defaultIcon" style="margin-right: 5px" /> | ||
| </span> | ||
|
|
@@ -124,6 +127,10 @@ export default { | |
| type: Object, | ||
| default: null | ||
| }, | ||
| allowClear: { | ||
| type: Boolean, | ||
| default: false | ||
| }, | ||
| showIcon: { | ||
| type: Boolean, | ||
| default: true | ||
|
|
@@ -135,6 +142,10 @@ export default { | |
| pageSize: { | ||
| type: Number, | ||
| default: null | ||
| }, | ||
| selectFirstOption: { | ||
| type: Boolean, | ||
| default: false | ||
| } | ||
| }, | ||
| data () { | ||
|
|
@@ -147,7 +158,8 @@ export default { | |
| searchTimer: null, | ||
| scrollHandlerAttached: false, | ||
| preselectedOptionValue: null, | ||
| successiveFetches: 0 | ||
| successiveFetches: 0, | ||
| hasAutoSelectedFirst: false | ||
| } | ||
| }, | ||
| created () { | ||
|
|
@@ -166,6 +178,19 @@ export default { | |
| }, | ||
| formattedSearchFooterMessage () { | ||
| return `${this.$t('label.showing.results.for').replace('%x', this.searchQuery)}` | ||
| }, | ||
| selectableOptions () { | ||
| const currentValue = this.$attrs.value | ||
| // Only filter out null/empty options when the current value is also null/undefined/empty | ||
| // This prevents such options from being selected and allows the placeholder to show instead | ||
| if (currentValue === null || currentValue === undefined || currentValue === '') { | ||
| return this.options.filter(option => { | ||
| const optionValue = option[this.optionValueKey] | ||
| return optionValue !== null && optionValue !== undefined && optionValue !== '' | ||
| }) | ||
| } | ||
| // When a valid value is selected, show all options | ||
| return this.options | ||
|
Comment on lines
+182
to
+193
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the benefit of this differentiation? Maybe we can show the null/empty option always or is it beneficial when using the value from route?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added this to maintain consistency with the existing behavior in UI. |
||
| } | ||
| }, | ||
| watch: { | ||
|
|
@@ -210,6 +235,7 @@ export default { | |
| }).finally(() => { | ||
| if (this.successiveFetches === 0) { | ||
| this.loading = false | ||
| this.autoSelectFirstOptionIfNeeded() | ||
| } | ||
| }) | ||
| }, | ||
|
|
@@ -224,7 +250,9 @@ export default { | |
| const match = this.options.find(entry => entry[this.optionValueKey] === matchValue) | ||
| if (!match) { | ||
| this.successiveFetches++ | ||
| if (this.options.length < this.totalCount) { | ||
| // Exclude defaultOption from count when comparing with totalCount | ||
| const apiOptionsCount = this.getApiOptionsCount() | ||
| if (apiOptionsCount < this.totalCount) { | ||
| this.fetchItems() | ||
| } else { | ||
| this.resetPreselectedOptionValue() | ||
|
|
@@ -246,6 +274,44 @@ export default { | |
| this.preselectedOptionValue = null | ||
| this.successiveFetches = 0 | ||
| }, | ||
| getApiOptionsCount () { | ||
| // Return count of options excluding the locally added defaultOption | ||
| if (this.defaultOption) { | ||
| const defaultOptionValue = this.defaultOption[this.optionValueKey] | ||
| return this.options.filter(option => option[this.optionValueKey] !== defaultOptionValue).length | ||
| } | ||
| return this.options.length | ||
| }, | ||
|
DaanHoogland marked this conversation as resolved.
Outdated
|
||
| autoSelectFirstOptionIfNeeded () { | ||
| if (!this.selectFirstOption || this.hasAutoSelectedFirst) { | ||
| return | ||
| } | ||
| // Don't auto-select if there's a preselected value being fetched | ||
| if (this.preselectedOptionValue) { | ||
| return | ||
| } | ||
| const currentValue = this.$attrs.value | ||
| if (currentValue !== undefined && currentValue !== null && currentValue !== '') { | ||
| return | ||
| } | ||
| if (this.options.length === 0) { | ||
| return | ||
| } | ||
| if (this.searchQuery && this.searchQuery.length > 0) { | ||
| return | ||
| } | ||
| // Only auto-select after initial load is complete (no more successive fetches) | ||
| if (this.successiveFetches > 0) { | ||
| return | ||
| } | ||
| const firstOption = this.options[0] | ||
| if (firstOption) { | ||
| const firstValue = firstOption[this.optionValueKey] | ||
| this.hasAutoSelectedFirst = true | ||
| this.$emit('change-option-value', firstValue) | ||
| this.$emit('change-option', firstOption) | ||
| } | ||
| }, | ||
| onSearchTimed (value) { | ||
| clearTimeout(this.searchTimer) | ||
| this.searchTimer = setTimeout(() => { | ||
|
|
@@ -264,7 +330,9 @@ export default { | |
| }, | ||
| onScroll (e) { | ||
| const nearBottom = e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - 10 | ||
| const hasMore = this.options.length < this.totalCount | ||
| // Exclude defaultOption from count when comparing with totalCount | ||
| const apiOptionsCount = this.getApiOptionsCount() | ||
| const hasMore = apiOptionsCount < this.totalCount | ||
| if (nearBottom && hasMore && !this.loading) { | ||
| this.fetchItems() | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.