mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-10 07:52:23 +03:00
1223 lines
33 KiB
Vue
1223 lines
33 KiB
Vue
<template>
|
||
<div :dir="dir" class="v-select" :class="stateClasses">
|
||
<div ref="toggle" @mousedown.prevent="toggleDropdown" class="vs__dropdown-toggle">
|
||
|
||
<div class="vs__selected-options" ref="selectedOptions">
|
||
<slot name="selected-option" v-for="selected in scopedValues" v-bind="selected">
|
||
<span :class="selected.bindings.class">
|
||
{{ selected.label }}
|
||
<component
|
||
ref="deselectButtons"
|
||
:is="selected.deselect.component"
|
||
v-if="selected.deselect.bindings.multiple"
|
||
v-bind="selected.deselect.bindings"
|
||
v-on="selected.deselect.events"
|
||
/>
|
||
</span>
|
||
</slot>
|
||
<slot name="search" v-bind="scope.search">
|
||
<input v-bind="scope.search.attributes" v-on="scope.search.events">
|
||
</slot>
|
||
</div>
|
||
|
||
<div class="vs__actions" ref="actions">
|
||
<slot name="clear">
|
||
<component
|
||
ref="clearButton"
|
||
:is="scope.clear.component"
|
||
v-bind="scope.clear.bindings"
|
||
v-on="scope.clear.events"
|
||
/>
|
||
</slot>
|
||
<slot name="open-indicator" v-bind="scope.openIndicator">
|
||
<component
|
||
v-if="scope.openIndicator.shouldDisplay"
|
||
:is="scope.openIndicator.component"
|
||
v-bind="scope.openIndicator.attributes"
|
||
/>
|
||
</slot>
|
||
<slot name="spinner" v-bind="scope.spinner">
|
||
<div :class="scope.spinner.bindings.class" v-show="mutableLoading">Loading...</div>
|
||
</slot>
|
||
</div>
|
||
</div>
|
||
|
||
<transition :name="transition">
|
||
<ul
|
||
ref="dropdownMenu"
|
||
v-if="dropdownOpen"
|
||
class="vs__dropdown-menu"
|
||
role="listbox"
|
||
@mousedown.prevent="onMousedown"
|
||
@mouseup="onMouseUp"
|
||
>
|
||
<slot name="option" v-for="option in scopedOptions" v-bind="option">
|
||
<li v-bind="option.attributes" v-on="option.events">{{ option.label }}</li>
|
||
</slot>
|
||
<li v-if="!filteredOptions.length" class="vs__no-options" @mousedown.stop="">
|
||
<slot name="no-options">Sorry, no matching options.</slot>
|
||
</li>
|
||
</ul>
|
||
</transition>
|
||
</div>
|
||
</template>
|
||
|
||
<script type="text/babel">
|
||
import pointerScroll from '../mixins/pointerScroll'
|
||
import typeAheadPointer from '../mixins/typeAheadPointer'
|
||
import ajax from '../mixins/ajax'
|
||
import childComponents from './childComponents';
|
||
|
||
const spreadableOptionProperties = (option) => typeof option === 'object' ? {...option} : {};
|
||
|
||
/**
|
||
* @name VueSelect
|
||
*/
|
||
export default {
|
||
components: {...childComponents},
|
||
|
||
mixins: [pointerScroll, typeAheadPointer, ajax],
|
||
|
||
props: {
|
||
/**
|
||
* Contains the currently selected value. Very similar to a
|
||
* `value` attribute on an <input>. You can listen for changes
|
||
* using 'change' event using v-on
|
||
* @type {Object||String||null}
|
||
*/
|
||
value: {},
|
||
|
||
/**
|
||
* An object with any custom components that you'd like to overwrite
|
||
* the default implementation of in your app. The keys in this object
|
||
* will be merged with the defaults.
|
||
* @see https://vue-select.org/guide/components.html
|
||
* @type {Function}
|
||
*/
|
||
components: {
|
||
type: Object,
|
||
default: () => ({}),
|
||
},
|
||
|
||
/**
|
||
* An array of strings or objects to be used as dropdown choices.
|
||
* If you are using an array of objects, vue-select will look for
|
||
* a `label` key (ex. [{label: 'This is Foo', value: 'foo'}]). A
|
||
* custom label key can be set with the `label` prop.
|
||
* @type {Array}
|
||
*/
|
||
options: {
|
||
type: Array,
|
||
default() {
|
||
return []
|
||
},
|
||
},
|
||
|
||
/**
|
||
* Disable the entire component.
|
||
* @type {Boolean}
|
||
*/
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
|
||
/**
|
||
* Can the user clear the selected property.
|
||
* @type {Boolean}
|
||
*/
|
||
clearable: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
|
||
/**
|
||
* Enable/disable filtering the options.
|
||
* @type {Boolean}
|
||
*/
|
||
searchable: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
|
||
/**
|
||
* Equivalent to the `multiple` attribute on a `<select>` input.
|
||
* @type {Boolean}
|
||
*/
|
||
multiple: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
|
||
/**
|
||
* Equivalent to the `placeholder` attribute on an `<input>`.
|
||
* @type {String}
|
||
*/
|
||
placeholder: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
|
||
/**
|
||
* Sets a Vue transition property on the `.vs__dropdown-menu`.
|
||
* @type {String}
|
||
*/
|
||
transition: {
|
||
type: String,
|
||
default: 'vs__fade'
|
||
},
|
||
|
||
/**
|
||
* Enables/disables clearing the search text when an option is selected.
|
||
* @type {Boolean}
|
||
*/
|
||
clearSearchOnSelect: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
|
||
/**
|
||
* Close a dropdown when an option is chosen. Set to false to keep the dropdown
|
||
* open (useful when combined with multi-select, for example)
|
||
* @type {Boolean}
|
||
*/
|
||
closeOnSelect: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
|
||
/**
|
||
* Tells vue-select what key to use when generating option
|
||
* labels when each `option` is an object.
|
||
* @type {String}
|
||
*/
|
||
label: {
|
||
type: String,
|
||
default: 'label'
|
||
},
|
||
|
||
/**
|
||
* Value of the 'autocomplete' field of the input
|
||
* element.
|
||
* @type {String}
|
||
*/
|
||
autocomplete: {
|
||
type: String,
|
||
default: 'off'
|
||
},
|
||
|
||
/**
|
||
* When working with objects, the reduce
|
||
* prop allows you to transform a given
|
||
* object to only the information you
|
||
* want passed to a v-model binding
|
||
* or @input event.
|
||
*/
|
||
reduce: {
|
||
type: Function,
|
||
default: option => option,
|
||
},
|
||
|
||
/**
|
||
* Decides whether an option is selectable or not. Not selectable options
|
||
* are displayed but disabled and cannot be selected.
|
||
*
|
||
* @type {Function}
|
||
* @since 3.3.0
|
||
* @param {Object|String} option
|
||
* @return {Boolean}
|
||
*/
|
||
selectable: {
|
||
type: Function,
|
||
default: option => true,
|
||
},
|
||
|
||
/**
|
||
* Callback to generate the label text. If {option}
|
||
* is an object, returns option[this.label] by default.
|
||
*
|
||
* Label text is used for filtering comparison and
|
||
* displaying. If you only need to adjust the
|
||
* display, you should use the `option` and
|
||
* `selected-option` slots.
|
||
*
|
||
* @type {Function}
|
||
* @param {Object || String} option
|
||
* @return {String}
|
||
*/
|
||
getOptionLabel: {
|
||
type: Function,
|
||
default(option) {
|
||
if (typeof option === 'object') {
|
||
if (!option.hasOwnProperty(this.label)) {
|
||
return console.warn(
|
||
`[vue-select warn]: Label key "option.${this.label}" does not` +
|
||
` exist in options object ${JSON.stringify(option)}.\n` +
|
||
'https://vue-select.org/api/props.html#getoptionlabel'
|
||
)
|
||
}
|
||
return option[this.label]
|
||
}
|
||
return option;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Callback to get an option key. If {option}
|
||
* is an object and has an {id}, returns {option.id}
|
||
* by default, otherwise tries to serialize {option}
|
||
* to JSON.
|
||
*
|
||
* The key must be unique for an option.
|
||
*
|
||
* @type {Function}
|
||
* @param {Object || String} option
|
||
* @return {String}
|
||
*/
|
||
getOptionKey: {
|
||
type: Function,
|
||
default (option) {
|
||
if (typeof option === 'object' && option.id) {
|
||
return option.id;
|
||
} else {
|
||
try {
|
||
return JSON.stringify(option);
|
||
} catch (e) {
|
||
return console.warn(
|
||
`[vue-select warn]: Could not stringify option ` +
|
||
`to generate unique key. Please provide'getOptionKey' prop ` +
|
||
`to return a unique key for each option.\n` +
|
||
'https://vue-select.org/api/props.html#getoptionkey'
|
||
);
|
||
}
|
||
}
|
||
},
|
||
},
|
||
|
||
getOptionScope: {
|
||
type: Function,
|
||
default (option, index) {
|
||
return {
|
||
...spreadableOptionProperties(option),
|
||
label: this.getOptionLabel(option),
|
||
attributes: {
|
||
key: this.getOptionKey(option),
|
||
class: {
|
||
'vs__dropdown-option': true,
|
||
'vs__dropdown-option--selected': this.isOptionSelected(option),
|
||
'vs__dropdown-option--highlight': index === this.typeAheadPointer,
|
||
'vs__dropdown-option--disabled': !this.selectable(option),
|
||
},
|
||
},
|
||
events: {
|
||
'mouseover': () => this.selectable(option) ? this.typeAheadPointer = index : null,
|
||
'mousedown': e => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
return this.selectable(option) ? this.select(option) : null;
|
||
},
|
||
},
|
||
};
|
||
},
|
||
},
|
||
|
||
getSelectedOptionScope: {
|
||
type: Function,
|
||
default (option, index) {
|
||
return {
|
||
...spreadableOptionProperties(option),
|
||
label: this.getOptionLabel(option),
|
||
deselect: this.getOptionDeselectScope(option),
|
||
bindings: {
|
||
key: this.getOptionKey(option),
|
||
option: this.normalizeOptionForSlot(option),
|
||
deselect: this.deselect,
|
||
multiple: this.multiple,
|
||
class: 'vs__selected',
|
||
},
|
||
events: {
|
||
'mouseover': () => this.selectable(option) ? this.typeAheadPointer = index : null,
|
||
'mousedown': e => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
return this.selectable(option) ? this.select(option) : null;
|
||
},
|
||
},
|
||
};
|
||
},
|
||
},
|
||
|
||
getOptionDeselectScope: {
|
||
type: Function,
|
||
default (option) {
|
||
return {
|
||
component: childComponents.Deselect,
|
||
bindings: {
|
||
'type': 'button',
|
||
'class': 'vs__deselect',
|
||
'aria-label': `Deselect ${this.getOptionLabel(option)}`,
|
||
'disabled': this.disabled,
|
||
'multiple': this.multiple,
|
||
'ref': 'deselectButtons'
|
||
},
|
||
events: {
|
||
'click': () => this.deselect(option),
|
||
},
|
||
};
|
||
},
|
||
},
|
||
|
||
/**
|
||
* Select the current value if selectOnTab is enabled
|
||
* @deprecated since 3.3
|
||
*/
|
||
onTab: {
|
||
type: Function,
|
||
default: function () {
|
||
if (this.selectOnTab && !this.isComposing) {
|
||
this.typeAheadSelect();
|
||
}
|
||
},
|
||
},
|
||
|
||
/**
|
||
* Enable/disable creating options from searchEl.
|
||
* @type {Boolean}
|
||
*/
|
||
taggable: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
|
||
/**
|
||
* Set the tabindex for the input field.
|
||
* @type {Number}
|
||
*/
|
||
tabindex: {
|
||
type: Number,
|
||
default: null
|
||
},
|
||
|
||
/**
|
||
* When true, newly created tags will be added to
|
||
* the options list.
|
||
* @type {Boolean}
|
||
*/
|
||
pushTags: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
|
||
/**
|
||
* When true, existing options will be filtered
|
||
* by the search text. Should not be used in conjunction
|
||
* with taggable.
|
||
* @type {Boolean}
|
||
*/
|
||
filterable: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
|
||
/**
|
||
* Callback to determine if the provided option should
|
||
* match the current search text. Used to determine
|
||
* if the option should be displayed.
|
||
* @type {Function}
|
||
* @param {Object || String} option
|
||
* @param {String} label
|
||
* @param {String} search
|
||
* @return {Boolean}
|
||
*/
|
||
filterBy: {
|
||
type: Function,
|
||
default(option, label, search) {
|
||
return (label || '').toLowerCase().indexOf(search.toLowerCase()) > -1
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Callback to filter results when search text
|
||
* is provided. Default implementation loops
|
||
* each option, and returns the result of
|
||
* this.filterBy.
|
||
* @type {Function}
|
||
* @param {Array} list of options
|
||
* @param {String} search text
|
||
* @param {Object} vSelect instance
|
||
* @return {Boolean}
|
||
*/
|
||
filter: {
|
||
"type": Function,
|
||
default(options, search) {
|
||
return options.filter((option) => {
|
||
let label = this.getOptionLabel(option)
|
||
if (typeof label === 'number') {
|
||
label = label.toString()
|
||
}
|
||
return this.filterBy(option, label, search)
|
||
});
|
||
}
|
||
},
|
||
|
||
/**
|
||
* User defined function for adding Options
|
||
* @type {Function}
|
||
*/
|
||
createOption: {
|
||
type: Function,
|
||
default (option) {
|
||
return (typeof this.optionList[0] === 'object') ? {[this.label]: option} : option;
|
||
},
|
||
},
|
||
|
||
/**
|
||
* When false, updating the options will not reset the selected value. Accepts
|
||
* a `boolean` or `function` that returns a `boolean`. If defined as a function,
|
||
* it will receive the params listed below.
|
||
*
|
||
* @since 3.4 - Type changed to {Boolean|Function}
|
||
*
|
||
* @type {Boolean|Function}
|
||
* @param {Array} newOptions
|
||
* @param {Array} oldOptions
|
||
* @param {Array} selectedValue
|
||
*/
|
||
resetOnOptionsChange: {
|
||
default: false,
|
||
validator: (value) => ['function', 'boolean'].includes(typeof value)
|
||
},
|
||
|
||
/**
|
||
* Disable the dropdown entirely.
|
||
* @type {Boolean}
|
||
*/
|
||
noDrop: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
|
||
/**
|
||
* Sets the id of the input element.
|
||
* @type {String}
|
||
* @default {null}
|
||
*/
|
||
inputId: {
|
||
type: String
|
||
},
|
||
|
||
/**
|
||
* Sets RTL support. Accepts 'ltr', 'rtl', 'auto'.
|
||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir
|
||
* @type {String}
|
||
* @default 'auto'
|
||
*/
|
||
dir: {
|
||
type: String,
|
||
default: 'auto'
|
||
},
|
||
|
||
/**
|
||
* When true, hitting the 'tab' key will select the current select value
|
||
* @type {Boolean}
|
||
* @deprecated since 3.3 - use selectOnKeyCodes instead
|
||
*/
|
||
selectOnTab: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
|
||
/**
|
||
* Keycodes that will select the current option.
|
||
* @type Array
|
||
*/
|
||
selectOnKeyCodes: {
|
||
type: Array,
|
||
default: () => [13],
|
||
},
|
||
|
||
/**
|
||
* Query Selector used to find the search input
|
||
* when the 'search' scoped slot is used.
|
||
*
|
||
* Must be a valid CSS selector string.
|
||
*
|
||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector
|
||
* @type {String}
|
||
*/
|
||
searchInputQuerySelector: {
|
||
type: String,
|
||
default: '[type=search]'
|
||
},
|
||
|
||
/**
|
||
* Used to modify the default keydown events map
|
||
* for the search input. Can be used to implement
|
||
* custom behaviour for key presses.
|
||
*/
|
||
mapKeydown: {
|
||
type: Function,
|
||
/**
|
||
* @param map {Object}
|
||
* @param vm {VueSelect}
|
||
* @return {Object}
|
||
*/
|
||
default: (map, vm) => map,
|
||
}
|
||
},
|
||
|
||
data() {
|
||
return {
|
||
search: '',
|
||
open: false,
|
||
isComposing: false,
|
||
pushedTags: [],
|
||
_value: [] // Internal value managed by Vue Select if no `value` prop is passed
|
||
}
|
||
},
|
||
|
||
watch: {
|
||
/**
|
||
* Maybe reset the value
|
||
* when options change.
|
||
* Make sure selected option
|
||
* is correct.
|
||
* @return {[type]} [description]
|
||
*/
|
||
options (newOptions, oldOptions) {
|
||
let shouldReset = () => typeof this.resetOnOptionsChange === 'function'
|
||
? this.resetOnOptionsChange(newOptions, oldOptions, this.selectedValue)
|
||
: this.resetOnOptionsChange;
|
||
|
||
if (!this.taggable && shouldReset()) {
|
||
this.clearSelection();
|
||
}
|
||
|
||
if (this.value && this.isTrackingValues) {
|
||
this.setInternalValueFromOptions(this.value);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Make sure to update internal
|
||
* value if prop changes outside
|
||
*/
|
||
value(val) {
|
||
if (this.isTrackingValues) {
|
||
this.setInternalValueFromOptions(val)
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Always reset the value when
|
||
* the multiple prop changes.
|
||
* @param {Boolean} isMultiple
|
||
* @return {void}
|
||
*/
|
||
multiple() {
|
||
this.clearSelection()
|
||
}
|
||
},
|
||
|
||
created() {
|
||
this.mutableLoading = this.loading;
|
||
|
||
if (typeof this.value !== "undefined" && this.isTrackingValues) {
|
||
this.setInternalValueFromOptions(this.value)
|
||
}
|
||
|
||
this.$on('option:created', this.maybePushTag)
|
||
},
|
||
|
||
methods: {
|
||
/**
|
||
* Make sure tracked value is
|
||
* one option if possible.
|
||
* @param {Object|String} value
|
||
* @return {void}
|
||
*/
|
||
setInternalValueFromOptions(value) {
|
||
if (Array.isArray(value)) {
|
||
this.$data._value = value.map(val => this.findOptionFromReducedValue(val));
|
||
} else {
|
||
this.$data._value = this.findOptionFromReducedValue(value);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Select a given option.
|
||
* @param {Object|String} option
|
||
* @return {void}
|
||
*/
|
||
select(option) {
|
||
if (!this.isOptionSelected(option)) {
|
||
if (this.taggable && !this.optionExists(option)) {
|
||
option = this.createOption(option);
|
||
this.$emit('option:created', option);
|
||
}
|
||
if (this.multiple) {
|
||
option = this.selectedValue.concat(option)
|
||
}
|
||
this.updateValue(option);
|
||
}
|
||
|
||
this.onAfterSelect(option)
|
||
},
|
||
|
||
/**
|
||
* De-select a given option.
|
||
* @param {Object|String} option
|
||
* @return {void}
|
||
*/
|
||
deselect (option) {
|
||
this.updateValue(this.selectedValue.filter(val => {
|
||
return !this.optionComparator(val, option);
|
||
}));
|
||
},
|
||
|
||
/**
|
||
* Clears the currently selected value(s)
|
||
* @return {void}
|
||
*/
|
||
clearSelection() {
|
||
this.updateValue(this.multiple ? [] : null)
|
||
},
|
||
|
||
/**
|
||
* Called from this.select after each selection.
|
||
* @param {Object|String} option
|
||
* @return {void}
|
||
*/
|
||
onAfterSelect(option) {
|
||
if (this.closeOnSelect) {
|
||
this.open = !this.open
|
||
this.searchEl.blur()
|
||
}
|
||
|
||
if (this.clearSearchOnSelect) {
|
||
this.search = ''
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Accepts a selected value, updates local
|
||
* state when required, and triggers the
|
||
* input event.
|
||
*
|
||
* @emits input
|
||
* @param value
|
||
*/
|
||
updateValue (value) {
|
||
if (this.isTrackingValues) {
|
||
// Vue select has to manage value
|
||
this.$data._value = value;
|
||
}
|
||
|
||
if (value !== null) {
|
||
if (Array.isArray(value)) {
|
||
value = value.map(val => this.reduce(val));
|
||
} else {
|
||
value = this.reduce(value);
|
||
}
|
||
}
|
||
|
||
this.$emit('input', value);
|
||
},
|
||
|
||
/**
|
||
* Toggle the visibility of the dropdown menu.
|
||
* @param {Event} e
|
||
* @return {void}
|
||
*/
|
||
toggleDropdown ({target}) {
|
||
// don't react to click on deselect/clear buttons,
|
||
// they dropdown state will be set in their click handlers
|
||
const ignoredButtons = [
|
||
...(this.$refs['deselectButtons'] || []),
|
||
...([this.$refs['clearButton']] || [])
|
||
];
|
||
|
||
if (ignoredButtons.some(ref => ref.contains(target) || ref === target)) {
|
||
return;
|
||
}
|
||
|
||
if (this.open) {
|
||
this.searchEl.blur();
|
||
} else if (!this.disabled) {
|
||
this.open = true;
|
||
this.searchEl.focus();
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Check if the given option is currently selected.
|
||
* @param {Object|String} option
|
||
* @return {Boolean} True when selected | False otherwise
|
||
*/
|
||
isOptionSelected(option) {
|
||
return this.selectedValue.some(value => {
|
||
return this.optionComparator(value, option)
|
||
})
|
||
},
|
||
|
||
/**
|
||
* Determine if two option objects are matching.
|
||
*
|
||
* @param value {Object}
|
||
* @param option {Object}
|
||
* @returns {boolean}
|
||
*/
|
||
optionComparator(value, option) {
|
||
if (typeof value !== 'object' && typeof option !== 'object') {
|
||
// Comparing primitives
|
||
if (value === option) {
|
||
return true
|
||
}
|
||
} else {
|
||
// Comparing objects
|
||
if (value === this.reduce(option)) {
|
||
return true
|
||
}
|
||
if ((this.getOptionLabel(value) === this.getOptionLabel(option)) || (this.getOptionLabel(value) === option)) {
|
||
return true
|
||
}
|
||
if (this.reduce(value) === this.reduce(option)) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
return false;
|
||
},
|
||
|
||
/**
|
||
* Finds an option from this.options
|
||
* where a reduced value matches
|
||
* the passed in value.
|
||
*
|
||
* @param value {Object}
|
||
* @returns {*}
|
||
*/
|
||
findOptionFromReducedValue (value) {
|
||
return this.options.find(option => JSON.stringify(this.reduce(option)) === JSON.stringify(value)) || value;
|
||
},
|
||
|
||
/**
|
||
* 'Private' function to close the search options
|
||
* @emits {search:blur}
|
||
* @returns {void}
|
||
*/
|
||
closeSearchOptions(){
|
||
this.open = false
|
||
this.$emit('search:blur')
|
||
},
|
||
|
||
/**
|
||
* Delete the value on Delete keypress when there is no
|
||
* text in the search input, & there's tags to delete
|
||
* @return {this.value}
|
||
*/
|
||
maybeDeleteValue() {
|
||
if (!this.searchEl.value.length && this.selectedValue && this.clearable) {
|
||
let value = null;
|
||
if (this.multiple) {
|
||
value = [...this.selectedValue.slice(0, this.selectedValue.length - 1)]
|
||
}
|
||
this.updateValue(value)
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Determine if an option exists
|
||
* within this.optionList array.
|
||
*
|
||
* @param {Object || String} option
|
||
* @return {boolean}
|
||
*/
|
||
optionExists(option) {
|
||
return this.optionList.some(opt => {
|
||
if (typeof opt === 'object' && this.getOptionLabel(opt) === option) {
|
||
return true
|
||
} else if (opt === option) {
|
||
return true
|
||
}
|
||
return false
|
||
})
|
||
},
|
||
|
||
/**
|
||
* Ensures that options are always
|
||
* passed as objects to scoped slots.
|
||
* @param option
|
||
* @return {*}
|
||
*/
|
||
normalizeOptionForSlot (option) {
|
||
return (typeof option === 'object') ? option : {[this.label]: option};
|
||
},
|
||
|
||
/**
|
||
* If push-tags is true, push the
|
||
* given option to `this.pushedTags`.
|
||
*
|
||
* @param {Object || String} option
|
||
* @return {void}
|
||
*/
|
||
maybePushTag(option) {
|
||
if (this.pushTags) {
|
||
this.pushedTags.push(option)
|
||
}
|
||
},
|
||
|
||
/**
|
||
* If there is any text in the search input, remove it.
|
||
* Otherwise, blur the search input to close the dropdown.
|
||
* @return {void}
|
||
*/
|
||
onEscape() {
|
||
if (!this.search.length) {
|
||
this.searchEl.blur()
|
||
} else {
|
||
this.search = ''
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Close the dropdown on blur.
|
||
* @emits {search:blur}
|
||
* @return {void}
|
||
*/
|
||
onSearchBlur() {
|
||
if (this.mousedown && !this.searching) {
|
||
this.mousedown = false
|
||
} else {
|
||
if (this.clearSearchOnBlur) {
|
||
this.search = ''
|
||
}
|
||
this.closeSearchOptions()
|
||
return
|
||
}
|
||
// Fixed bug where no-options message could not be closed
|
||
if (this.search.length === 0 && this.options.length === 0){
|
||
this.closeSearchOptions()
|
||
return
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Open the dropdown on focus.
|
||
* @emits {search:focus}
|
||
* @return {void}
|
||
*/
|
||
onSearchFocus() {
|
||
this.open = true
|
||
this.$emit('search:focus')
|
||
},
|
||
|
||
/**
|
||
* Event-Handler to help workaround IE11 (probably fixes 10 as well)
|
||
* firing a `blur` event when clicking
|
||
* the dropdown's scrollbar, causing it
|
||
* to collapse abruptly.
|
||
* @see https://github.com/sagalbot/vue-select/issues/106
|
||
* @return {void}
|
||
*/
|
||
onMousedown() {
|
||
this.mousedown = true
|
||
},
|
||
|
||
/**
|
||
* Event-Handler to help workaround IE11 (probably fixes 10 as well)
|
||
* @see https://github.com/sagalbot/vue-select/issues/106
|
||
* @return {void}
|
||
*/
|
||
onMouseUp() {
|
||
this.mousedown = false
|
||
},
|
||
|
||
/**
|
||
* Search <input> KeyBoardEvent handler.
|
||
* @param e {KeyboardEvent}
|
||
* @return {Function}
|
||
*/
|
||
onSearchKeyDown (e) {
|
||
const preventAndSelect = e => {
|
||
e.preventDefault();
|
||
return !this.isComposing && this.typeAheadSelect();
|
||
};
|
||
|
||
const defaults = {
|
||
// delete
|
||
8: e => this.maybeDeleteValue(),
|
||
// tab
|
||
9: e => this.onTab(),
|
||
// esc
|
||
27: e => this.onEscape(),
|
||
// up.prevent
|
||
38: e => {
|
||
e.preventDefault();
|
||
return this.typeAheadUp();
|
||
},
|
||
// down.prevent
|
||
40: e => {
|
||
e.preventDefault();
|
||
return this.typeAheadDown();
|
||
},
|
||
};
|
||
|
||
this.selectOnKeyCodes.forEach(keyCode => defaults[keyCode] = preventAndSelect);
|
||
|
||
const handlers = this.mapKeydown(defaults, this);
|
||
|
||
if (typeof handlers[e.keyCode] === 'function') {
|
||
return handlers[e.keyCode](e);
|
||
}
|
||
}
|
||
},
|
||
|
||
computed: {
|
||
/**
|
||
* Determine if the component needs to
|
||
* track the state of values internally.
|
||
* @return {boolean}
|
||
*/
|
||
isTrackingValues () {
|
||
return typeof this.value === 'undefined' || this.$options.propsData.hasOwnProperty('reduce');
|
||
},
|
||
|
||
/**
|
||
* The options that are currently selected.
|
||
* @return {Array}
|
||
*/
|
||
selectedValue () {
|
||
let value = this.value;
|
||
|
||
if (this.isTrackingValues) {
|
||
// Vue select has to manage value internally
|
||
value = this.$data._value;
|
||
}
|
||
|
||
if (value) {
|
||
return [].concat(value);
|
||
}
|
||
|
||
return [];
|
||
},
|
||
|
||
scopedValues() {
|
||
return this.selectedValue.map(option => this.getSelectedOptionScope(option));
|
||
},
|
||
|
||
/**
|
||
* The options available to be chosen
|
||
* from the dropdown, including any
|
||
* tags that have been pushed.
|
||
*
|
||
* @return {Array}
|
||
*/
|
||
optionList () {
|
||
return this.options.concat(this.pushedTags);
|
||
},
|
||
|
||
/**
|
||
* Find the search input DOM element.
|
||
* @returns {HTMLInputElement}
|
||
*/
|
||
searchEl () {
|
||
return !!this.$scopedSlots['search']
|
||
? this.$refs.selectedOptions.querySelector(this.searchInputQuerySelector)
|
||
: this.$refs.search;
|
||
},
|
||
|
||
/**
|
||
* Each key of this object is the name of a scoped slot
|
||
* within the component. The value of that key is the
|
||
* object that will be passed as the scope for the
|
||
* slot. There are a few slots that take place in
|
||
* v-for loops – these slots can't be captured in
|
||
* a computed prop, so there's specific methods
|
||
* for each of those slots.
|
||
*
|
||
* @returns {Object}
|
||
*/
|
||
scope () {
|
||
return {
|
||
search: {
|
||
attributes: {
|
||
'class': 'vs__search',
|
||
'disabled': this.disabled,
|
||
'placeholder': this.searchPlaceholder,
|
||
'tabindex': this.tabindex,
|
||
'readonly': !this.searchable,
|
||
'id': this.inputId,
|
||
'aria-expanded': this.dropdownOpen,
|
||
'aria-label': 'Search for an option',
|
||
'ref': 'search',
|
||
'role': 'combobox',
|
||
'type': 'search',
|
||
'autocomplete': 'off',
|
||
'value': this.search,
|
||
},
|
||
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,
|
||
},
|
||
},
|
||
clear: {
|
||
component: this.childComponents.Deselect,
|
||
bindings: {
|
||
'ref': 'clearButton',
|
||
'disabled': this.disabled,
|
||
'type': 'button',
|
||
'class': 'vs__clear',
|
||
'title': 'Clear Selection',
|
||
'style': { 'display': this.showClearButton ? 'block': 'none' }
|
||
},
|
||
events: {
|
||
'click': () => this.clearSelection(),
|
||
},
|
||
},
|
||
noOptions: {
|
||
text: 'Sorry, no options',
|
||
options: this.scopedOptions,
|
||
search: this.search,
|
||
attributes: {
|
||
class: 'vs__no-options',
|
||
},
|
||
events: {
|
||
mousedown: e => e.stopPropagation(),
|
||
},
|
||
},
|
||
spinner: {
|
||
loading: this.mutableLoading,
|
||
bindings: {
|
||
'class': 'vs__spinner',
|
||
},
|
||
},
|
||
openIndicator: {
|
||
shouldDisplay: ! this.noDrop,
|
||
component: this.childComponents.OpenIndicator,
|
||
attributes: {
|
||
'ref': 'openIndicator',
|
||
'role': 'presentation',
|
||
'class': 'vs__open-indicator',
|
||
},
|
||
},
|
||
};
|
||
},
|
||
|
||
/**
|
||
* Returns an object containing the child components
|
||
* that will be used throughout the component. The
|
||
* `component` prop can be used to overwrite the defaults.
|
||
*
|
||
* @return {Object}
|
||
*/
|
||
childComponents () {
|
||
return {
|
||
...childComponents,
|
||
...this.components
|
||
};
|
||
},
|
||
|
||
/**
|
||
* Holds the current state of the component.
|
||
* @return {Object}
|
||
*/
|
||
stateClasses() {
|
||
return {
|
||
'vs--open': this.dropdownOpen,
|
||
'vs--single': !this.multiple,
|
||
'vs--searching': this.searching && !this.noDrop,
|
||
'vs--searchable': this.searchable && !this.noDrop,
|
||
'vs--unsearchable': !this.searchable,
|
||
'vs--loading': this.mutableLoading,
|
||
'vs--disabled': this.disabled
|
||
}
|
||
},
|
||
|
||
/**
|
||
* If search text should clear on blur
|
||
* @return {Boolean} True when single and clearSearchOnSelect
|
||
*/
|
||
clearSearchOnBlur() {
|
||
return this.clearSearchOnSelect && !this.multiple
|
||
},
|
||
|
||
/**
|
||
* Return the current state of the
|
||
* search input
|
||
* @return {Boolean} True if non empty value
|
||
*/
|
||
searching() {
|
||
return !! this.search
|
||
},
|
||
|
||
/**
|
||
* Return the current state of the
|
||
* dropdown menu.
|
||
* @return {Boolean} True if open
|
||
*/
|
||
dropdownOpen() {
|
||
return this.noDrop ? false : this.open && !this.mutableLoading
|
||
},
|
||
|
||
/**
|
||
* Return the placeholder string if it's set
|
||
* & there is no value selected.
|
||
* @return {String} Placeholder text
|
||
*/
|
||
searchPlaceholder() {
|
||
if (this.isValueEmpty && this.placeholder) {
|
||
return this.placeholder;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* The currently displayed options, filtered
|
||
* by the search elements value. If tagging
|
||
* true, the search text will be prepended
|
||
* if it doesn't already exist.
|
||
*
|
||
* @return {array}
|
||
*/
|
||
filteredOptions() {
|
||
const optionList = [].concat(this.optionList);
|
||
|
||
if (!this.filterable && !this.taggable) {
|
||
return optionList;
|
||
}
|
||
|
||
let options = this.search.length ? this.filter(optionList, this.search, this) : optionList;
|
||
if (this.taggable && this.search.length && !this.optionExists(this.search)) {
|
||
options.unshift(this.search)
|
||
}
|
||
return options
|
||
},
|
||
|
||
scopedOptions() {
|
||
return this.filteredOptions.map((option, index) => this.getOptionScope(option, index));
|
||
},
|
||
|
||
/**
|
||
* Check if there aren't any options selected.
|
||
* @return {Boolean}
|
||
*/
|
||
isValueEmpty() {
|
||
return this.selectedValue.length === 0;
|
||
},
|
||
|
||
/**
|
||
* Determines if the clear button should be displayed.
|
||
* @return {Boolean}
|
||
*/
|
||
showClearButton() {
|
||
return !this.multiple && this.clearable && !this.open && !this.isValueEmpty
|
||
}
|
||
},
|
||
|
||
}
|
||
</script>
|