Multiple Selection and Keyboard support for AutoComplete

pull/12/head
cagataycivici 2019-01-29 15:46:07 +03:00
parent 8cdb74834a
commit e5a54cfbed
11 changed files with 18963 additions and 22741 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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,6 +90,9 @@ export default {
this.unbindOutsideClickListener();
},
alignOverlay() {
if (this.multiple)
DomHandler.relativePosition(this.$refs.overlay, this.$refs.multiContainer);
else
DomHandler.relativePosition(this.$refs.overlay, this.$refs.input);
},
bindOutsideClickListener() {
@ -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,15 +198,7 @@ export default {
query: query
});
},
getInputElement() {
return this.multiple ? this.$refs.inputMultiple : this.$refs.input;
}
},
computed: {
listeners() {
return {
...this.$listeners,
input: event => {
onInput(event) {
if (this.timeout) {
clearTimeout(this.timeout);
}
@ -197,18 +219,127 @@ export default {
}, this.delay);
}
else {
this.hidePanel();
this.hideOverlay();
}
}
},
focus: event => {
onFocus() {
this.focused = true;
this.$emit('focus', event);
},
blur: 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: 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) {

View File

@ -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) {