Multiple Selection and Keyboard support for AutoComplete
parent
8cdb74834a
commit
e5a54cfbed
|
@ -423,6 +423,12 @@ body .p-dropdown:not(.p-disabled).p-focus {
|
|||
body .p-dropdown .p-dropdown-label {
|
||||
padding-right: 2em;
|
||||
}
|
||||
body .p-dropdown .p-dropdown-label:focus {
|
||||
outline: 0 none;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
body .p-dropdown .p-dropdown-trigger {
|
||||
background-color: #585858;
|
||||
width: 2em;
|
||||
|
@ -702,12 +708,6 @@ body .p-listbox .p-listbox-footer {
|
|||
body .p-editor-container .p-editor-toolbar {
|
||||
border: 1px solid #191919;
|
||||
background-color: #191919;
|
||||
-moz-border-radius-topleft: 3px;
|
||||
-webkit-border-top-left-radius: 3px;
|
||||
border-top-left-radius: 3px;
|
||||
-moz-border-radius-topright: 3px;
|
||||
-webkit-border-top-right-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
body .p-editor-container .p-editor-content {
|
||||
border: 1px solid #191919;
|
||||
|
@ -737,7 +737,7 @@ body .p-editor-container .ql-snow.ql-toolbar .ql-picker-item:hover,
|
|||
body .p-editor-container .ql-snow .ql-toolbar .ql-picker-item:hover,
|
||||
body .p-editor-container .ql-snow.ql-toolbar .ql-picker-item.ql-selected,
|
||||
body .p-editor-container .ql-snow .ql-toolbar .ql-picker-item.ql-selected {
|
||||
color: #dedede;
|
||||
color: #FFB300;
|
||||
}
|
||||
body .p-editor-container .ql-snow.ql-toolbar button:hover .ql-stroke,
|
||||
body .p-editor-container .ql-snow .ql-toolbar button:hover .ql-stroke,
|
||||
|
@ -763,7 +763,7 @@ body .p-editor-container .ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-mi
|
|||
body .p-editor-container .ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,
|
||||
body .p-editor-container .ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,
|
||||
body .p-editor-container .ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter {
|
||||
stroke: #dedede;
|
||||
stroke: #FFB300;
|
||||
}
|
||||
body .p-rating .p-rating-icon {
|
||||
font-size: 20px;
|
||||
|
@ -3670,9 +3670,14 @@ body .p-megamenu.p-megamenu-vertical .p-megamenu-root-list > .p-menuitem > .p-me
|
|||
body .p-panelmenu .p-icon {
|
||||
position: static;
|
||||
}
|
||||
body .p-panelmenu .p-panelmenu-panel .p-panelmenu-header {
|
||||
margin-top: 2px;
|
||||
}
|
||||
body .p-panelmenu .p-panelmenu-panel:first-child .p-panelmenu-header {
|
||||
margin-top: 0;
|
||||
}
|
||||
body .p-panelmenu .p-panelmenu-header {
|
||||
padding: 0;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
body .p-panelmenu .p-panelmenu-header > a {
|
||||
border: 1px solid #191919;
|
||||
|
@ -3742,9 +3747,6 @@ body .p-panelmenu .p-panelmenu-header.p-highlight > a:hover {
|
|||
body .p-panelmenu .p-panelmenu-header.p-highlight > a:hover .p-panelmenu-icon {
|
||||
color: #212121;
|
||||
}
|
||||
body .p-panelmenu .p-panelmenu-panel {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
body .p-panelmenu .p-panelmenu-panel:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -41,9 +41,9 @@
|
|||
}
|
||||
|
||||
.p-autocomplete-panel .p-autocomplete-list {
|
||||
border: 0 none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0 none;
|
||||
}
|
||||
|
||||
.p-autocomplete-panel .p-autocomplete-list-item {
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
<template>
|
||||
<span :class="containerClass">
|
||||
<input ref="input" :class="inputClass" v-bind="$attrs" v-on="listeners" :value="inputValue" type="text" autoComplete="off">
|
||||
<i className="p-autocomplete-loader pi pi-spinner pi-spin" v-show="searching"></i>
|
||||
<input ref="input" :class="inputClass" v-bind="$attrs" v-on="listeners" :value="inputValue" type="text" autoComplete="off" v-if="!multiple">
|
||||
<ul ref="multiContainer" :class="multiContainerClass" v-if="multiple" @click="onMultiContainerClick">
|
||||
<li v-for="(item, i) of value" :key="i" class="p-autocomplete-token p-highlight">
|
||||
<span class="p-autocomplete-token-icon pi pi-fw pi-times" @click="removeItem($event, i)"></span>
|
||||
<span class="p-autocomplete-token-label">{{getItemContent(item)}}</span>
|
||||
</li>
|
||||
<li class="p-autocomplete-input-token">
|
||||
<input ref="input" type="text" autoComplete="off" :disabled="disabled" v-bind="$attrs" v-on="listeners">
|
||||
</li>
|
||||
</ul>
|
||||
<i class="p-autocomplete-loader pi pi-spinner pi-spin" v-show="searching"></i>
|
||||
<Button ref="dropdownButton" type="button" icon="pi pi-fw pi-chevron-down" class="p-autocomplete-dropdown" :disabled="disabled" @click="onDropdownClick" v-if="dropdown"/>
|
||||
<transition name="p-input-overlay" @enter="onOverlayEnter" @leave="onOverlayLeave">
|
||||
<div ref="overlay" class="p-autocomplete-panel" :style="{'max-height': scrollHeight}" v-if="overlayVisible">
|
||||
<ul class="p-autocomplete-items p-autocomplete-list p-component">
|
||||
<li v-for="(item, i) of suggestions" class="p-autocomplete-list-item" :key="i" @click="onItemClick($event, item)">
|
||||
<li v-for="(item, i) of suggestions" class="p-autocomplete-list-item" :key="i" @click="selectItem($event, item)">
|
||||
<slot name="item" :item="item" :index="i">
|
||||
{{getItemContent(item)}}
|
||||
</slot>
|
||||
|
@ -59,7 +68,7 @@ export default {
|
|||
};
|
||||
},
|
||||
watch: {
|
||||
suggestions() {
|
||||
suggestions() {
|
||||
if (this.searching) {
|
||||
|
||||
if (this.suggestions && this.suggestions.length)
|
||||
|
@ -81,7 +90,10 @@ export default {
|
|||
this.unbindOutsideClickListener();
|
||||
},
|
||||
alignOverlay() {
|
||||
DomHandler.relativePosition(this.$refs.overlay, this.$refs.input);
|
||||
if (this.multiple)
|
||||
DomHandler.relativePosition(this.$refs.overlay, this.$refs.multiContainer);
|
||||
else
|
||||
DomHandler.relativePosition(this.$refs.overlay, this.$refs.input);
|
||||
},
|
||||
bindOutsideClickListener() {
|
||||
if (!this.outsideClickListener) {
|
||||
|
@ -94,7 +106,13 @@ export default {
|
|||
}
|
||||
},
|
||||
isOutsideClicked(event) {
|
||||
return !this.$refs.overlay.contains(event.target) && event.target !== this.getInputElement() && !this.isDropdownClicked(event);
|
||||
return !this.$refs.overlay.contains(event.target) && !this.isInputClicked(event) && !this.isDropdownClicked(event);
|
||||
},
|
||||
isInputClicked(event) {
|
||||
if (this.multiple)
|
||||
return event.target === this.$refs.multiContainer || this.$refs.multiContainer.contains(event.target);
|
||||
else
|
||||
return event.target === this.$refs.input;
|
||||
},
|
||||
isDropdownClicked(event) {
|
||||
return this.$refs.dropdownButton ? (event.target === this.$refs.dropdownButton || this.$refs.dropdownButton.$el.contains(event.target)) : false;
|
||||
|
@ -105,13 +123,13 @@ export default {
|
|||
this.outsideClickListener = null;
|
||||
}
|
||||
},
|
||||
onItemClick(event, item) {
|
||||
selectItem(event, item) {
|
||||
if (this.multiple) {
|
||||
/*this.inputEl.value = '';
|
||||
if (!this.isSelected(option)) {
|
||||
let newValue = this.props.value ? [...this.props.value, option] : [option];
|
||||
this.updateModel(event, newValue);
|
||||
}*/
|
||||
this.$refs.input.value = '';
|
||||
if (!this.isSelected(item)) {
|
||||
let newValue = this.value ? [...this.value, item] : [item];
|
||||
this.$emit('input', newValue);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.$emit('input', item);
|
||||
|
@ -125,6 +143,18 @@ export default {
|
|||
this.focus();
|
||||
this.hideOverlay();
|
||||
},
|
||||
onMultiContainerClick(event) {
|
||||
this.focus();
|
||||
},
|
||||
removeItem(event, index) {
|
||||
let removedValue = this.value[index];
|
||||
let newValue = this.value.filter((val, i) => (index !== i));
|
||||
this.$emit('input', newValue);
|
||||
this.$emit('unselect', {
|
||||
originalEvent: event,
|
||||
value: removedValue
|
||||
});
|
||||
},
|
||||
onDropdownClick(event) {
|
||||
this.focus();
|
||||
const query = this.getInputElement().value;
|
||||
|
@ -149,7 +179,7 @@ export default {
|
|||
this.overlayVisible = false;
|
||||
},
|
||||
focus() {
|
||||
this.getInputElement().focus();
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
search(event, query, source) {
|
||||
//allow empty string but not undefined or null
|
||||
|
@ -168,47 +198,148 @@ export default {
|
|||
query: query
|
||||
});
|
||||
},
|
||||
getInputElement() {
|
||||
return this.multiple ? this.$refs.inputMultiple : this.$refs.input;
|
||||
onInput(event) {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
let query = event.target.value;
|
||||
if (!this.multiple) {
|
||||
this.$emit('input', query);
|
||||
}
|
||||
|
||||
if (query.length === 0) {
|
||||
this.hideOverlay();
|
||||
this.$emit('clear');
|
||||
}
|
||||
else {
|
||||
if (query.length >= this.minLength) {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.search(event, query, 'input');
|
||||
}, this.delay);
|
||||
}
|
||||
else {
|
||||
this.hideOverlay();
|
||||
}
|
||||
}
|
||||
},
|
||||
onFocus() {
|
||||
this.focused = true;
|
||||
this.$emit('focus', event);
|
||||
},
|
||||
onBlur() {
|
||||
this.focused = false;
|
||||
this.$emit('blur', event);
|
||||
},
|
||||
onKeyDown(event) {
|
||||
if (this.overlayVisible) {
|
||||
let highlightItem = DomHandler.findSingle(this.$refs.overlay, 'li.p-highlight');
|
||||
|
||||
switch(event.which) {
|
||||
//down
|
||||
case 40:
|
||||
if (highlightItem) {
|
||||
let nextElement = highlightItem.nextElementSibling;
|
||||
if (nextElement) {
|
||||
DomHandler.addClass(nextElement, 'p-highlight');
|
||||
DomHandler.removeClass(highlightItem, 'p-highlight');
|
||||
DomHandler.scrollInView(this.$refs.overlay, nextElement);
|
||||
}
|
||||
}
|
||||
else {
|
||||
DomHandler.addClass(this.$refs.overlay.firstChild.firstChild, 'p-highlight');
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
//up
|
||||
case 38:
|
||||
if (highlightItem) {
|
||||
let previousElement = highlightItem.previousElementSibling;
|
||||
if (previousElement) {
|
||||
DomHandler.addClass(previousElement, 'p-highlight');
|
||||
DomHandler.removeClass(highlightItem, 'p-highlight');
|
||||
DomHandler.scrollInView(this.$refs.overlay, previousElement);
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
//enter,tab
|
||||
case 13:
|
||||
if (highlightItem) {
|
||||
this.selectItem(event, this.suggestions[DomHandler.index(highlightItem)]);
|
||||
this.hideOverlay();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
//escape
|
||||
case 27:
|
||||
this.hideOverlay();
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
//tab
|
||||
case 9:
|
||||
if (highlightItem) {
|
||||
this.selectItem(event, this.suggestions[DomHandler.index(highlightItem)]);
|
||||
}
|
||||
|
||||
this.hideOverlay();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.multiple) {
|
||||
switch(event.which) {
|
||||
//backspace
|
||||
case 8:
|
||||
if (this.value && this.value.length && !this.$refs.input.value) {
|
||||
let removedValue = this.value[this.value.length - 1];
|
||||
let newValue = this.value.slice(0, -1);
|
||||
|
||||
this.$emit('input', newValue);
|
||||
this.$emit('unselect', {
|
||||
originalEvent: event,
|
||||
value: removedValue
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
isSelected(val) {
|
||||
let selected = false;
|
||||
if (this.value && this.value.length) {
|
||||
for (let i = 0; i < this.value.length; i++) {
|
||||
if (ObjectUtils.equals(this.value[i], val)) {
|
||||
selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
listeners() {
|
||||
return {
|
||||
...this.$listeners,
|
||||
input: event => {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
let query = event.target.value;
|
||||
if (!this.multiple) {
|
||||
this.$emit('input', query);
|
||||
}
|
||||
|
||||
if (query.length === 0) {
|
||||
this.hideOverlay();
|
||||
this.$emit('clear');
|
||||
}
|
||||
else {
|
||||
if (query.length >= this.minLength) {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.search(event, query, 'input');
|
||||
}, this.delay);
|
||||
}
|
||||
else {
|
||||
this.hidePanel();
|
||||
}
|
||||
}
|
||||
},
|
||||
focus: event => {
|
||||
this.focused = true;
|
||||
this.$emit('focus', event);
|
||||
},
|
||||
blur: event => {
|
||||
this.focused = false;
|
||||
this.$emit('blur', event);
|
||||
}
|
||||
input: this.onInput,
|
||||
focus: this.onFocus,
|
||||
blur: this.onBlur,
|
||||
keydown: this.onKeyDown
|
||||
};
|
||||
},
|
||||
containerClass() {
|
||||
|
@ -225,6 +356,12 @@ export default {
|
|||
'p-disabled': this.disabled
|
||||
}];
|
||||
},
|
||||
multiContainerClass() {
|
||||
return ['p-autocomplete-multiple-container p-component p-inputtext', {
|
||||
'p-disabled': this.disabled,
|
||||
'p-focus': this.focused
|
||||
}];
|
||||
},
|
||||
inputValue() {
|
||||
if (this.value) {
|
||||
if (this.field) {
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
|
||||
<div class="content-section implementation">
|
||||
<h3 class="first">Basic</h3>
|
||||
<AutoComplete v-model="country" :suggestions="filteredCountriesBasic" @complete="searchCountryBasic($event)" field="name" />
|
||||
<span style="marginLeft: .5em">Country: {{country || 'none'}}</span>
|
||||
<AutoComplete v-model="selectedCountry" :suggestions="filteredCountriesBasic" @complete="searchCountryBasic($event)" field="name" />
|
||||
<span style="marginLeft: .5em">Country: {{selectedCountry || 'none'}}</span>
|
||||
|
||||
<h3>Dropdown and Templating</h3>
|
||||
<AutoComplete v-model="brand" :suggestions="filteredBrands" @complete="searchBrand($event)" placeholder="Hint: type 'v' or 'f'" :dropdown="true">
|
||||
|
@ -22,6 +22,14 @@
|
|||
</template>
|
||||
</AutoComplete>
|
||||
<span style="marginLeft: .5em">Brand: {{brand || 'none'}}</span>
|
||||
|
||||
<h3>Multiple</h3>
|
||||
<span class="p-fluid">
|
||||
<AutoComplete :multiple="true" v-model="selectedCountries" :suggestions="filteredCountriesMultiple" @complete="searchCountryMultiple($event)" field="name" />
|
||||
</span>
|
||||
<ul>
|
||||
<li v-for="(c,i) of selectedCountries" :key="i">{{c}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -33,8 +41,10 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
countries: null,
|
||||
country: null,
|
||||
selectedCountry: null,
|
||||
filteredCountriesBasic: null,
|
||||
selectedCountries: [],
|
||||
filteredCountriesMultiple: null,
|
||||
brands: null,
|
||||
brand: null,
|
||||
filteredBrands: null
|
||||
|
@ -49,12 +59,19 @@ export default {
|
|||
this.brands = ['Audi', 'BMW', 'Fiat', 'Ford', 'Honda', 'Jaguar', 'Mercedes', 'Renault', 'Volvo'];
|
||||
},
|
||||
methods: {
|
||||
searchCountry(query) {
|
||||
return this.countries.filter((country) => {
|
||||
return country.name.toLowerCase().startsWith(query.toLowerCase());
|
||||
});
|
||||
},
|
||||
searchCountryBasic(event) {
|
||||
setTimeout(() => {
|
||||
let results = this.countries.filter((country) => {
|
||||
return country.name.toLowerCase().startsWith(event.query.toLowerCase());
|
||||
});
|
||||
this.filteredCountriesBasic = results;
|
||||
this.filteredCountriesBasic = this.searchCountry(event.query);
|
||||
}, 250);
|
||||
},
|
||||
searchCountryMultiple(event) {
|
||||
setTimeout(() => {
|
||||
this.filteredCountriesMultiple = this.searchCountry(event.query);
|
||||
}, 250);
|
||||
},
|
||||
searchBrand(event) {
|
||||
|
|
Loading…
Reference in New Issue