diff --git a/src/components/dropdown/Dropdown.vue b/src/components/dropdown/Dropdown.vue index 678efd727..3f40c029e 100755 --- a/src/components/dropdown/Dropdown.vue +++ b/src/components/dropdown/Dropdown.vue @@ -706,6 +706,10 @@ input.p-dropdown-label { overflow: hidden; } +.p-dropdown-item-group { + cursor: auto; +} + .p-dropdown-items { margin: 0; padding: 0; diff --git a/src/components/listbox/Listbox.vue b/src/components/listbox/Listbox.vue index f6193b046..f736987a3 100755 --- a/src/components/listbox/Listbox.vue +++ b/src/components/listbox/Listbox.vue @@ -326,6 +326,10 @@ export default { overflow: hidden; } +.p-listbox-item-group { + cursor: auto; +} + .p-listbox-filter-container { position: relative; } diff --git a/src/components/multiselect/MultiSelect.d.ts b/src/components/multiselect/MultiSelect.d.ts index a2ad524cb..55b293c04 100755 --- a/src/components/multiselect/MultiSelect.d.ts +++ b/src/components/multiselect/MultiSelect.d.ts @@ -6,18 +6,23 @@ interface MultiSelectProps { optionLabel?: string; optionValue?: any; optionDisabled?: boolean; + optionGroupLabel?: string; + optionGroupChildren?: string; scrollHeight?: string; placeholder?: string; disabled?: boolean; - filter?: boolean; tabindex?: string; inputId?: string; dataKey?: string; + filter?: boolean; filterPlaceholder?: string; filterLocale?: string; + filterMatchMode?: string; + filterFields?: string[]; ariaLabelledBy?: string; appendTo?: string; emptyFilterMessage?: string; + emptyMessage?: string; display?: string; } diff --git a/src/components/multiselect/MultiSelect.vue b/src/components/multiselect/MultiSelect.vue index 51f494a6e..4820b4cd7 100755 --- a/src/components/multiselect/MultiSelect.vue +++ b/src/components/multiselect/MultiSelect.vue @@ -25,6 +25,7 @@
+
@@ -67,6 +94,7 @@ import {ConnectedOverlayScrollHandler} from 'primevue/utils'; import {ObjectUtils} from 'primevue/utils'; import {DomHandler} from 'primevue/utils'; +import {FilterService} from 'primevue/api'; import Ripple from 'primevue/ripple'; export default { @@ -77,18 +105,28 @@ export default { optionLabel: null, optionValue: null, optionDisabled: null, + optionGroupLabel: null, + optionGroupChildren: null, scrollHeight: { type: String, default: '200px' }, placeholder: String, disabled: Boolean, - filter: Boolean, tabindex: String, inputId: String, dataKey: null, + filter: Boolean, filterPlaceholder: String, filterLocale: String, + filterMatchMode: { + type: String, + default: 'contains' + }, + filterFields: { + type: Array, + default: null + }, ariaLabelledBy: null, appendTo: { type: String, @@ -96,7 +134,11 @@ export default { }, emptyFilterMessage: { type: String, - default: 'No results found' + default: null + }, + emptyMessage: { + type: String, + default: null }, display: { type: String, @@ -136,6 +178,15 @@ export default { getOptionRenderKey(option) { return this.dataKey ? ObjectUtils.resolveFieldData(option, this.dataKey) : this.getOptionLabel(option); }, + getOptionGroupRenderKey(optionGroup) { + return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupLabel); + }, + getOptionGroupLabel(optionGroup) { + return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupLabel); + }, + getOptionGroupChildren(optionGroup) { + return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupChildren); + }, isOptionDisabled(option) { return this.optionDisabled ? ObjectUtils.resolveFieldData(option, this.optionDisabled) : false; }, @@ -277,7 +328,7 @@ export default { let nextItem = item.nextElementSibling; if (nextItem) - return DomHandler.hasClass(nextItem, 'p-disabled') ? this.findNextItem(nextItem) : nextItem; + return DomHandler.hasClass(nextItem, 'p-disabled') || DomHandler.hasClass(nextItem, 'p-multiselect-item-group') ? this.findNextItem(nextItem) : nextItem; else return null; }, @@ -285,7 +336,7 @@ export default { let prevItem = item.previousElementSibling; if (prevItem) - return DomHandler.hasClass(prevItem, 'p-disabled') ? this.findPrevItem(prevItem) : prevItem; + return DomHandler.hasClass(prevItem, 'p-disabled') || DomHandler.hasClass(prevItem, 'p-multiselect-item-group') ? this.findPrevItem(prevItem) : prevItem; else return null; }, @@ -366,23 +417,49 @@ export default { return !(this.$el.isSameNode(event.target) || this.$el.contains(event.target) || (this.overlay && this.overlay.contains(event.target))); }, getLabelByValue(val) { - let label = null; - + let option; if (this.options) { - for (let option of this.options) { - let optionValue = this.getOptionValue(option); - - if(ObjectUtils.equals(optionValue, val, this.equalityKey)) { - label = this.getOptionLabel(option); - break; + if (this.optionGroupLabel) { + for (let optionGroup of this.options) { + option = this.findOptionByValue(val, this.getOptionGroupChildren(optionGroup)); + if (option) { + break; + } } } + else { + option = this.findOptionByValue(val, this.options); + } } - return label; + return option ? this.getOptionLabel(option): null; + }, + findOptionByValue(val, list) { + for (let option of list) { + let optionValue = this.getOptionValue(option); + + if(ObjectUtils.equals(optionValue, val, this.equalityKey)) { + return option; + } + } + + return null; }, onToggleAll(event) { - const value = this.allSelected ? [] : this.visibleOptions && this.visibleOptions.map(option => this.getOptionValue(option)); + let value = null; + + if (this.allSelected) { + value = []; + } + else if (this.visibleOptions) { + if (this.optionGroupLabel) { + value = []; + this.visibleOptions.forEach(optionGroup => value = [...value, ...this.getOptionGroupChildren(optionGroup)]); + } + else { + value = this.visibleOptions.map(option => this.getOptionValue(option)); + } + } this.$emit('update:modelValue', value); this.$emit('change', {originalEvent: event, value: value}); @@ -420,11 +497,25 @@ export default { } }, computed: { - visibleOptions() { - if (this.filterValue && this.filterValue.trim().length > 0) - return this.options.filter(option => this.getOptionLabel(option).toLocaleLowerCase(this.filterLocale).indexOf(this.filterValue.toLocaleLowerCase(this.filterLocale)) > -1); - else + visibleOptions() { + if (this.filterValue) { + if (this.optionGroupLabel) { + let filteredGroups = []; + for (let optgroup of this.options) { + let filteredSubOptions = FilterService.filter(this.getOptionGroupChildren(optgroup), this.searchFields, this.filterValue, this.filterMatchMode, this.filterLocale); + if (filteredSubOptions && filteredSubOptions.length) { + filteredGroups.push({...optgroup, ...{items: filteredSubOptions}}); + } + } + return filteredGroups + } + else { + return FilterService.filter(this.options, this.searchFields, this.filterValue, 'contains', this.filterLocale); + } + } + else { return this.options; + } }, containerClass() { return [ @@ -468,25 +559,54 @@ export default { }, allSelected() { if (this.filterValue && this.filterValue.trim().length > 0) { - let allSelected = true; - if(this.visibleOptions.length > 0) { - for (let option of this.visibleOptions) { - if (!this.isSelected(option)) { - allSelected = false; - break; - } - } + if (this.visibleOptions.length === 0) { + return false; } - else - allSelected = false; - return allSelected; + + if (this.optionGroupLabel) { + for (let optionGroup of this.visibleOptions) { + for (let option of this.getOptionGroupChildren(optionGroup)) { + if (!this.isSelected(option)) { + return false; + } + } + } + } + else { + for (let option of this.visibleOptions) { + if (!this.isSelected(option)) { + return false; + } + } + } + + return true; } else { - return this.modelValue && this.options && (this.modelValue.length > 0 && this.modelValue.length === this.options.length); + if (this.modelValue && this.options) { + let optionCount = 0; + if (this.optionGroupLabel) + this.options.forEach(optionGroup => optionCount += this.getOptionGroupChildren(optionGroup).length); + else + optionCount = this.options.length; + + return optionCount > 0 && optionCount === this.modelValue.length; + } + + return false; } }, equalityKey() { return this.optionValue ? null : this.dataKey; + }, + searchFields() { + return this.filterFields || [this.optionLabel]; + }, + emptyFilterMessageText() { + return this.emptyFilterMessage || this.$primevue.config.locale.emptyFilterMessage; + }, + emptyMessageText() { + return this.emptyMessage || this.$primevue.config.locale.emptyMessage; } }, directives: { @@ -568,6 +688,10 @@ export default { overflow: hidden; } +.p-multiselect-item-group { + cursor: auto; +} + .p-multiselect-header { display: flex; align-items: center; diff --git a/src/views/dropdown/DropdownDoc.vue b/src/views/dropdown/DropdownDoc.vue index 530cbdf99..37b26f26c 100755 --- a/src/views/dropdown/DropdownDoc.vue +++ b/src/views/dropdown/DropdownDoc.vue @@ -34,6 +34,52 @@ data() {
Placeholder

Common pattern is providing an empty option as the placeholder when using native selects, however Dropdown has built-in support using the placeholder option so it is suggested to use it instead of creating an empty option.

+
Grouping
+

Options groups are specified with the optionGroupLabel and optionGroupChildren properties.

+

+export default {
+    data() {
+        return {
+            selectedGroupedCity: null,
+            groupedCities: [{
+                label: 'Germany', code: 'DE', 
+                items: [
+                    {label: 'Berlin', value: 'Berlin'},
+                    {label: 'Frankfurt', value: 'Frankfurt'},
+                    {label: 'Hamburg', value: 'Hamburg'},
+                    {label: 'Munich', value: 'Munich'}
+                ]
+            },
+            {
+                label: 'USA', code: 'US', 
+                items: [
+                    {label: 'Chicago', value: 'Chicago'},
+                    {label: 'Los Angeles', value: 'Los Angeles'},
+                    {label: 'New York', value: 'New York'},
+                    {label: 'San Francisco', value: 'San Francisco'}
+                ]
+            },
+            {
+                label: 'Japan', code: 'JP', 
+                items: [
+                    {label: 'Kyoto', value: 'Kyoto'},
+                    {label: 'Osaka', value: 'Osaka'},
+                    {label: 'Tokyo', value: 'Tokyo'},
+                    {label: 'Yokohama', value: 'Yokohama'}
+                ]
+            }]
+        }
+    }
+}
+
+ +

+
+
Filtering

Filtering allows searching items in the list using an input field at the header. In order to use filtering, enable filter property. By default, optionLabel is used when searching and filterFields can be used to customize the fields being utilized. Furthermore, filterMatchMode is available diff --git a/src/views/listbox/ListboxDoc.vue b/src/views/listbox/ListboxDoc.vue index 280d71369..dc79467d0 100755 --- a/src/views/listbox/ListboxDoc.vue +++ b/src/views/listbox/ListboxDoc.vue @@ -38,23 +38,6 @@ data() {


 <Listbox v-model="selectedCity" :options="cities" optionLabel="name" :multiple="true"/>
 
-
- -
Templating
-

Label of an option is used as the display text of an item by default, for custom content support define an option template that gets the option instance as a parameter. - In addition optiongroup, header, footer, emptyfilter and empty slots are provided for further customization.

-

 
Grouping
@@ -97,9 +80,9 @@ export default {

 
@@ -110,6 +93,23 @@ export default {

 <Listbox v-model="selectedCity" :options="cities" optionLabel="name" :filter="true"/>
 
+
+ +
Templating
+

Label of an option is used as the display text of an item by default, for custom content support define an option template that gets the option instance as a parameter. + In addition optiongroup, header, footer, emptyfilter and empty slots are provided for further customization.

+

 
Properties
diff --git a/src/views/multiselect/MultiSelectDemo.vue b/src/views/multiselect/MultiSelectDemo.vue index 2791a5140..089bc612f 100755 --- a/src/views/multiselect/MultiSelectDemo.vue +++ b/src/views/multiselect/MultiSelectDemo.vue @@ -11,10 +11,20 @@
Basic
- +
Chips
- + + +
Grouped
+ + +
Advanced with Templating and Filtering
@@ -50,6 +60,7 @@ export default { selectedCities1: null, selectedCities2: null, selectedCountries: null, + selectedGroupedCities: null, cities: [ {name: 'New York', code: 'NY'}, {name: 'Rome', code: 'RM'}, @@ -68,7 +79,34 @@ export default { {name: 'Japan', code: 'JP'}, {name: 'Spain', code: 'ES'}, {name: 'United States', code: 'US'} - ] + ], + groupedCities: [{ + label: 'Germany', code: 'DE', + items: [ + {label: 'Berlin', value: 'Berlin'}, + {label: 'Frankfurt', value: 'Frankfurt'}, + {label: 'Hamburg', value: 'Hamburg'}, + {label: 'Munich', value: 'Munich'} + ] + }, + { + label: 'USA', code: 'US', + items: [ + {label: 'Chicago', value: 'Chicago'}, + {label: 'Los Angeles', value: 'Los Angeles'}, + {label: 'New York', value: 'New York'}, + {label: 'San Francisco', value: 'San Francisco'} + ] + }, + { + label: 'Japan', code: 'JP', + items: [ + {label: 'Kyoto', value: 'Kyoto'}, + {label: 'Osaka', value: 'Osaka'}, + {label: 'Tokyo', value: 'Tokyo'}, + {label: 'Yokohama', value: 'Yokohama'} + ] + }] } }, components: { @@ -79,7 +117,7 @@ export default { diff --git a/src/views/multiselect/MultiSelectDoc.vue b/src/views/multiselect/MultiSelectDoc.vue index 0cf361f64..6359c8cfa 100755 --- a/src/views/multiselect/MultiSelectDoc.vue +++ b/src/views/multiselect/MultiSelectDoc.vue @@ -42,11 +42,68 @@ data() { -
Custom Content
-

Label of an option is used as the display text of an item by default, for custom content support define an option template that gets the option instance as a parameter.

-

In addition the value template can be used to customize the selected values display instead of the default comma separated list.

+
Grouping
+

Options groups are specified with the optionGroupLabel and optionGroupChildren properties.

+

+export default {
+    data() {
+        return {
+            selectedGroupedCities: null,
+            groupedCities: [{
+                label: 'Germany', code: 'DE', 
+                items: [
+                    {label: 'Berlin', value: 'Berlin'},
+                    {label: 'Frankfurt', value: 'Frankfurt'},
+                    {label: 'Hamburg', value: 'Hamburg'},
+                    {label: 'Munich', value: 'Munich'}
+                ]
+            },
+            {
+                label: 'USA', code: 'US', 
+                items: [
+                    {label: 'Chicago', value: 'Chicago'},
+                    {label: 'Los Angeles', value: 'Los Angeles'},
+                    {label: 'New York', value: 'New York'},
+                    {label: 'San Francisco', value: 'San Francisco'}
+                ]
+            },
+            {
+                label: 'Japan', code: 'JP', 
+                items: [
+                    {label: 'Kyoto', value: 'Kyoto'},
+                    {label: 'Osaka', value: 'Osaka'},
+                    {label: 'Tokyo', value: 'Tokyo'},
+                    {label: 'Yokohama', value: 'Yokohama'}
+                ]
+            }]
+        }
+    }
+}
+
+ +

+
+ +
Filtering
+

Filtering allows searching items in the list using an input field at the header. In order to use filtering, enable filter property. By default, + optionLabel is used when searching and filterFields can be used to customize the fields being utilized. Furthermore, filterMatchMode is available + to define the search algorithm. Valid values are "contains" (default), "startsWith" and "endsWith".

+ +

+<MultiSelect v-model="selectedCars" :options="cars" :filter="true" optionLabel="brand" placeholder="Select Brands"/>
+
+
+ +
Templating
+

Label of an option is used as the display text of an item by default, for custom content support define an option template that gets the option instance as a parameter. + In addition value, optiongroup, header, footer, emptyfilter and empty slots are provided for further customization.


-
- -
Filter
-

Filtering allows searching items in the list using an input field at the header. In order to use filtering, enable the filter property.

-

-<MultiSelect v-model="selectedCars" :options="cars" :filter="true" optionLabel="brand" placeholder="Select Brands"/>
-
 
Properties
@@ -116,12 +167,54 @@ data() { null Property name or getter function to use as the disabled flag of an option, defaults to false when not defined. + + optionGroupLabel + string + null + Property name or getter function to use as the label of an option group. + + + optionGroupChildren + string + null + Property name or getter function that refers to the children options of option group. + scrollHeight string 200px Height of the viewport in pixels, a scrollbar is defined if height of list exceeds this value. + + filter + boolean + false + When specified, displays a filter input at header. + + + filterPlaceholder + string + null + Placeholder text to show when filter input is empty. + + + filterLocale + string + undefined + Locale to use in filtering. The default locale is the host environment's current locale. + + + filterMatchMode + string + contains + Defines the filtering algorithm to use when searching the options. Valid values are "contains" (default), "startsWith" and "endsWith" + + + filterFields + array + null + Fields used when filtering the options, defaults to optionLabel. + placeholder string @@ -134,12 +227,6 @@ data() { false When present, it specifies that the component should be disabled. - - filter - boolean - false - When specified, displays an input field to filter the items on keyup. - tabindex string @@ -158,18 +245,6 @@ data() { null A property to uniquely identify an option. - - filterPlaceholder - string - null - Placeholder text to show when filter input is empty. - - - filterLocale - string - undefined - Locale to use in filtering. The default locale is the host environment's current locale. - ariaLabelledBy string @@ -186,7 +261,13 @@ data() { emptyFilterMessage string No results found - Text to display when filtering does not return any results. + Text to display when filtering does not return any results. Defaults to value from PrimeVue locale configuration. + + + emptyMessage + string + No results found + Text to display when there are no options available. Defaults to value from PrimeVue locale configuration. display @@ -270,6 +351,53 @@ data() {
+
Slots
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameParameters
optionoption: Option instance
+ index: Index of the option
optiongroupoption: OptionGroup instance
+ index: Index of the option group
valuevalue: Value of the component
+ placeholder: Placeholder prop value
headervalue: Value of the component
+ options: Displayed options
footervalue: Value of the component
+ options: Displayed options
emptyfilter-
empty-
+
+
Styling

Following is the list of structural style classes, for theming classes visit theming page.

@@ -331,10 +459,20 @@ data() {