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,46 +46,53 @@
<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-if="!optionGroupLabel"> <template v-slot:content="{ styleClass, contentRef, items, getItemOptions }">
<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)" <ul :ref="contentRef" :class="['p-multiselect-items p-component', styleClass]" role="listbox" aria-multiselectable="true">
:key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @keydown="onOptionKeyDown($event, option)" :tabindex="tabindex||'0'" :aria-label="getOptionLabel(option)" v-ripple> <template v-if="!optionGroupLabel">
<div class="p-checkbox p-component"> <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)"
<div :class="['p-checkbox-box', {'p-highlight': isSelected(option)}]"> :key="getOptionRenderKey(option)" @click="onOptionSelect($event, option)" @keydown="onOptionKeyDown($event, option)" :tabindex="tabindex||'0'" :aria-label="getOptionLabel(option)" v-ripple>
<span :class="['p-checkbox-icon', {'pi pi-check': isSelected(option)}]"></span> <div class="p-checkbox p-component">
</div> <div :class="['p-checkbox-box', {'p-highlight': isSelected(option)}]">
</div> <span :class="['p-checkbox-icon', {'pi pi-check': isSelected(option)}]"></span>
<slot name="option" :option="option" :index="i"> </div>
<span>{{getOptionLabel(option)}}</span>
</slot>
</li>
</template>
<template v-else>
<template v-for="(optionGroup, i) of visibleOptions" :key="getOptionGroupRenderKey(optionGroup)">
<li class="p-multiselect-item-group">
<slot name="optiongroup" :option="optionGroup" :index="i">{{getOptionGroupLabel(optionGroup)}}</slot>
</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)"
: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-box', {'p-highlight': isSelected(option)}]">
<span :class="['p-checkbox-icon', {'pi pi-check': isSelected(option)}]"></span>
</div> </div>
</div> <slot name="option" :option="option" :index="getOptionIndex(i, getItemOptions)">
<slot name="option" :option="option" :index="i"> <span>{{getOptionLabel(option)}}</span>
<span>{{getOptionLabel(option)}}</span> </slot>
</slot> </li>
</template>
<template v-else>
<template v-for="(optionGroup, i) of items" :key="getOptionGroupRenderKey(optionGroup)">
<li class="p-multiselect-item-group">
<slot name="optiongroup" :option="optionGroup" :index="getOptionIndex(i, getItemOptions)">{{getOptionGroupLabel(optionGroup)}}</slot>
</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)"
: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-box', {'p-highlight': isSelected(option)}]">
<span :class="['p-checkbox-icon', {'pi pi-check': isSelected(option)}]"></span>
</div>
</div>
<slot name="option" :option="option" :index="getOptionIndex(i, getItemOptions)">
<span>{{getOptionLabel(option)}}</span>
</slot>
</li>
</template>
</template>
<li v-if="filterValue && (!items || (items && items.length === 0))" class="p-multiselect-empty-message">
<slot name="emptyfilter">{{emptyFilterMessageText}}</slot>
</li> </li>
</template> <li v-else-if="(!options || (options && options.length === 0))" class="p-multiselect-empty-message">
<slot name="empty">{{emptyMessageText}}</slot>
</li>
</ul>
</template> </template>
<li v-if="filterValue && (!visibleOptions || (visibleOptions && visibleOptions.length === 0))" class="p-multiselect-empty-message"> <template v-slot:loader="{ options }" v-if="$slots.loader">
<slot name="emptyfilter">{{emptyFilterMessageText}}</slot> <slot name="loader" :options="options"></slot>
</li> </template>
<li v-else-if="(!options || (options && options.length === 0))" class="p-multiselect-empty-message"> </VirtualScroller>
<slot name="empty">{{emptyMessageText}}</slot>
</li>
</ul>
</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;
}, },
isSelected(option) { getSelectedOptionIndex() {
let selected = false; if (this.modelValue != null && this.options) {
let optionValue = this.getOptionValue(option); if (this.optionGroupLabel) {
for (let i = 0; i < this.options.length; i++) {
if (this.modelValue) { let selectedOptionIndex = this.findOptionIndexInList(this.modelValue, this.getOptionGroupChildren(this.options[i]));
for (let val of this.modelValue) { if (selectedOptionIndex !== -1) {
if (ObjectUtils.equals(val, optionValue, this.equalityKey)) { return {group: i, option: selectedOptionIndex};
selected = true; }
break;
} }
} }
else {
return this.findOptionIndexInList(this.modelValue, this.options);
}
} }
return selected; return -1;
},
findOptionIndexInList(value, list) {
return value ? list.findIndex(item => value.some(val => ObjectUtils.equals(val, this.getOptionValue(item), this.equalityKey))) : -1;
},
isSelected(option) {
if (this.modelValue) {
let optionValue = this.getOptionValue(option);
let key = this.equalityKey;
return this.modelValue.some(val => ObjectUtils.equals(val, optionValue, key));
}
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,24 +534,37 @@ 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) {
let value = null; if (this.selectAll !== null) {
this.$emit('selectall-change', {originalEvent: event, checked: !this.allSelected});
if (this.allSelected) {
value = [];
} }
else if (this.visibleOptions) { else {
if (this.optionGroupLabel) { let value = null;
if (this.allSelected) {
value = []; value = [];
this.visibleOptions.forEach(optionGroup => value = [...value, ...this.getOptionGroupChildren(optionGroup)]);
} }
else { else if (this.visibleOptions) {
value = this.visibleOptions.map(option => this.getOptionValue(option)); 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('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,13 +637,18 @@ export default {
let label; let label;
if (this.modelValue && this.modelValue.length) { if (this.modelValue && this.modelValue.length) {
label = ''; if (!this.maxSelectedLabels || this.modelValue.length <= this.maxSelectedLabels) {
for(let i = 0; i < this.modelValue.length; i++) { label = '';
if(i !== 0) { for(let i = 0; i < this.modelValue.length; i++) {
label += ', '; if(i !== 0) {
} label += ', ';
}
label += this.getLabelByValue(this.modelValue[i]); label += this.getLabelByValue(this.modelValue[i]);
}
}
else {
return this.getSelectedItemsLabel();
} }
} }
else { else {
@ -587,42 +658,47 @@ export default {
return label; return label;
}, },
allSelected() { allSelected() {
if (this.filterValue && this.filterValue.trim().length > 0) { if (this.selectAll !== null) {
if (this.visibleOptions.length === 0) { return this.selectAll;
return false; }
} else {
if (this.filterValue && this.filterValue.trim().length > 0) {
if (this.visibleOptions.length === 0) {
return false;
}
if (this.optionGroupLabel) { if (this.optionGroupLabel) {
for (let optionGroup of this.visibleOptions) { for (let optionGroup of this.visibleOptions) {
for (let option of this.getOptionGroupChildren(optionGroup)) { for (let option of this.getOptionGroupChildren(optionGroup)) {
if (!this.isSelected(option)) {
return false;
}
}
}
}
else {
for (let option of this.visibleOptions) {
if (!this.isSelected(option)) { if (!this.isSelected(option)) {
return false; return false;
} }
} }
} }
return true;
} }
else { else {
for (let option of this.visibleOptions) { if (this.modelValue && this.options) {
if (!this.isSelected(option)) { let optionCount = 0;
return false; 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;
} }
return true;
}
else {
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() { equalityKey() {
@ -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>