Refactor #1451 - For MultiSelect

pull/1994/head
mertsincan 2021-08-17 14:02:42 +03:00
parent 0823bee377
commit c8421dcb0f
2 changed files with 182 additions and 94 deletions

View File

@ -1,4 +1,5 @@
import { VNode } from 'vue'; import { VNode } from 'vue';
import { VirtualScrollerProps } from '../virtualscroller';
interface MultiSelectProps { interface MultiSelectProps {
modelValue?: any; modelValue?: any;
@ -25,10 +26,14 @@ interface MultiSelectProps {
emptyMessage?: string; emptyMessage?: string;
display?: string; display?: string;
panelClass?: string; panelClass?: string;
selectedItemsLabel?: string;
maxSelectedLabels?: number;
selectionLimit?: number; selectionLimit?: number;
showToggleAll?: boolean; showToggleAll?: boolean;
loading?: boolean; loading?: boolean;
loadingIcon?: string; loadingIcon?: string;
virtualScrollerOptions?: VirtualScrollerProps;
selectAll?: boolean;
} }
declare class MultiSelect { declare class MultiSelect {
@ -40,6 +45,7 @@ declare class MultiSelect {
$emit(eventName: 'show'): this; $emit(eventName: 'show'): this;
$emit(eventName: 'hide'): this; $emit(eventName: 'hide'): this;
$emit(eventName: 'filter', e: { originalEvent: Event, value: string }): this; $emit(eventName: 'filter', e: { originalEvent: Event, value: string }): this;
$emit(eventName: 'selectall-change', e: { originalEvent: Event, checked: boolean }): this;
$slots: { $slots: {
value: VNode[]; value: VNode[];
header: VNode[]; header: VNode[];

View File

@ -46,25 +46,27 @@
<span class="p-multiselect-close-icon pi pi-times" /> <span class="p-multiselect-close-icon pi pi-times" />
</button> </button>
</div> </div>
<div class="p-multiselect-items-wrapper" :style="{'max-height': scrollHeight}"> <div class="p-multiselect-items-wrapper" :style="{'max-height': virtualScrollerDisabled ? scrollHeight : ''}">
<ul class="p-multiselect-items p-component" role="listbox" aria-multiselectable="true"> <VirtualScroller :ref="virtualScrollerRef" v-bind="virtualScrollerOptions" :items="visibleOptions" :style="{'height': scrollHeight}" :disabled="virtualScrollerDisabled">
<template v-slot:content="{ styleClass, contentRef, items, getItemOptions }">
<ul :ref="contentRef" :class="['p-multiselect-items p-component', styleClass]" role="listbox" aria-multiselectable="true">
<template v-if="!optionGroupLabel"> <template v-if="!optionGroupLabel">
<li v-for="(option, i) of visibleOptions" :class="['p-multiselect-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" role="option" :aria-selected="isSelected(option)" <li v-for="(option, i) of items" :class="['p-multiselect-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" role="option" :aria-selected="isSelected(option)"
:key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @keydown="onOptionKeyDown($event, option)" :tabindex="tabindex||'0'" :aria-label="getOptionLabel(option)" v-ripple> :key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @keydown="onOptionKeyDown($event, option)" :tabindex="tabindex||'0'" :aria-label="getOptionLabel(option)" v-ripple>
<div class="p-checkbox p-component"> <div class="p-checkbox p-component">
<div :class="['p-checkbox-box', {'p-highlight': isSelected(option)}]"> <div :class="['p-checkbox-box', {'p-highlight': isSelected(option)}]">
<span :class="['p-checkbox-icon', {'pi pi-check': isSelected(option)}]"></span> <span :class="['p-checkbox-icon', {'pi pi-check': isSelected(option)}]"></span>
</div> </div>
</div> </div>
<slot name="option" :option="option" :index="i"> <slot name="option" :option="option" :index="getOptionIndex(i, getItemOptions)">
<span>{{getOptionLabel(option)}}</span> <span>{{getOptionLabel(option)}}</span>
</slot> </slot>
</li> </li>
</template> </template>
<template v-else> <template v-else>
<template v-for="(optionGroup, i) of visibleOptions" :key="getOptionGroupRenderKey(optionGroup)"> <template v-for="(optionGroup, i) of items" :key="getOptionGroupRenderKey(optionGroup)">
<li class="p-multiselect-item-group"> <li class="p-multiselect-item-group">
<slot name="optiongroup" :option="optionGroup" :index="i">{{getOptionGroupLabel(optionGroup)}}</slot> <slot name="optiongroup" :option="optionGroup" :index="getOptionIndex(i, getItemOptions)">{{getOptionGroupLabel(optionGroup)}}</slot>
</li> </li>
<li v-for="(option, i) of getOptionGroupChildren(optionGroup)" :class="['p-multiselect-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" role="option" :aria-selected="isSelected(option)" <li v-for="(option, i) of getOptionGroupChildren(optionGroup)" :class="['p-multiselect-item', {'p-highlight': isSelected(option), 'p-disabled': isOptionDisabled(option)}]" role="option" :aria-selected="isSelected(option)"
:key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @keydown="onOptionKeyDown($event, option)" :tabindex="tabindex||'0'" :aria-label="getOptionLabel(option)" v-ripple> :key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @keydown="onOptionKeyDown($event, option)" :tabindex="tabindex||'0'" :aria-label="getOptionLabel(option)" v-ripple>
@ -73,19 +75,24 @@
<span :class="['p-checkbox-icon', {'pi pi-check': isSelected(option)}]"></span> <span :class="['p-checkbox-icon', {'pi pi-check': isSelected(option)}]"></span>
</div> </div>
</div> </div>
<slot name="option" :option="option" :index="i"> <slot name="option" :option="option" :index="getOptionIndex(i, getItemOptions)">
<span>{{getOptionLabel(option)}}</span> <span>{{getOptionLabel(option)}}</span>
</slot> </slot>
</li> </li>
</template> </template>
</template> </template>
<li v-if="filterValue && (!visibleOptions || (visibleOptions && visibleOptions.length === 0))" class="p-multiselect-empty-message"> <li v-if="filterValue && (!items || (items && items.length === 0))" class="p-multiselect-empty-message">
<slot name="emptyfilter">{{emptyFilterMessageText}}</slot> <slot name="emptyfilter">{{emptyFilterMessageText}}</slot>
</li> </li>
<li v-else-if="(!options || (options && options.length === 0))" class="p-multiselect-empty-message"> <li v-else-if="(!options || (options && options.length === 0))" class="p-multiselect-empty-message">
<slot name="empty">{{emptyMessageText}}</slot> <slot name="empty">{{emptyMessageText}}</slot>
</li> </li>
</ul> </ul>
</template>
<template v-slot:loader="{ options }" v-if="$slots.loader">
<slot name="loader" :options="options"></slot>
</template>
</VirtualScroller>
</div> </div>
<slot name="footer" :value="modelValue" :options="visibleOptions"></slot> <slot name="footer" :value="modelValue" :options="visibleOptions"></slot>
</div> </div>
@ -99,10 +106,11 @@ import {ConnectedOverlayScrollHandler,ObjectUtils,DomHandler,ZIndexUtils} from '
import OverlayEventBus from 'primevue/overlayeventbus'; import OverlayEventBus from 'primevue/overlayeventbus';
import {FilterService} from 'primevue/api'; import {FilterService} from 'primevue/api';
import Ripple from 'primevue/ripple'; import Ripple from 'primevue/ripple';
import VirtualScroller from 'primevue/virtualscroller';
export default { export default {
name: 'MultiSelect', name: 'MultiSelect',
emits: ['update:modelValue', 'before-show', 'before-hide', 'change', 'show', 'hide', 'filter'], emits: ['update:modelValue', 'before-show', 'before-hide', 'change', 'show', 'hide', 'filter', 'selectall-change'],
props: { props: {
modelValue: null, modelValue: null,
options: Array, options: Array,
@ -149,6 +157,14 @@ export default {
default: 'comma' default: 'comma'
}, },
panelClass: null, panelClass: null,
selectedItemsLabel: {
type: String,
default: '{0} items selected'
},
maxSelectedLabels: {
type: Number,
default: null
},
selectionLimit: { selectionLimit: {
type: Number, type: Number,
default: null default: null
@ -164,6 +180,14 @@ export default {
loadingIcon: { loadingIcon: {
type: String, type: String,
default: 'pi pi-spinner pi-spin' default: 'pi pi-spinner pi-spin'
},
virtualScrollerOptions: {
type: Object,
default: null
},
selectAll: {
type: Boolean,
default: null
} }
}, },
data() { data() {
@ -178,6 +202,7 @@ export default {
resizeListener: null, resizeListener: null,
scrollHandler: null, scrollHandler: null,
overlay: null, overlay: null,
virtualScroller: null,
beforeUnmount() { beforeUnmount() {
this.unbindOutsideClickListener(); this.unbindOutsideClickListener();
this.unbindResizeListener(); this.unbindResizeListener();
@ -193,6 +218,9 @@ export default {
} }
}, },
methods: { methods: {
getOptionIndex(index, fn) {
return this.virtualScrollerDisabled ? index : (fn && fn(index)['index']);
},
getOptionLabel(option) { getOptionLabel(option) {
return this.optionLabel ? ObjectUtils.resolveFieldData(option, this.optionLabel) : option; return this.optionLabel ? ObjectUtils.resolveFieldData(option, this.optionLabel) : option;
}, },
@ -218,20 +246,35 @@ export default {
return this.optionDisabled ? ObjectUtils.resolveFieldData(option, this.optionDisabled) : false; return this.optionDisabled ? ObjectUtils.resolveFieldData(option, this.optionDisabled) : false;
}, },
getSelectedOptionIndex() {
if (this.modelValue != null && this.options) {
if (this.optionGroupLabel) {
for (let i = 0; i < this.options.length; i++) {
let selectedOptionIndex = this.findOptionIndexInList(this.modelValue, this.getOptionGroupChildren(this.options[i]));
if (selectedOptionIndex !== -1) {
return {group: i, option: selectedOptionIndex};
}
}
}
else {
return this.findOptionIndexInList(this.modelValue, this.options);
}
}
return -1;
},
findOptionIndexInList(value, list) {
return value ? list.findIndex(item => value.some(val => ObjectUtils.equals(val, this.getOptionValue(item), this.equalityKey))) : -1;
},
isSelected(option) { isSelected(option) {
let selected = false;
let optionValue = this.getOptionValue(option);
if (this.modelValue) { if (this.modelValue) {
for (let val of this.modelValue) { let optionValue = this.getOptionValue(option);
if (ObjectUtils.equals(val, optionValue, this.equalityKey)) { let key = this.equalityKey;
selected = true;
break; return this.modelValue.some(val => ObjectUtils.equals(val, optionValue, key));
}
}
} }
return selected; return false;
}, },
show() { show() {
this.$emit('before-show'); this.$emit('before-show');
@ -317,7 +360,7 @@ export default {
if (selected) if (selected)
value = this.modelValue.filter(val => !ObjectUtils.equals(val, this.getOptionValue(option), this.equalityKey)); value = this.modelValue.filter(val => !ObjectUtils.equals(val, this.getOptionValue(option), this.equalityKey));
else else
value = [...this.modelValue || [], this.getOptionValue(option)]; value = [...(this.modelValue || []), this.getOptionValue(option)];
this.$emit('update:modelValue', value); this.$emit('update:modelValue', value);
this.$emit('change', {originalEvent: event, value: value}); this.$emit('change', {originalEvent: event, value: value});
@ -383,6 +426,13 @@ export default {
this.$refs.filterInput.focus(); this.$refs.filterInput.focus();
} }
if (!this.virtualScrollerDisabled) {
const selectedIndex = this.getSelectedOptionIndex();
if (selectedIndex !== -1) {
this.virtualScroller.scrollToIndex(selectedIndex);
}
}
this.$emit('show'); this.$emit('show');
}, },
onOverlayLeave() { onOverlayLeave() {
@ -484,7 +534,19 @@ export default {
return null; return null;
}, },
getSelectedItemsLabel() {
let pattern = /{(.*?)}/;
if (pattern.test(this.selectedItemsLabel)) {
return this.selectedItemsLabel.replace(this.selectedItemsLabel.match(pattern)[0], this.modelValue.length + '');
}
return this.selectedItemsLabel;
},
onToggleAll(event) { onToggleAll(event) {
if (this.selectAll !== null) {
this.$emit('selectall-change', {originalEvent: event, checked: !this.allSelected});
}
else {
let value = null; let value = null;
if (this.allSelected) { if (this.allSelected) {
@ -502,6 +564,7 @@ export default {
this.$emit('update:modelValue', value); this.$emit('update:modelValue', value);
this.$emit('change', {originalEvent: event, value: value}); this.$emit('change', {originalEvent: event, value: value});
}
}, },
onFilterChange(event) { onFilterChange(event) {
this.$emit('filter', {originalEvent: event, value: event.target.value}); this.$emit('filter', {originalEvent: event, value: event.target.value});
@ -512,6 +575,9 @@ export default {
overlayRef(el) { overlayRef(el) {
this.overlay = el; this.overlay = el;
}, },
virtualScrollerRef(el) {
this.virtualScroller = el;
},
removeChip(item) { removeChip(item) {
let value = this.modelValue.filter(val => !ObjectUtils.equals(val, item, this.equalityKey)); let value = this.modelValue.filter(val => !ObjectUtils.equals(val, item, this.equalityKey));
@ -571,6 +637,7 @@ export default {
let label; let label;
if (this.modelValue && this.modelValue.length) { if (this.modelValue && this.modelValue.length) {
if (!this.maxSelectedLabels || this.modelValue.length <= this.maxSelectedLabels) {
label = ''; label = '';
for(let i = 0; i < this.modelValue.length; i++) { for(let i = 0; i < this.modelValue.length; i++) {
if(i !== 0) { if(i !== 0) {
@ -580,6 +647,10 @@ export default {
label += this.getLabelByValue(this.modelValue[i]); label += this.getLabelByValue(this.modelValue[i]);
} }
} }
else {
return this.getSelectedItemsLabel();
}
}
else { else {
label = this.placeholder; label = this.placeholder;
} }
@ -587,6 +658,10 @@ export default {
return label; return label;
}, },
allSelected() { allSelected() {
if (this.selectAll !== null) {
return this.selectAll;
}
else {
if (this.filterValue && this.filterValue.trim().length > 0) { if (this.filterValue && this.filterValue.trim().length > 0) {
if (this.visibleOptions.length === 0) { if (this.visibleOptions.length === 0) {
return false; return false;
@ -624,6 +699,7 @@ export default {
return false; return false;
} }
}
}, },
equalityKey() { equalityKey() {
return this.optionValue ? null : this.dataKey; return this.optionValue ? null : this.dataKey;
@ -643,6 +719,9 @@ export default {
appendTarget() { appendTarget() {
return this.appendDisabled ? null : this.appendTo; return this.appendDisabled ? null : this.appendTo;
}, },
virtualScrollerDisabled() {
return !this.virtualScrollerOptions;
},
maxSelectionLimitReached() { maxSelectionLimitReached() {
return this.selectionLimit && (this.modelValue && this.modelValue.length === this.selectionLimit); return this.selectionLimit && (this.modelValue && this.modelValue.length === this.selectionLimit);
}, },
@ -652,6 +731,9 @@ export default {
}, },
directives: { directives: {
'ripple': Ripple 'ripple': Ripple
},
components: {
'VirtualScroller': VirtualScroller
} }
} }
</script> </script>