Fixed #976 - AutoComplete Enhancements

pull/978/head
Cagatay Civici 2021-02-15 18:05:04 +03:00
parent 72dbd25535
commit d2cbbba647
5 changed files with 315 additions and 18 deletions

View File

@ -4,6 +4,8 @@ interface AutoCompleteProps {
modelValue?: any;
suggestions?: any[];
field?: string|Function;
optionGroupLabel?: string;
optionGroupChildren?: string;
scrollHeight?: string;
dropdown?: boolean;
dropdownMode?: string;

View File

@ -16,13 +16,25 @@
<Button ref="dropdownButton" type="button" icon="pi pi-chevron-down" class="p-autocomplete-dropdown" :disabled="$attrs.disabled" @click="onDropdownClick" v-if="dropdown"/>
<transition name="p-connected-overlay" @enter="onOverlayEnter" @leave="onOverlayLeave">
<div :ref="overlayRef" class="p-autocomplete-panel p-component" :style="{'max-height': scrollHeight}" v-if="overlayVisible">
<slot name="header" :value="modelValue" :suggestions="suggestions"></slot>
<ul :id="listId" class="p-autocomplete-items" role="listbox">
<li v-for="(item, i) of suggestions" class="p-autocomplete-item" :key="i" @click="selectItem($event, item)" role="option" v-ripple>
<slot name="item" :item="item" :index="i">
{{getItemContent(item)}}
</slot>
</li>
<template v-if="!optionGroupLabel">
<li v-for="(item, i) of suggestions" class="p-autocomplete-item" :key="i" @click="selectItem($event, item)" role="option" v-ripple>
<slot name="item" :item="item" :index="i">{{getItemContent(item)}}</slot>
</li>
</template>
<template v-else>
<template v-for="(optionGroup, i) of suggestions" :key="getOptionGroupRenderKey(optionGroup)">
<li class="p-autocomplete-item-group" >
<slot name="optiongroup" :item="optionGroup" :index="i">{{getOptionGroupLabel(optionGroup)}}</slot>
</li>
<li v-for="(item, i) of getOptionGroupChildren(optionGroup)" class="p-autocomplete-item" :key="i" @click="selectItem($event, item)" role="option" v-ripple>
<slot name="item" :item="item" :index="i">{{getItemContent(item)}}</slot>
</li>
</template>
</template>
</ul>
<slot name="footer" :value="modelValue" :suggestions="suggestions"></slot>
</div>
</transition>
</span>
@ -49,6 +61,8 @@ export default {
type: [String,Function],
default: null
},
optionGroupLabel: null,
optionGroupChildren: null,
scrollHeight: {
type: String,
default: '200px'
@ -102,7 +116,6 @@ export default {
watch: {
suggestions() {
if (this.searching) {
if (this.suggestions && this.suggestions.length)
this.showOverlay();
else
@ -129,6 +142,15 @@ export default {
}
},
methods: {
getOptionGroupRenderKey(optionGroup) {
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupLabel);
},
getOptionGroupLabel(optionGroup) {
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupLabel);
},
getOptionGroupChildren(optionGroup) {
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupChildren);
},
onOverlayEnter() {
this.overlay.style.zIndex = String(DomHandler.generateZIndex());
this.appendContainer();
@ -328,7 +350,7 @@ export default {
//down
case 40:
if (highlightItem) {
let nextElement = highlightItem.nextElementSibling;
let nextElement = this.findNextItem(highlightItem);
if (nextElement) {
DomHandler.addClass(nextElement, 'p-highlight');
DomHandler.removeClass(highlightItem, 'p-highlight');
@ -336,7 +358,14 @@ export default {
}
}
else {
DomHandler.addClass(this.overlay.firstChild.firstElementChild, 'p-highlight');
highlightItem = this.overlay.firstElementChild.firstElementChild;
if (DomHandler.hasClass(highlightItem, 'p-autocomplete-item-group')) {
highlightItem = this.findNextItem(highlightItem);
}
if (highlightItem) {
DomHandler.addClass(highlightItem, 'p-highlight');
}
}
event.preventDefault();
@ -345,7 +374,7 @@ export default {
//up
case 38:
if (highlightItem) {
let previousElement = highlightItem.previousElementSibling;
let previousElement = this.findPrevItem(highlightItem);
if (previousElement) {
DomHandler.addClass(previousElement, 'p-highlight');
DomHandler.removeClass(highlightItem, 'p-highlight');
@ -407,6 +436,22 @@ export default {
}
}
},
findNextItem(item) {
let nextItem = item.nextElementSibling;
if (nextItem)
return DomHandler.hasClass(nextItem, 'p-autocomplete-item-group') ? this.findNextItem(nextItem) : nextItem;
else
return null;
},
findPrevItem(item) {
let prevItem = item.previousElementSibling;
if (prevItem)
return DomHandler.hasClass(prevItem, 'p-autocomplete-item-group') ? this.findPrevItem(prevItem) : prevItem;
else
return null;
},
onChange(event) {
if (this.forceSelection) {
let valid = false;

View File

@ -13,6 +13,16 @@
<h5>Basic</h5>
<AutoComplete v-model="selectedCountry1" :suggestions="filteredCountries" @complete="searchCountry($event)" field="name" />
<h5>Grouped</h5>
<AutoComplete v-model="selectedCity" :suggestions="filteredCities" @complete="searchCity($event)" field="label" optionGroupLabel="label" optionGroupChildren="items">
<template #optiongroup="slotProps">
<div class="p-d-flex p-ai-center country-item">
<img src="../../assets/images/flag_placeholder.png" :class="'flag flag-' + slotProps.item.code.toLowerCase()" width="18" />
<div>{{slotProps.item.label}}</div>
</div>
</template>
</AutoComplete>
<h5>Dropdown, Templating and Force Selection</h5>
<AutoComplete v-model="selectedCountry2" :suggestions="filteredCountries" @complete="searchCountry($event)" :dropdown="true" field="name" forceSelection>
<template #item="slotProps">
@ -37,6 +47,7 @@
<script>
import CountryService from '../../service/CountryService';
import AutoCompleteDoc from './AutoCompleteDoc';
import {FilterService,FilterMatchMode} from 'primevue/api';
export default {
data() {
@ -44,8 +55,37 @@ export default {
countries: null,
selectedCountry1: null,
selectedCountry2: null,
selectedCity: null,
filteredCities: null,
filteredCountries: null,
selectedCountries: []
selectedCountries: [],
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'}
]
}]
}
},
countryService: null,
@ -67,6 +107,19 @@ export default {
});
}
}, 250);
},
searchCity(event) {
let query = event.query;
let filteredCities = [];
for (let country of this.groupedCities) {
let filteredItems = FilterService.filter(country.items, ['label'], query, FilterMatchMode.CONTAINS);
if (filteredItems && filteredItems.length) {
filteredCities.push({...country, ...{items: filteredItems}});
}
}
this.filteredCities = filteredCities;
}
},
components: {

View File

@ -60,6 +60,51 @@ export default {
<pre v-code><code>
&lt;AutoComplete field="label" v-model="selectedCountry" :suggestions="filteredCountriesBasic" @complete="searchCountryBasic($event)" /&gt;
</code></pre>
<h5>Grouping</h5>
<p>Options groups are specified with the <i>optionGroupLabel</i> and <i>optionGroupChildren</i> properties.</p>
<pre v-code.script><code>
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'}
]
}]
}
}
}
</code></pre>
<pre v-code><code><template v-pre>
&lt;AutoComplete v-model="selectedCity" :suggestions="filteredCities" @complete="searchCity($event)"
field="label" optionGroupLabel="label" optionGroupChildren="items"&gt;&lt;/AutoComplete&gt;
</template>
</code></pre>
<h5>Force Selection</h5>
@ -71,10 +116,11 @@ export default {
</code></pre>
<h5>Templating</h5>
<p>Item template allows displaying custom content inside the suggestions panel. The slotProps variable passed to the template provides an item property to represent an item in the suggestions collection.</p>
<p>Item template allows displaying custom content inside the suggestions panel. The slotProps variable passed to the template provides an item property to represent an item in the suggestions collection.
In addition <i>optiongroup</i>, <i>header</i> and <i>footer</i> slots are provided for further customization</p>
<pre v-code><code><template v-pre>
&lt;AutoComplete v-model="brand" :suggestions="filteredBrands" @complete="searchBrand($event)" placeholder="Hint: type 'v' or 'f'" :dropdown="true"&gt;
&lt;template #item="slotProps"&gt;
&lt;template #item="slotProps"&gt;
&lt;img :alt="slotProps.item" :src="'demo/images/car/' + slotProps.item + '.png'" /&gt;
&lt;div&gt;{{slotProps.item}}&lt;/div&gt;
&lt;/template&gt;
@ -113,6 +159,18 @@ export default {
<td>null</td>
<td>Property name or getter function of a suggested object to resolve and display.</td>
</tr>
<tr>
<td>optionGroupLabel</td>
<td>string</td>
<td>null</td>
<td>Property name or getter function to use as the label of an option group.</td>
</tr>
<tr>
<td>optionGroupChildren</td>
<td>string</td>
<td>null</td>
<td>Property name or getter function that refers to the children options of option group.</td>
</tr>
<tr>
<td>scrollHeight</td>
<td>string</td>
@ -227,6 +285,40 @@ export default {
</table>
</div>
<h5>Slots</h5>
<div class="doc-tablewrapper">
<table class="doc-table">
<thead>
<tr>
<th>Name</th>
<th>Parameters</th>
</tr>
</thead>
<tbody>
<tr>
<td>item</td>
<td>item: Option instance <br />
index: Index of the option</td>
</tr>
<tr>
<td>optiongroup</td>
<td>item: OptionGroup instance <br />
index: Index of the option group</td>
</tr>
<tr>
<td>header</td>
<td>value: Value of the component <br />
suggestions: Displayed options</td>
</tr>
<tr>
<td>footer</td>
<td>value: Value of the component <br />
suggestions: Displayed options</td>
</tr>
</tbody>
</table>
</div>
<h5>Styling</h5>
<p>Following is the list of structural style classes</p>
<div class="doc-tablewrapper">
@ -285,6 +377,16 @@ export default {
&lt;h5&gt;Basic&lt;/h5&gt;
&lt;AutoComplete v-model="selectedCountry1" :suggestions="filteredCountries" @complete="searchCountry($event)" field="name" /&gt;
&lt;h5&gt;Grouped&lt;/h5&gt;
&lt;AutoComplete v-model="selectedCity" :suggestions="filteredCities" @complete="searchCity($event)" field="label" optionGroupLabel="label" optionGroupChildren="items"&gt;
&lt;template #optiongroup="slotProps"&gt;
&lt;div class="p-d-flex p-ai-center country-item"&gt;
&lt;img src="../../assets/images/flag_placeholder.png" :class="'flag flag-' + slotProps.item.code.toLowerCase()" width="18" /&gt;
&lt;div&gt;{{slotProps.item.label}}&lt;/div&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;/AutoComplete&gt;
&lt;h5&gt;Dropdown, Templating and Force Selection&lt;/h5&gt;
&lt;AutoComplete v-model="selectedCountry2" :suggestions="filteredCountries" @complete="searchCountry($event)" :dropdown="true" field="name" forceSelection&gt;
&lt;template #item="slotProps"&gt;
@ -304,6 +406,7 @@ export default {
<pre v-code.script><code>
import CountryService from '../../service/CountryService';
import {FilterService,FilterMatchMode} from 'primevue/api';
export default {
data() {
@ -311,8 +414,37 @@ export default {
countries: null,
selectedCountry1: null,
selectedCountry2: null,
selectedCity: null,
filteredCities: null,
filteredCountries: null,
selectedCountries: []
selectedCountries: [],
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'}
]
}]
}
},
countryService: null,
@ -334,6 +466,19 @@ export default {
});
}
}, 250);
},
searchCity(event) {
let query = event.query;
let filteredCities = [];
for (let country of this.groupedCities) {
let filteredItems = FilterService.filter(country.items, ['label'], query, FilterMatchMode.CONTAINS);
if (filteredItems && filteredItems.length) {
filteredCities.push({...country, ...{items: filteredItems}});
}
}
this.filteredCities = filteredCities;
}
}
}
@ -358,11 +503,21 @@ export default {
<h5>Basic</h5>
<AutoComplete v-model="selectedCountry1" :suggestions="filteredCountries" @complete="searchCountry($event)" field="name" />
<h5>Grouped</h5>
<AutoComplete v-model="selectedCity" :suggestions="filteredCities" @complete="searchCity($event)" field="label" optionGroupLabel="label" optionGroupChildren="items">
<template #optiongroup="slotProps">
<div class="p-d-flex p-ai-center country-item">
<img src="../../assets/images/flag_placeholder.png" :class="'flag flag-' + slotProps.item.code.toLowerCase()" width="18" />
<div>{{slotProps.item.label}}</div>
</div>
</template>
</AutoComplete>
<h5>Dropdown, Templating and Force Selection</h5>
<AutoComplete v-model="selectedCountry2" :suggestions="filteredCountries" @complete="searchCountry($event)" :dropdown="true" field="name" forceSelection>
<template #item="slotProps">
<div class="country-item">
<img src="https://www.primefaces.org/wp-content/uploads/2020/05/placeholder.png" />
<img src="../../assets/images/flag_placeholder.png" :class="'flag flag-' + slotProps.item.code.toLowerCase()" />
<div>{{slotProps.item.name}}</div>
</div>
</template>
@ -378,15 +533,46 @@ export default {
</template>
<script>
import CountryService from '../service/CountryService';
import CountryService from '../../service/CountryService';
import {FilterService,FilterMatchMode} from 'primevue/api';
export default {
data() {
return {
countries: null,
selectedCountry1: null,
selectedCountry2: null,
selectedCity: null,
filteredCities: null,
filteredCountries: null,
selectedCountries: []
selectedCountries: [],
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'}
]
}]
}
},
countryService: null,
@ -408,6 +594,19 @@ export default {
});
}
}, 250);
},
searchCity(event) {
let query = event.query;
let filteredCities = [];
for (let country of this.groupedCities) {
let filteredItems = FilterService.filter(country.items, ['label'], query, FilterMatchMode.CONTAINS);
if (filteredItems && filteredItems.length) {
filteredCities.push({...country, ...{items: filteredItems}});
}
}
this.filteredCities = filteredCities;
}
}
}`,

View File

@ -100,14 +100,12 @@ export default {
In addition <i>optiongroup</i>, <i>header</i>, <i>footer</i>, <i>emptyfilter</i> and <i>empty</i> slots are provided for further customization.</p>
<pre v-code><code><template v-pre>
&lt;Listbox v-model="selectedCars" :options="cars" :multiple="true" :filter="true" optionLabel="brand" listStyle="max-height:250px" style="width:15em"&gt;
&lt;template #header&gt;&lt;/template&gt;
&lt;template #option="slotProps"&gt;
&lt;div&gt;
&lt;img :alt="slotProps.option.brand" :src="'demo/images/car/' + slotProps.option.brand + '.png'" /&gt;
&lt;span&gt;{{slotProps.option.brand}}&lt;/span&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;template #footer&gt;&lt;/footer&gt;
&lt;/Listbox&gt;
</template>
</code></pre>