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

View File

@ -29,7 +29,7 @@
<transition name="p-connected-overlay" @enter="onOverlayEnter" @leave="onOverlayLeave" @after-leave="onOverlayAfterLeave">
<div :ref="overlayRef" :class="panelStyleClass" v-if="overlayVisible" @click="onOverlayClick">
<slot name="header" :value="modelValue" :options="visibleOptions"></slot>
<div class="p-multiselect-header" v-if="(showToggleAll && selectionLimit == null) || filter">
<div class="p-multiselect-header" v-if="(showToggleAll && selectionLimit == null) || filter">
<div class="p-checkbox p-component" @click="onToggleAll" role="checkbox" :aria-checked="allSelected" v-if="showToggleAll && selectionLimit == null">
<div class="p-hidden-accessible">
<input type="checkbox" readonly @focus="onHeaderCheckboxFocus" @blur="onHeaderCheckboxBlur">
@ -46,46 +46,53 @@
<span class="p-multiselect-close-icon pi pi-times" />
</button>
</div>
<div class="p-multiselect-items-wrapper" :style="{'max-height': scrollHeight}">
<ul class="p-multiselect-items p-component" role="listbox" aria-multiselectable="true">
<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)"
: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="i">
<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 class="p-multiselect-items-wrapper" :style="{'max-height': virtualScrollerDisabled ? scrollHeight : ''}">
<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">
<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>
<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="i">
<span>{{getOptionLabel(option)}}</span>
</slot>
<slot name="option" :option="option" :index="getOptionIndex(i, getItemOptions)">
<span>{{getOptionLabel(option)}}</span>
</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>
</template>
<li v-else-if="(!options || (options && options.length === 0))" class="p-multiselect-empty-message">
<slot name="empty">{{emptyMessageText}}</slot>
</li>
</ul>
</template>
<li v-if="filterValue && (!visibleOptions || (visibleOptions && visibleOptions.length === 0))" class="p-multiselect-empty-message">
<slot name="emptyfilter">{{emptyFilterMessageText}}</slot>
</li>
<li v-else-if="(!options || (options && options.length === 0))" class="p-multiselect-empty-message">
<slot name="empty">{{emptyMessageText}}</slot>
</li>
</ul>
<template v-slot:loader="{ options }" v-if="$slots.loader">
<slot name="loader" :options="options"></slot>
</template>
</VirtualScroller>
</div>
<slot name="footer" :value="modelValue" :options="visibleOptions"></slot>
</div>
@ -99,10 +106,11 @@ import {ConnectedOverlayScrollHandler,ObjectUtils,DomHandler,ZIndexUtils} from '
import OverlayEventBus from 'primevue/overlayeventbus';
import {FilterService} from 'primevue/api';
import Ripple from 'primevue/ripple';
import VirtualScroller from 'primevue/virtualscroller';
export default {
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: {
modelValue: null,
options: Array,
@ -149,6 +157,14 @@ export default {
default: 'comma'
},
panelClass: null,
selectedItemsLabel: {
type: String,
default: '{0} items selected'
},
maxSelectedLabels: {
type: Number,
default: null
},
selectionLimit: {
type: Number,
default: null
@ -164,6 +180,14 @@ export default {
loadingIcon: {
type: String,
default: 'pi pi-spinner pi-spin'
},
virtualScrollerOptions: {
type: Object,
default: null
},
selectAll: {
type: Boolean,
default: null
}
},
data() {
@ -178,6 +202,7 @@ export default {
resizeListener: null,
scrollHandler: null,
overlay: null,
virtualScroller: null,
beforeUnmount() {
this.unbindOutsideClickListener();
this.unbindResizeListener();
@ -193,6 +218,9 @@ export default {
}
},
methods: {
getOptionIndex(index, fn) {
return this.virtualScrollerDisabled ? index : (fn && fn(index)['index']);
},
getOptionLabel(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;
},
isSelected(option) {
let selected = false;
let optionValue = this.getOptionValue(option);
if (this.modelValue) {
for (let val of this.modelValue) {
if (ObjectUtils.equals(val, optionValue, this.equalityKey)) {
selected = true;
break;
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 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() {
this.$emit('before-show');
@ -317,7 +360,7 @@ export default {
if (selected)
value = this.modelValue.filter(val => !ObjectUtils.equals(val, this.getOptionValue(option), this.equalityKey));
else
value = [...this.modelValue || [], this.getOptionValue(option)];
value = [...(this.modelValue || []), this.getOptionValue(option)];
this.$emit('update:modelValue', value);
this.$emit('change', {originalEvent: event, value: value});
@ -383,6 +426,13 @@ export default {
this.$refs.filterInput.focus();
}
if (!this.virtualScrollerDisabled) {
const selectedIndex = this.getSelectedOptionIndex();
if (selectedIndex !== -1) {
this.virtualScroller.scrollToIndex(selectedIndex);
}
}
this.$emit('show');
},
onOverlayLeave() {
@ -484,24 +534,37 @@ export default {
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) {
let value = null;
if (this.allSelected) {
value = [];
if (this.selectAll !== null) {
this.$emit('selectall-change', {originalEvent: event, checked: !this.allSelected});
}
else if (this.visibleOptions) {
if (this.optionGroupLabel) {
else {
let value = null;
if (this.allSelected) {
value = [];
this.visibleOptions.forEach(optionGroup => value = [...value, ...this.getOptionGroupChildren(optionGroup)]);
}
else {
value = this.visibleOptions.map(option => this.getOptionValue(option));
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});
this.$emit('update:modelValue', value);
this.$emit('change', {originalEvent: event, value: value});
}
},
onFilterChange(event) {
this.$emit('filter', {originalEvent: event, value: event.target.value});
@ -512,6 +575,9 @@ export default {
overlayRef(el) {
this.overlay = el;
},
virtualScrollerRef(el) {
this.virtualScroller = el;
},
removeChip(item) {
let value = this.modelValue.filter(val => !ObjectUtils.equals(val, item, this.equalityKey));
@ -571,13 +637,18 @@ export default {
let label;
if (this.modelValue && this.modelValue.length) {
label = '';
for(let i = 0; i < this.modelValue.length; i++) {
if(i !== 0) {
label += ', ';
}
if (!this.maxSelectedLabels || this.modelValue.length <= this.maxSelectedLabels) {
label = '';
for(let i = 0; i < this.modelValue.length; i++) {
if(i !== 0) {
label += ', ';
}
label += this.getLabelByValue(this.modelValue[i]);
label += this.getLabelByValue(this.modelValue[i]);
}
}
else {
return this.getSelectedItemsLabel();
}
}
else {
@ -587,42 +658,47 @@ export default {
return label;
},
allSelected() {
if (this.filterValue && this.filterValue.trim().length > 0) {
if (this.visibleOptions.length === 0) {
return false;
}
if (this.selectAll !== null) {
return this.selectAll;
}
else {
if (this.filterValue && this.filterValue.trim().length > 0) {
if (this.visibleOptions.length === 0) {
return false;
}
if (this.optionGroupLabel) {
for (let optionGroup of this.visibleOptions) {
for (let option of this.getOptionGroupChildren(optionGroup)) {
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 {
for (let option of this.visibleOptions) {
if (!this.isSelected(option)) {
return false;
}
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;
}
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() {
@ -643,6 +719,9 @@ export default {
appendTarget() {
return this.appendDisabled ? null : this.appendTo;
},
virtualScrollerDisabled() {
return !this.virtualScrollerOptions;
},
maxSelectionLimitReached() {
return this.selectionLimit && (this.modelValue && this.modelValue.length === this.selectionLimit);
},
@ -652,6 +731,9 @@ export default {
},
directives: {
'ripple': Ripple
},
components: {
'VirtualScroller': VirtualScroller
}
}
</script>