2
0
mirror of https://github.com/tenrok/vue-select.git synced 2026-06-22 10:30:34 +03:00

Merge branch 'master' into customizable-text

This commit is contained in:
Jeff Sagal
2021-08-01 12:38:43 -07:00
9 changed files with 658 additions and 615 deletions
+1
View File
@@ -19,6 +19,7 @@ export default {
required: true,
},
height: {
type: [String, Number],
default: 250,
},
},
+4 -1
View File
@@ -1,6 +1,9 @@
<template>
<ul>
<li v-for="{ login, avatar_url, html_url, contributions } in contributors">
<li
v-for="{ login, avatar_url, html_url, contributions } in contributors"
:key="login"
>
<img :src="`${avatar_url}&s=75`" :alt="`${login}'s Avatar`" />
<div>
<a :href="html_url">@{{ login }}</a>
+1 -1
View File
@@ -4,7 +4,7 @@
<th>Name</th>
<th>Country</th>
</tr>
<tr v-for="person in people">
<tr v-for="person in people" :key="person.name">
<td>{{ person.name }}</td>
<td>
<v-select
+4 -4
View File
@@ -188,11 +188,11 @@
v-bind="configuration"
placeholder="country objects, using option scoped slots"
>
<template slot="selected-option" slot-scope="{ label, value }">
{{ label }} -- {{ value }}
<template slot="selected-option" slot-scope="option">
{{ option.label }} -- {{ option.value }}
</template>
<template slot="option" slot-scope="{ label, value }">
{{ label }} ({{ value }})
<template slot="option" slot-scope="option">
{{ option.label }} ({{ option.value }})
</template>
</v-select>
</div>
@@ -1,5 +1,6 @@
<template>
<v-select>
<!-- eslint-disable-next-line vue/no-unused-vars -->
<template #no-options="{ search, searching, loading }">
This is the no options slot.
</template>
@@ -1,4 +1,5 @@
<template>
<!-- eslint-disable vue/no-unused-vars -->
<v-select :options="books" label="title">
<template
#selected-option-container="{ option, deselect, multiple, disabled }"
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<ul>
<li v-for="{ createdAt, login, avatarUrl } in sponsors">
<li v-for="{ createdAt, login, avatarUrl } in sponsors" :key="login">
<img :src="avatarUrl + '&s=150'" :alt="`@${login}'s avatar`" />
<p>
<a :href="`https://github.com/${login}`">@{{ login }}</a> <br />
+202 -167
View File
@@ -8,12 +8,12 @@
<div
:id="`vs${uid}__combobox`"
ref="toggle"
@mousedown="toggleDropdown($event)"
class="vs__dropdown-toggle"
role="combobox"
:aria-expanded="dropdownOpen.toString()"
:aria-owns="`vs${uid}__listbox`"
:aria-label="i18n.search.ariaLabel"
aria-label="Search for option"
@mousedown="toggleDropdown($event)"
>
<div ref="selectedOptions" class="vs__selected-options">
<slot
@@ -33,15 +33,13 @@
</slot>
<button
v-if="multiple"
ref="deselectButtons"
:disabled="disabled"
@click="deselect(option)"
type="button"
class="vs__deselect"
:title="i18n.deselectButton.ariaLabel(getOptionLabel(option))"
:aria-label="
i18n.deselectButton.ariaLabel(getOptionLabel(option))
"
ref="deselectButtons"
:title="`Deselect ${getOptionLabel(option)}`"
:aria-label="`Deselect ${getOptionLabel(option)}`"
@click="deselect(option)"
>
<component :is="childComponents.Deselect" />
</button>
@@ -60,13 +58,13 @@
<div ref="actions" class="vs__actions">
<button
v-show="showClearButton"
ref="clearButton"
:disabled="disabled"
type="button"
@click="clearSelection"
class="vs__clear"
title="i18n.clearButton.ariaLabel"
aria-label="i18n.clearButton.ariaLabel"
ref="clearButton"
title="Clear Selected"
aria-label="Clear Selected"
@click="clearSelection"
>
<component :is="childComponents.Deselect" />
</button>
@@ -80,31 +78,29 @@
</slot>
<slot name="spinner" v-bind="scope.spinner">
<div v-show="mutableLoading" class="vs__spinner">
{{ i18n.spinner.text }}
</div>
<div v-show="mutableLoading" class="vs__spinner">Loading...</div>
</slot>
</div>
</div>
<transition :name="transition">
<ul
ref="dropdownMenu"
v-if="dropdownOpen"
:id="`vs${uid}__listbox`"
ref="dropdownMenu"
:key="`vs${uid}__listbox`"
v-append-to-body
class="vs__dropdown-menu"
role="listbox"
tabindex="-1"
@mousedown.prevent="onMousedown"
@mouseup="onMouseUp"
tabindex="-1"
v-append-to-body
>
<slot name="list-header" v-bind="scope.listHeader" />
<li
v-for="(option, index) in filteredOptions"
role="option"
:key="getOptionKey(option)"
:id="`vs${uid}__option-${index}`"
:key="getOptionKey(option)"
role="option"
class="vs__dropdown-option"
:class="{
'vs__dropdown-option--selected': isOptionSelected(option),
@@ -120,9 +116,9 @@
</slot>
</li>
<li v-if="filteredOptions.length === 0" class="vs__no-options">
<slot name="no-options" v-bind="scope.noOptions">{{
i18n.noOptions.text
}}</slot>
<slot name="no-options" v-bind="scope.noOptions"
>Sorry, no matching options.</slot
>
</li>
<slot name="list-footer" v-bind="scope.listFooter" />
</ul>
@@ -137,13 +133,10 @@
</div>
</template>
<script type="text/babel">
import {
ajax,
pointerScroll,
i18n,
pointer as typeAheadPointer,
} from '../mixins'
<script>
import pointerScroll from '../mixins/pointerScroll'
import typeAheadPointer from '../mixins/typeAheadPointer'
import ajax from '../mixins/ajax'
import childComponents from './childComponents'
import appendToBody from '../directives/appendToBody'
import sortAndStringify from '../utility/sortAndStringify'
@@ -155,9 +148,9 @@ import uniqueId from '../utility/uniqueId'
export default {
components: { ...childComponents },
directives: {appendToBody},
directives: { appendToBody },
mixins: [pointerScroll, typeAheadPointer, ajax, i18n],
mixins: [pointerScroll, typeAheadPointer, ajax],
props: {
/**
@@ -166,6 +159,7 @@ export default {
* using 'change' event using v-on
* @type {Object||String||null}
*/
// eslint-disable-next-line vue/require-default-prop,vue/require-prop-types
value: {},
/**
@@ -529,6 +523,7 @@ export default {
* @type {String}
* @default {null}
*/
// eslint-disable-next-line vue/require-default-prop
inputId: {
type: String,
},
@@ -582,6 +577,7 @@ export default {
* for the search input. Can be used to implement
* custom behaviour for key presses.
*/
mapKeydown: {
type: Function,
/**
@@ -653,6 +649,7 @@ export default {
open: false,
isComposing: false,
pushedTags: [],
// eslint-disable-next-line vue/no-reserved-keys
_value: [], // Internal value managed by Vue Select if no `value` prop is passed
}
},
@@ -663,26 +660,29 @@ export default {
* track the state of values internally.
* @return {boolean}
*/
isTrackingValues () {
return typeof this.value === 'undefined' || this.$options.propsData.hasOwnProperty('reduce');
isTrackingValues() {
return (
typeof this.value === 'undefined' ||
this.$options.propsData.hasOwnProperty('reduce')
)
},
/**
* The options that are currently selected.
* @return {Array}
*/
selectedValue () {
let value = this.value;
selectedValue() {
let value = this.value
if (this.isTrackingValues) {
// Vue select has to manage value internally
value = this.$data._value;
value = this.$data._value
}
if (value) {
return [].concat(value);
return [].concat(value)
}
return [];
return []
},
/**
@@ -692,61 +692,65 @@ export default {
*
* @return {Array}
*/
optionList () {
return this.options.concat(this.pushTags ? this.pushedTags : []);
optionList() {
return this.options.concat(this.pushTags ? this.pushedTags : [])
},
/**
* Find the search input DOM element.
* @returns {HTMLInputElement}
*/
searchEl () {
searchEl() {
return !!this.$scopedSlots['search']
? this.$refs.selectedOptions.querySelector(this.searchInputQuerySelector)
: this.$refs.search;
? this.$refs.selectedOptions.querySelector(
this.searchInputQuerySelector
)
: this.$refs.search
},
/**
* The object to be bound to the $slots.search scoped slot.
* @returns {Object}
*/
scope () {
scope() {
const listSlot = {
search: this.search,
loading: this.loading,
searching: this.searching,
filteredOptions: this.filteredOptions
};
filteredOptions: this.filteredOptions,
}
return {
search: {
attributes: {
'disabled': this.disabled,
'placeholder': this.searchPlaceholder,
'tabindex': this.tabindex,
'readonly': !this.searchable,
'id': this.inputId,
disabled: this.disabled,
placeholder: this.searchPlaceholder,
tabindex: this.tabindex,
readonly: !this.searchable,
id: this.inputId,
'aria-autocomplete': 'list',
'aria-labelledby': `vs${this.uid}__combobox`,
'aria-controls': `vs${this.uid}__listbox`,
'ref': 'search',
'type': 'search',
'autocomplete': this.autocomplete,
'value': this.search,
...(this.dropdownOpen && this.filteredOptions[this.typeAheadPointer] ? {
'aria-activedescendant': `vs${this.uid}__option-${this.typeAheadPointer}`
} : {}),
ref: 'search',
type: 'search',
autocomplete: this.autocomplete,
value: this.search,
...(this.dropdownOpen && this.filteredOptions[this.typeAheadPointer]
? {
'aria-activedescendant': `vs${this.uid}__option-${this.typeAheadPointer}`,
}
: {}),
},
events: {
'compositionstart': () => this.isComposing = true,
'compositionend': () => this.isComposing = false,
'keydown': this.onSearchKeyDown,
'blur': this.onSearchBlur,
'focus': this.onSearchFocus,
'input': (e) => this.search = e.target.value,
compositionstart: () => (this.isComposing = true),
compositionend: () => (this.isComposing = false),
keydown: this.onSearchKeyDown,
blur: this.onSearchBlur,
focus: this.onSearchFocus,
input: (e) => (this.search = e.target.value),
},
},
spinner: {
loading: this.mutableLoading
loading: this.mutableLoading,
},
noOptions: {
search: this.search,
@@ -755,16 +759,16 @@ export default {
},
openIndicator: {
attributes: {
'ref': 'openIndicator',
'role': 'presentation',
'class': 'vs__open-indicator',
ref: 'openIndicator',
role: 'presentation',
class: 'vs__open-indicator',
},
},
listHeader: listSlot,
listFooter: listSlot,
header: { ...listSlot, deselect: this.deselect },
footer: { ...listSlot, deselect: this.deselect }
};
footer: { ...listSlot, deselect: this.deselect },
}
},
/**
@@ -774,11 +778,11 @@ export default {
*
* @return {Object}
*/
childComponents () {
childComponents() {
return {
...childComponents,
...this.components
};
...this.components,
}
},
/**
@@ -793,7 +797,7 @@ export default {
'vs--searchable': this.searchable && !this.noDrop,
'vs--unsearchable': !this.searchable,
'vs--loading': this.mutableLoading,
'vs--disabled': this.disabled
'vs--disabled': this.disabled,
}
},
@@ -803,7 +807,7 @@ export default {
* @return {Boolean} True if non empty value
*/
searching() {
return !! this.search
return !!this.search
},
/**
@@ -812,7 +816,7 @@ export default {
* @return {Boolean} True if open
*/
dropdownOpen() {
return this.dropdownShouldOpen(this);
return this.dropdownShouldOpen(this)
},
/**
@@ -821,9 +825,9 @@ export default {
* @return {String} Placeholder text
*/
searchPlaceholder() {
if (this.isValueEmpty && this.placeholder) {
return this.placeholder;
}
return this.isValueEmpty && this.placeholder
? this.placeholder
: undefined
},
/**
@@ -835,20 +839,22 @@ export default {
* @return {array}
*/
filteredOptions() {
const optionList = [].concat(this.optionList);
const optionList = [].concat(this.optionList)
if (!this.filterable && !this.taggable) {
return optionList;
return optionList
}
let options = this.search.length ? this.filter(optionList, this.search, this) : optionList;
let options = this.search.length
? this.filter(optionList, this.search, this)
: optionList
if (this.taggable && this.search.length) {
const createdOption = this.createOption(this.search);
const createdOption = this.createOption(this.search)
if (!this.optionExists(createdOption)) {
options.unshift(createdOption);
options.unshift(createdOption)
}
}
return options;
return options
},
/**
@@ -856,7 +862,7 @@ export default {
* @return {Boolean}
*/
isValueEmpty() {
return this.selectedValue.length === 0;
return this.selectedValue.length === 0
},
/**
@@ -864,7 +870,9 @@ export default {
* @return {Boolean}
*/
showClearButton() {
return !this.multiple && this.clearable && !this.open && !this.isValueEmpty
return (
!this.multiple && this.clearable && !this.open && !this.isValueEmpty
)
},
},
@@ -876,17 +884,22 @@ export default {
* is correct.
* @return {[type]} [description]
*/
options (newOptions, oldOptions) {
let shouldReset = () => typeof this.resetOnOptionsChange === 'function'
? this.resetOnOptionsChange(newOptions, oldOptions, this.selectedValue)
: this.resetOnOptionsChange;
options(newOptions, oldOptions) {
let shouldReset = () =>
typeof this.resetOnOptionsChange === 'function'
? this.resetOnOptionsChange(
newOptions,
oldOptions,
this.selectedValue
)
: this.resetOnOptionsChange
if (!this.taggable && shouldReset()) {
this.clearSelection();
this.clearSelection()
}
if (this.value && this.isTrackingValues) {
this.setInternalValueFromOptions(this.value);
this.setInternalValueFromOptions(this.value)
}
},
@@ -903,7 +916,6 @@ export default {
/**
* Always reset the value when
* the multiple prop changes.
* @param {Boolean} isMultiple
* @return {void}
*/
multiple() {
@@ -911,14 +923,14 @@ export default {
},
open(isOpen) {
this.$emit(isOpen ? 'open' : 'close');
}
this.$emit(isOpen ? 'open' : 'close')
},
},
created() {
this.mutableLoading = this.loading;
this.mutableLoading = this.loading
if (typeof this.value !== "undefined" && this.isTrackingValues) {
if (typeof this.value !== 'undefined' && this.isTrackingValues) {
this.setInternalValueFromOptions(this.value)
}
@@ -934,9 +946,11 @@ export default {
*/
setInternalValueFromOptions(value) {
if (Array.isArray(value)) {
this.$data._value = value.map(val => this.findOptionFromReducedValue(val));
this.$data._value = value.map((val) =>
this.findOptionFromReducedValue(val)
)
} else {
this.$data._value = this.findOptionFromReducedValue(value);
this.$data._value = this.findOptionFromReducedValue(value)
}
},
@@ -946,16 +960,16 @@ export default {
* @return {void}
*/
select(option) {
this.$emit('option:selecting', option);
this.$emit('option:selecting', option)
if (!this.isOptionSelected(option)) {
if (this.taggable && !this.optionExists(option)) {
this.$emit('option:created', option);
this.$emit('option:created', option)
}
if (this.multiple) {
option = this.selectedValue.concat(option)
}
this.updateValue(option);
this.$emit('option:selected', option);
this.updateValue(option)
this.$emit('option:selected', option)
}
this.onAfterSelect(option)
},
@@ -965,12 +979,14 @@ export default {
* @param {Object|String} option
* @return {void}
*/
deselect (option) {
this.$emit('option:deselecting', option);
this.updateValue(this.selectedValue.filter(val => {
return !this.optionComparator(val, option);
}));
this.$emit('option:deselected', option);
deselect(option) {
this.$emit('option:deselecting', option)
this.updateValue(
this.selectedValue.filter((val) => {
return !this.optionComparator(val, option)
})
)
this.$emit('option:deselected', option)
},
/**
@@ -988,7 +1004,7 @@ export default {
*/
onAfterSelect(option) {
if (this.closeOnSelect) {
this.open = !this.open;
this.open = !this.open
this.searchEl.blur()
}
@@ -1005,21 +1021,21 @@ export default {
* @emits input
* @param value
*/
updateValue (value) {
updateValue(value) {
if (typeof this.value === 'undefined') {
// Vue select has to manage value
this.$data._value = value;
this.$data._value = value
}
if (value !== null) {
if (Array.isArray(value)) {
value = value.map(val => this.reduce(val));
value = value.map((val) => this.reduce(val))
} else {
value = this.reduce(value);
value = this.reduce(value)
}
}
this.$emit('input', value);
this.$emit('input', value)
},
/**
@@ -1027,10 +1043,10 @@ export default {
* @param {Event} event
* @return {void}
*/
toggleDropdown (event) {
const targetIsNotSearch = event.target !== this.searchEl;
toggleDropdown(event) {
const targetIsNotSearch = event.target !== this.searchEl
if (targetIsNotSearch) {
event.preventDefault();
event.preventDefault()
}
// don't react to click on deselect/clear buttons,
@@ -1038,18 +1054,23 @@ export default {
const ignoredButtons = [
...(this.$refs['deselectButtons'] || []),
...([this.$refs['clearButton']] || []),
];
]
if (this.searchEl === undefined || ignoredButtons.filter(Boolean).some(ref => ref.contains(event.target) || ref === event.target)) {
event.preventDefault();
return;
if (
this.searchEl === undefined ||
ignoredButtons
.filter(Boolean)
.some((ref) => ref.contains(event.target) || ref === event.target)
) {
event.preventDefault()
return
}
if (this.open && targetIsNotSearch) {
this.searchEl.blur();
this.searchEl.blur()
} else if (!this.disabled) {
this.open = true;
this.searchEl.focus();
this.open = true
this.searchEl.focus()
}
},
@@ -1059,7 +1080,9 @@ export default {
* @return {Boolean} True when selected | False otherwise
*/
isOptionSelected(option) {
return this.selectedValue.some(value => this.optionComparator(value, option))
return this.selectedValue.some((value) =>
this.optionComparator(value, option)
)
},
/**
@@ -1070,7 +1093,7 @@ export default {
* @returns {boolean}
*/
optionComparator(a, b) {
return this.getOptionKey(a) === this.getOptionKey(b);
return this.getOptionKey(a) === this.getOptionKey(b)
},
/**
@@ -1081,16 +1104,14 @@ export default {
* @param value {Object}
* @returns {*}
*/
findOptionFromReducedValue (value) {
const predicate = option => JSON.stringify(this.reduce(option)) === JSON.stringify(value);
findOptionFromReducedValue(value) {
const predicate = (option) =>
JSON.stringify(this.reduce(option)) === JSON.stringify(value)
const matches = [
...this.options,
...this.pushedTags,
].filter(predicate);
const matches = [...this.options, ...this.pushedTags].filter(predicate)
if (matches.length === 1) {
return matches[0];
return matches[0]
}
/**
@@ -1099,7 +1120,11 @@ export default {
* unique reduced value.
* @see https://github.com/sagalbot/vue-select/issues/1089#issuecomment-597238735
*/
return matches.find(match => this.optionComparator(match, this.$data._value)) || value;
return (
matches.find((match) =>
this.optionComparator(match, this.$data._value)
) || value
)
},
/**
@@ -1107,7 +1132,7 @@ export default {
* @emits {search:blur}
* @returns {void}
*/
closeSearchOptions(){
closeSearchOptions() {
this.open = false
this.$emit('search:blur')
},
@@ -1118,10 +1143,17 @@ export default {
* @return {this.value}
*/
maybeDeleteValue() {
if (!this.searchEl.value.length && this.selectedValue && this.selectedValue.length && this.clearable) {
let value = null;
if (
!this.searchEl.value.length &&
this.selectedValue &&
this.selectedValue.length &&
this.clearable
) {
let value = null
if (this.multiple) {
value = [...this.selectedValue.slice(0, this.selectedValue.length - 1)]
value = [
...this.selectedValue.slice(0, this.selectedValue.length - 1),
]
}
this.updateValue(value)
}
@@ -1135,7 +1167,9 @@ export default {
* @return {boolean}
*/
optionExists(option) {
return this.optionList.some(_option => this.optionComparator(_option, option))
return this.optionList.some((_option) =>
this.optionComparator(_option, option)
)
},
/**
@@ -1144,8 +1178,8 @@ export default {
* @param option
* @return {*}
*/
normalizeOptionForSlot (option) {
return (typeof option === 'object') ? option : {[this.label]: option};
normalizeOptionForSlot(option) {
return typeof option === 'object' ? option : { [this.label]: option }
},
/**
@@ -1155,8 +1189,8 @@ export default {
* @param {Object || String} option
* @return {void}
*/
pushTag (option) {
this.pushedTags.push(option);
pushTag(option) {
this.pushedTags.push(option)
},
/**
@@ -1181,7 +1215,7 @@ export default {
if (this.mousedown && !this.searching) {
this.mousedown = false
} else {
const { clearSearchOnSelect, multiple } = this;
const { clearSearchOnSelect, multiple } = this
if (this.clearSearchOnBlur({ clearSearchOnSelect, multiple })) {
this.search = ''
}
@@ -1189,7 +1223,7 @@ export default {
return
}
// Fixed bug where no-options message could not be closed
if (this.search.length === 0 && this.options.length === 0){
if (this.search.length === 0 && this.options.length === 0) {
this.closeSearchOptions()
return
}
@@ -1231,38 +1265,39 @@ export default {
* @param e {KeyboardEvent}
* @return {Function}
*/
onSearchKeyDown (e) {
const preventAndSelect = e => {
e.preventDefault();
return !this.isComposing && this.typeAheadSelect();
};
onSearchKeyDown(e) {
const preventAndSelect = (e) => {
e.preventDefault()
return !this.isComposing && this.typeAheadSelect()
}
const defaults = {
// backspace
8: e => this.maybeDeleteValue(),
8: (e) => this.maybeDeleteValue(),
// tab
9: e => this.onTab(),
9: (e) => this.onTab(),
// esc
27: e => this.onEscape(),
27: (e) => this.onEscape(),
// up.prevent
38: e => {
e.preventDefault();
return this.typeAheadUp();
38: (e) => {
e.preventDefault()
return this.typeAheadUp()
},
// down.prevent
40: e => {
e.preventDefault();
return this.typeAheadDown();
40: (e) => {
e.preventDefault()
return this.typeAheadDown()
},
};
}
this.selectOnKeyCodes.forEach(keyCode => defaults[keyCode] = preventAndSelect);
this.selectOnKeyCodes.forEach(
(keyCode) => (defaults[keyCode] = preventAndSelect)
)
const handlers = this.mapKeydown(defaults, this);
const handlers = this.mapKeydown(defaults, this)
if (typeof handlers[e.keyCode] === 'function') {
return handlers[e.keyCode](e);
}
return handlers[e.keyCode](e)
}
},
},
+2
View File
@@ -7,8 +7,10 @@ export default {
left,
width,
} = context.$refs.toggle.getBoundingClientRect()
let scrollX = window.scrollX || window.pageXOffset
let scrollY = window.scrollY || window.pageYOffset
el.unbindPosition = context.calculatePosition(el, context, {
width: width + 'px',
left: scrollX + left + 'px',