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

Merge branch 'master' into gitbook

# Conflicts:
#	docs/md/OnChange.md
This commit is contained in:
Jeff
2018-01-13 11:43:00 -08:00
9 changed files with 428 additions and 56 deletions
+17 -7
View File
@@ -9,17 +9,15 @@
<!--<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">--> <!--<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">-->
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"> --> <!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"> -->
<style> <style>
html,
body,
#app {
height: 100vh;
}
#app { #app {
height: 95vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-start;
flex-wrap: wrap;
align-content: center;
} }
.v-select { .v-select {
@@ -32,13 +30,25 @@
<body> <body>
<div id="app"> <div id="app">
<v-select placeholder="default" :options="options"></v-select> <v-select placeholder="default" :options="options"></v-select>
<v-select placeholder="default, RTL" :options="options" dir="rtl"></v-select>
<v-select placeholder="multiple" multiple :options="options"></v-select> <v-select placeholder="multiple" multiple :options="options"></v-select>
<v-select placeholder="multiple, taggable" multiple taggable :options="options" no-drop></v-select> <v-select placeholder="multiple, taggable" multiple taggable :options="options" no-drop></v-select>
<v-select placeholder="multiple, taggable, push-tags" multiple push-tags taggable :options="[{label: 'Foo', value: 'foo'}]"></v-select> <v-select placeholder="multiple, taggable, push-tags" multiple push-tags taggable :options="[{label: 'Foo', value: 'foo'}]"></v-select>
<v-select placeholder="multiple, closeOnSelect=true" multiple :options="['cat', 'dog', 'bear']"></v-select> <v-select placeholder="multiple, closeOnSelect=true" multiple :options="['cat', 'dog', 'bear']"></v-select>
<v-select placeholder="multiple, closeOnSelect=false" multiple :close-on-select="false" :options="['cat', 'dog', 'bear']"></v-select> <v-select placeholder="multiple, closeOnSelect=false" multiple :close-on-select="false" :options="['cat', 'dog', 'bear']"></v-select>
<v-select placeholder="unsearchable" :options="options" :searchable="false"></v-select> <v-select placeholder="searchable=false" :options="options" :searchable="false"></v-select>
<v-select placeholder="search github.." label="full_name" @search="search" :options="ajaxRes"></v-select> <v-select placeholder="search github.." label="full_name" @search="search" :options="ajaxRes"></v-select>
<v-select placeholder="custom option template" :options="options" multiple>
<template slot="selected-option" slot-scope="option">
{{option.label}}
</template>
<template slot="option" slot-scope="option">
{{option.label}} ({{option.value}})
</template>
</v-select>
<v-select placeholder="disabled" disabled value="disabled"></v-select>
<v-select placeholder="disabled multiple" disabled multiple :value="['disabled', 'multiple']"></v-select>
<v-select placeholder="filterable=false, @search=searchPeople" label="first_name" :filterable="false" @search="searchPeople" :options="people"></v-select>
</div> </div>
</body> </body>
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -122,7 +122,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<pre><v-code lang="markup">&#x3C;v-select v-on:change=&#x22;consoleCallback&#x22; :options=&#x22;countries&#x22;&#x3E;&#x3C;/v-select&#x3E;</v-code></pre> <pre><v-code lang="markup">&#x3C;v-select :on-change=&#x22;consoleCallback&#x22; :options=&#x22;countries&#x22;&#x3E;&#x3C;/v-select&#x3E;</v-code></pre>
<pre><v-code lang="javascript">methods: { <pre><v-code lang="javascript">methods: {
consoleCallback(val) { consoleCallback(val) {
console.dir(JSON.stringify(val)) console.dir(JSON.stringify(val))
+26
View File
@@ -0,0 +1,26 @@
### Change Event <small>Vuex Compatibility</small>
vue-select provides a `change` event. This function is passed the currently selected value(s) as it's only parameter.
This is very useful when integrating with Vuex, as it will allow your to trigger an action to update your vuex state object. Choose a callback and see it in action.
<div class="form-inline">
<div class="radio"><label><input type="radio" v-model="callback" value="console"> `console.log(val)`</label> </div>
<div class="radio"><label><input type="radio" v-model="callback" value="alert"> `alert(val)`</label> </div>
</div>
```html
<v-select :on-change="consoleCallback" :options="countries"></v-select>
```
```js
methods: {
consoleCallback(val) {
console.dir(JSON.stringify(val))
},
alertCallback(val) {
alert(JSON.stringify(val))
}
}
```
+2 -7
View File
@@ -1,12 +1,13 @@
{ {
"name": "vue-select", "name": "vue-select",
"version": "2.2.0", "version": "2.4.0",
"description": "A native Vue.js component that provides similar functionality to Select2 without the overhead of jQuery.", "description": "A native Vue.js component that provides similar functionality to Select2 without the overhead of jQuery.",
"author": "Jeff Sagal <sagalbot@gmail.com>", "author": "Jeff Sagal <sagalbot@gmail.com>",
"private": false, "private": false,
"main": "dist/vue-select.js", "main": "dist/vue-select.js",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"start": "npm run dev",
"dev": "node build/dev-server.js", "dev": "node build/dev-server.js",
"docs": "gitbook serve docs", "docs": "gitbook serve docs",
"build": "node build/build.js", "build": "node build/build.js",
@@ -14,12 +15,6 @@
"test": "karma start test/unit/karma.conf.js --single-run", "test": "karma start test/unit/karma.conf.js --single-run",
"test-watch": "karma start test/unit/karma.conf.js --single-run=false --auto-watch=true" "test-watch": "karma start test/unit/karma.conf.js --single-run=false --auto-watch=true"
}, },
"browserify": {
"transform": [
"vueify",
"babelify"
]
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/sagalbot/vue-select.git" "url": "https://github.com/sagalbot/vue-select.git"
+179 -30
View File
@@ -3,12 +3,30 @@
position: relative; position: relative;
font-family: sans-serif; font-family: sans-serif;
} }
.v-select, .v-select,
.v-select * { .v-select * {
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
} }
/* Rtl support */
.v-select.rtl .open-indicator {
left: 10px;
right: auto;
}
.v-select.rtl .selected-tag {
float: right;
margin-right: 3px;
margin-left: 1px;
}
.v-select.rtl .dropdown-menu {
text-align: right;
}
.v-select.rtl .dropdown-toggle .clear {
left: 30px;
right: auto;
}
/* Open Indicator */ /* Open Indicator */
.v-select .open-indicator { .v-select .open-indicator {
position: absolute; position: absolute;
@@ -20,7 +38,6 @@
transition: all 150ms cubic-bezier(1.000, -0.115, 0.975, 0.855); transition: all 150ms cubic-bezier(1.000, -0.115, 0.975, 0.855);
transition-timing-function: cubic-bezier(1.000, -0.115, 0.975, 0.855); transition-timing-function: cubic-bezier(1.000, -0.115, 0.975, 0.855);
opacity: 1; opacity: 1;
transition: opacity .1s;
height: 20px; width: 10px; height: 20px; width: 10px;
} }
.v-select .open-indicator:before { .v-select .open-indicator:before {
@@ -58,7 +75,6 @@
border: 1px solid rgba(60, 60, 60, .26); border: 1px solid rgba(60, 60, 60, .26);
border-radius: 4px; border-radius: 4px;
white-space: normal; white-space: normal;
transition: border-radius .25s;
} }
.v-select .dropdown-toggle:after { .v-select .dropdown-toggle:after {
visibility: hidden; visibility: hidden;
@@ -68,6 +84,22 @@
clear: both; clear: both;
height: 0; height: 0;
} }
/* Clear Button */
.v-select .dropdown-toggle .clear {
position: absolute;
bottom: 9px;
right: 30px;
font-size: 23px;
font-weight: 700;
line-height: 1;
color: rgba(60, 60, 60, .5);
padding: 0;
border: 0;
background-color: transparent;
cursor: pointer;
}
/* Dropdown Toggle States */ /* Dropdown Toggle States */
.v-select.searchable .dropdown-toggle { .v-select.searchable .dropdown-toggle {
cursor: text; cursor: text;
@@ -174,14 +206,14 @@
background: none; background: none;
position: relative; position: relative;
box-shadow: none; box-shadow: none;
float: left;
clear: none;
} }
/* Search Input States */
.v-select.unsearchable input[type="search"] { .v-select.unsearchable input[type="search"] {
max-width: 1px; opacity: 0;
} }
/* List Items */ .v-select.unsearchable input[type="search"]:hover {
cursor: pointer;
}
/* List Items */
.v-select li { .v-select li {
line-height: 1.42857143; /* Normalize line height */ line-height: 1.42857143; /* Normalize line height */
} }
@@ -233,6 +265,17 @@
width: 5em; width: 5em;
height: 5em; height: 5em;
} }
/* Disabled state */
.v-select.disabled .dropdown-toggle,
.v-select.disabled .dropdown-toggle .clear,
.v-select.disabled .dropdown-toggle input,
.v-select.disabled .selected-tag .close,
.v-select.disabled .open-indicator {
cursor: not-allowed;
background-color: rgb(248, 248, 248);
}
/* Loading Spinner States */ /* Loading Spinner States */
.v-select.loading .spinner { .v-select.loading .spinner {
opacity: 1; opacity: 1;
@@ -266,15 +309,20 @@
</style> </style>
<template> <template>
<div class="dropdown v-select" :class="dropdownClasses"> <div :dir="dir" class="dropdown v-select" :class="dropdownClasses">
<div ref="toggle" @mousedown.prevent="toggleDropdown" class="dropdown-toggle"> <div ref="toggle" @mousedown.prevent="toggleDropdown" :class="['dropdown-toggle', 'clearfix']">
<span class="selected-tag" v-for="option in valueAsArray" v-bind:key="option.index"> <slot v-for="option in valueAsArray" name="selected-option-container"
{{ getOptionLabel(option) }} :option="option" :deselect="deselect">
<button v-if="multiple" @click="deselect(option)" type="button" class="close" aria-label="Remove option"> <span class="selected-tag" v-bind:key="option.index">
<span aria-hidden="true">&times;</span> <slot name="selected-option" v-bind="option">
</button> {{ getOptionLabel(option) }}
</span> </slot>
<button v-if="multiple" :disabled="disabled" @click="deselect(option)" type="button" class="close" aria-label="Remove option">
<span aria-hidden="true">&times;</span>
</button>
</span>
</slot>
<input <input
ref="search" ref="search"
@@ -283,18 +331,32 @@
@keyup.esc="onEscape" @keyup.esc="onEscape"
@keydown.up.prevent="typeAheadUp" @keydown.up.prevent="typeAheadUp"
@keydown.down.prevent="typeAheadDown" @keydown.down.prevent="typeAheadDown"
@keyup.enter.prevent="typeAheadSelect" @keydown.enter.prevent="typeAheadSelect"
@blur="onSearchBlur" @blur="onSearchBlur"
@focus="onSearchFocus" @focus="onSearchFocus"
type="search" type="search"
class="form-control" class="form-control"
autocomplete="false"
:disabled="disabled"
:placeholder="searchPlaceholder" :placeholder="searchPlaceholder"
:tabindex="tabindex"
:readonly="!searchable" :readonly="!searchable"
:style="{ width: isValueEmpty ? '100%' : 'auto' }" :style="{ width: isValueEmpty ? '100%' : 'auto' }"
:id="inputId" :id="inputId"
aria-label="Search for option" aria-label="Search for option"
> >
<button
v-show="showClearButton"
:disabled="disabled"
@click="clearSelection"
type="button"
class="clear"
title="Clear selection"
>
<span aria-hidden="true">&times;</span>
</button>
<i v-if="!noDrop" ref="openIndicator" role="presentation" class="open-indicator"></i> <i v-if="!noDrop" ref="openIndicator" role="presentation" class="open-indicator"></i>
<slot name="spinner"> <slot name="spinner">
@@ -306,7 +368,9 @@
<ul ref="dropdownMenu" v-if="dropdownOpen" class="dropdown-menu" :style="{ 'max-height': maxHeight }"> <ul ref="dropdownMenu" v-if="dropdownOpen" class="dropdown-menu" :style="{ 'max-height': maxHeight }">
<li v-for="(option, index) in filteredOptions" v-bind:key="index" :class="{ active: isOptionSelected(option), highlight: index === typeAheadPointer }" @mouseover="typeAheadPointer = index"> <li v-for="(option, index) in filteredOptions" v-bind:key="index" :class="{ active: isOptionSelected(option), highlight: index === typeAheadPointer }" @mouseover="typeAheadPointer = index">
<a @mousedown.prevent="select(option)"> <a @mousedown.prevent="select(option)">
<slot name="option" v-bind="option">
{{ getOptionLabel(option) }} {{ getOptionLabel(option) }}
</slot>
</a> </a>
</li> </li>
<li v-if="!filteredOptions.length" class="no-options"> <li v-if="!filteredOptions.length" class="no-options">
@@ -341,7 +405,7 @@
* If you are using an array of objects, vue-select will look for * If you are using an array of objects, vue-select will look for
* a `label` key (ex. [{label: 'This is Foo', value: 'foo'}]). A * a `label` key (ex. [{label: 'This is Foo', value: 'foo'}]). A
* custom label key can be set with the `label` prop. * custom label key can be set with the `label` prop.
* @type {Object} * @type {Array}
*/ */
options: { options: {
type: Array, type: Array,
@@ -350,6 +414,15 @@
}, },
}, },
/**
* Disable the entire component.
* @type {Boolean}
*/
disabled: {
type: Boolean,
default: false
},
/** /**
* Sets the max-height property on the dropdown list. * Sets the max-height property on the dropdown list.
* @deprecated * @deprecated
@@ -371,7 +444,7 @@
/** /**
* Equivalent to the `multiple` attribute on a `<select>` input. * Equivalent to the `multiple` attribute on a `<select>` input.
* @type {Object} * @type {Boolean}
*/ */
multiple: { multiple: {
type: Boolean, type: Boolean,
@@ -380,7 +453,7 @@
/** /**
* Equivalent to the `placeholder` attribute on an `<input>`. * Equivalent to the `placeholder` attribute on an `<input>`.
* @type {Object} * @type {String}
*/ */
placeholder: { placeholder: {
type: String, type: String,
@@ -429,6 +502,7 @@
/** /**
* Callback to generate the label text. If {option} * Callback to generate the label text. If {option}
* is an object, returns option[this.label] by default. * is an object, returns option[this.label] by default.
* @type {Function}
* @param {Object || String} option * @param {Object || String} option
* @return {String} * @return {String}
*/ */
@@ -436,6 +510,13 @@
type: Function, type: Function,
default(option) { default(option) {
if (typeof option === 'object') { 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` +
'http://sagalbot.github.io/vue-select/#ex-labels'
)
}
if (this.label && option[this.label]) { if (this.label && option[this.label]) {
return option[this.label] return option[this.label]
} }
@@ -444,12 +525,27 @@
} }
}, },
/**
* Callback to filter the search result the label text.
* @type {Function}
* @param {Object || String} option
* @param {String} label
* @param {String} search
* @return {Boolean}
*/
filterFunction: {
type: Function,
default(option, label, search) {
return (label || '').toLowerCase().indexOf(search.toLowerCase()) > -1
}
},
/** /**
* An optional callback function that is called each time the selected * An optional callback function that is called each time the selected
* value(s) change. When integrating with Vuex, use this callback to trigger * value(s) change. When integrating with Vuex, use this callback to trigger
* an action, rather than using :value.sync to retreive the selected value. * an action, rather than using :value.sync to retreive the selected value.
* @type {Function} * @type {Function}
* @default {null} * @param {Object || String} val
*/ */
onChange: { onChange: {
type: Function, type: Function,
@@ -467,6 +563,15 @@
default: false 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 * When true, newly created tags will be added to
* the options list. * the options list.
@@ -477,6 +582,17 @@
default: false 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
},
/** /**
* User defined function for adding Options * User defined function for adding Options
* @type {Function} * @type {Function}
@@ -517,7 +633,18 @@
*/ */
inputId: { inputId: {
type: String 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'
},
}, },
data() { data() {
@@ -645,6 +772,14 @@
} }
}, },
/**
* Clears the currently selected value(s)
* @return {void}
*/
clearSelection() {
this.mutableValue = this.multiple ? [] : null
},
/** /**
* Called from this.select after each selection. * Called from this.select after each selection.
* @param {Object|String} option * @param {Object|String} option
@@ -671,8 +806,10 @@
if (this.open) { if (this.open) {
this.$refs.search.blur() // dropdown will close on blur this.$refs.search.blur() // dropdown will close on blur
} else { } else {
this.open = true if (!this.disabled) {
this.$refs.search.focus() this.open = true
this.$refs.search.focus()
}
} }
} }
}, },
@@ -796,7 +933,9 @@
searching: this.searching, searching: this.searching,
searchable: this.searchable, searchable: this.searchable,
unsearchable: !this.searchable, unsearchable: !this.searchable,
loading: this.mutableLoading loading: this.mutableLoading,
rtl: this.dir === 'rtl',
disabled: this.disabled
} }
}, },
@@ -846,13 +985,15 @@
* @return {array} * @return {array}
*/ */
filteredOptions() { filteredOptions() {
if (!this.filterable && !this.taggable) {
return this.mutableOptions.slice()
}
let options = this.mutableOptions.filter((option) => { let options = this.mutableOptions.filter((option) => {
if (typeof option === 'object' && option.hasOwnProperty(this.label)) { let label = this.getOptionLabel(option)
return option[this.label].toLowerCase().indexOf(this.search.toLowerCase()) > -1 if (typeof label === 'number') {
} else if (typeof option === 'object' && !option.hasOwnProperty(this.label)) { label = label.toString()
return console.warn(`[vue-select warn]: Label key "option.${this.label}" does not exist in options object.\nhttp://sagalbot.github.io/vue-select/#ex-labels`)
} }
return option.toLowerCase().indexOf(this.search.toLowerCase()) > -1 return this.filterFunction(option, label, this.search)
}) })
if (this.taggable && this.search.length && !this.optionExists(this.search)) { if (this.taggable && this.search.length && !this.optionExists(this.search)) {
options.unshift(this.search) options.unshift(this.search)
@@ -883,10 +1024,18 @@
if (this.multiple) { if (this.multiple) {
return this.mutableValue return this.mutableValue
} else if (this.mutableValue) { } else if (this.mutableValue) {
return [this.mutableValue] return [].concat(this.mutableValue)
} }
return [] return []
},
/**
* Determines if the clear button should be displayed.
* @return {Boolean}
*/
showClearButton() {
return !this.multiple && !this.open && this.mutableValue != null
} }
}, },
+12 -1
View File
@@ -17,13 +17,24 @@ new Vue({
placeholder: "placeholder", placeholder: "placeholder",
value: null, value: null,
options: countries, options: countries,
ajaxRes: [] ajaxRes: [],
people: []
}, },
methods: { methods: {
search(search, loading) { search(search, loading) {
loading(true) loading(true)
this.getRepositories(search, loading, this) this.getRepositories(search, loading, this)
}, },
searchPeople(search, loading) {
loading(true)
this.getPeople(loading, this)
},
getPeople: debounce((loading, vm) => {
vm.$http.get(`https://reqres.in/api/users?per_page=10`).then(res => {
vm.people = res.data.data
loading(false)
})
}, 250),
getRepositories: debounce((search, loading, vm) => { getRepositories: debounce((search, loading, vm) => {
vm.$http.get(`https://api.github.com/search/repositories?q=${search}`).then(res => { vm.$http.get(`https://api.github.com/search/repositories?q=${search}`).then(res => {
vm.ajaxRes = res.data.items vm.ajaxRes = res.data.items
+184 -3
View File
@@ -4,7 +4,7 @@
import Vue from 'vue' import Vue from 'vue'
import vSelect from 'src/components/Select.vue' import vSelect from 'src/components/Select.vue'
import pointerScroll from 'src/mixins/pointerScroll.js' import pointerScroll from 'src/mixins/pointerScroll.js'
Vue.config.productionTip = false
// http://vue-loader.vuejs.org/en/workflow/testing-with-mocks.html // http://vue-loader.vuejs.org/en/workflow/testing-with-mocks.html
const Mock = require('!!vue?inject!src/components/Select.vue') const Mock = require('!!vue?inject!src/components/Select.vue')
@@ -65,7 +65,7 @@ function searchSubmit(vm, search = false) {
vm.$children[0].search = search vm.$children[0].search = search
} }
trigger(vm.$children[0].$refs.search, 'keyup', function (e) { trigger(vm.$children[0].$refs.search, 'keydown', function (e) {
e.keyCode = 13 e.keyCode = 13
}) })
} }
@@ -295,6 +295,15 @@ describe('Select.vue', () => {
expect(vm.$refs.select.filteredOptions).toEqual(['bar','baz']) expect(vm.$refs.select.filteredOptions).toEqual(['bar','baz'])
}) })
it('should not filter the array of strings if filterable is false', () => {
const vm = new Vue({
template: `<div><v-select ref="select" :filterable="false" :options="['foo','bar','baz']" v-model="value"></v-select></div>`,
data: {value: 'foo'}
}).$mount()
vm.$refs.select.search = 'ba'
expect(vm.$refs.select.filteredOptions).toEqual(['foo','bar','baz'])
})
it('should filter without case-sensitivity', () => { it('should filter without case-sensitivity', () => {
const vm = new Vue({ const vm = new Vue({
template: `<div><v-select ref="select" :options="['Foo','Bar','Baz']" v-model="value"></v-select></div>`, template: `<div><v-select ref="select" :options="['Foo','Bar','Baz']" v-model="value"></v-select></div>`,
@@ -312,9 +321,40 @@ describe('Select.vue', () => {
vm.$refs.select.search = 'ba' vm.$refs.select.search = 'ba'
expect(JSON.stringify(vm.$refs.select.filteredOptions)).toEqual(JSON.stringify([{label: 'Bar', value: 'bar'}, {label: 'Baz', value: 'baz'}])) expect(JSON.stringify(vm.$refs.select.filteredOptions)).toEqual(JSON.stringify([{label: 'Bar', value: 'bar'}, {label: 'Baz', value: 'baz'}]))
}) })
it('can use a custom filterFunction passed via props', ()=>{
const vm = new Vue({
template: `<div><v-select ref="select" :filterFunction="customFn" :options="[{label: 'Aoo', value: 'foo'}, {label: 'Bar', value: 'bar'}, {label: 'Baz', value: 'baz'}]" v-model="value"></v-select></div>`,
data: {value: 'foo'},
methods:{
customFn: (option, label, search) => label.match(new RegExp('^' + search, 'i'))
}
}).$mount()
vm.$refs.select.search = 'a'
expect(JSON.stringify(vm.$refs.select.filteredOptions)).toEqual(JSON.stringify([{label: 'Aoo', value: 'foo'}]))
})
}) })
describe('Toggling Dropdown', () => { describe('Toggling Dropdown', () => {
it('should not open the dropdown when the el is clicked but the component is disabled', (done) => {
const vm = new Vue({
template: '<div><v-select :options="options" :value="value" disabled></v-select></div>',
components: {vSelect},
data: {
value: [{label: 'one'}],
options: [{label: 'one'}]
}
}).$mount()
vm.$children[0].toggleDropdown({target: vm.$children[0].$refs.search})
Vue.nextTick(() => {
Vue.nextTick(() => {
expect(vm.$children[0].open).toEqual(false)
done()
})
})
})
it('should open the dropdown when the el is clicked', (done) => { it('should open the dropdown when the el is clicked', (done) => {
const vm = new Vue({ const vm = new Vue({
template: '<div><v-select :options="options" :value="value"></v-select></div>', template: '<div><v-select :options="options" :value="value"></v-select></div>',
@@ -674,6 +714,22 @@ describe('Select.vue', () => {
}) })
}) })
it('should not remove tag when close icon is clicked and component is disabled', (done) => {
const vm = new Vue({
template: '<div><v-select disabled multiple :options="options" v-model="value"></v-select></div>',
components: {vSelect},
data: {
value: ['one'],
options: ['one', 'two', 'three']
}
}).$mount()
vm.$children[0].$refs.toggle.querySelector('.close').click()
Vue.nextTick(() => {
expect(vm.$children[0].mutableValue).toEqual(['one'])
done()
})
})
it('should remove the last item in the value array on delete keypress when multiple is true', () => { it('should remove the last item in the value array on delete keypress when multiple is true', () => {
const vm = new Vue({ const vm = new Vue({
@@ -726,7 +782,7 @@ describe('Select.vue', () => {
}).$mount() }).$mount()
Vue.nextTick(() => { Vue.nextTick(() => {
expect(console.warn).toHaveBeenCalledWith( expect(console.warn).toHaveBeenCalledWith(
'[vue-select warn]: Label key "option.label" does not exist in options object.' + '[vue-select warn]: Label key "option.label" does not exist in options object {}.' +
'\nhttp://sagalbot.github.io/vue-select/#ex-labels' '\nhttp://sagalbot.github.io/vue-select/#ex-labels'
) )
done() done()
@@ -853,6 +909,21 @@ describe('Select.vue', () => {
expect(vm.$children[0].mutableOptions).toEqual(['one', 'two', 'three']) expect(vm.$children[0].mutableOptions).toEqual(['one', 'two', 'three'])
}) })
it('should add a freshly created option/tag to the options list when pushTags is true and filterable is false', () => {
const vm = new Vue({
template: '<div><v-select :options="options" push-tags :value="value" :filterable="false" :multiple="true" :taggable="true"></v-select></div>',
components: {vSelect},
data: {
value: ['one'],
options: ['one', 'two']
}
}).$mount()
searchSubmit(vm, 'three')
expect(vm.$children[0].mutableOptions).toEqual(['one', 'two', 'three'])
expect(vm.$children[0].filteredOptions).toEqual(['one', 'two', 'three'])
})
it('wont add a freshly created option/tag to the options list when pushTags is false', () => { it('wont add a freshly created option/tag to the options list when pushTags is false', () => {
const vm = new Vue({ const vm = new Vue({
template: '<div><v-select :options="options" :value="value" :multiple="true" :taggable="true"></v-select></div>', template: '<div><v-select :options="options" :value="value" :multiple="true" :taggable="true"></v-select></div>',
@@ -867,6 +938,21 @@ describe('Select.vue', () => {
expect(vm.$children[0].mutableOptions).toEqual(['one', 'two']) expect(vm.$children[0].mutableOptions).toEqual(['one', 'two'])
}) })
it('wont add a freshly created option/tag to the options list when pushTags is false and filterable is false', () => {
const vm = new Vue({
template: '<div><v-select :options="options" :value="value" :multiple="true" :filterable="false" :taggable="true"></v-select></div>',
components: {vSelect},
data: {
value: ['one'],
options: ['one', 'two']
}
}).$mount()
searchSubmit(vm, 'three')
expect(vm.$children[0].mutableOptions).toEqual(['one', 'two'])
expect(vm.$children[0].filteredOptions).toEqual(['one', 'two'])
})
it('should select an existing option if the search string matches a string from options', (done) => { it('should select an existing option if the search string matches a string from options', (done) => {
let two = 'two' let two = 'two'
const vm = new Vue({ const vm = new Vue({
@@ -908,6 +994,28 @@ describe('Select.vue', () => {
}) })
}) })
it('should select an existing option if the search string matches an objects label from options when filter-options is false', (done) => {
let two = {label: 'two'}
const vm = new Vue({
template: '<div><v-select :options="options" taggable :filterable="false"></v-select></div>',
data: {
options: [{label: 'one'}, two]
}
}).$mount()
vm.$children[0].search = 'two'
Vue.nextTick(() => {
searchSubmit(vm)
// This needs to be wrapped in nextTick() twice so that filteredOptions can
// calculate after setting the search text, and move the typeAheadPointer index to 0.
Vue.nextTick(() => {
expect(vm.$children[0].mutableValue.label).toBe(two.label)
done()
})
})
})
it('should not reset the selected value when the options property changes', (done) => { it('should not reset the selected value when the options property changes', (done) => {
const vm = new Vue({ const vm = new Vue({
template: '<div><v-select :options="options" :value="value" :multiple="true" taggable></v-select></div>', template: '<div><v-select :options="options" :value="value" :multiple="true" taggable></v-select></div>',
@@ -924,6 +1032,22 @@ describe('Select.vue', () => {
}) })
}) })
it('should not reset the selected value when the options property changes when filterable is false', (done) => {
const vm = new Vue({
template: '<div><v-select :options="options" :value="value" :multiple="true" :filterable="false" taggable></v-select></div>',
components: {vSelect},
data: {
value: [{label: 'one'}],
options: [{label: 'one'}]
}
}).$mount()
vm.$children[0].mutableOptions = [{label: 'two'}]
Vue.nextTick(() => {
expect(vm.$children[0].mutableValue).toEqual([{label: 'one'}])
done()
})
})
it('should not allow duplicate tags when using string options', (done) => { it('should not allow duplicate tags when using string options', (done) => {
const vm = new Vue({ const vm = new Vue({
template: `<div><v-select ref="select" taggable multiple></v-select></div>`, template: `<div><v-select ref="select" taggable multiple></v-select></div>`,
@@ -1187,4 +1311,61 @@ describe('Select.vue', () => {
}) })
}) })
}) })
describe( 'Clear button', () => {
it( 'should be displayed on single select when value is selected', () => {
const VueSelect = Vue.extend( vSelect )
const vm = new VueSelect({
propsData: {
options: ['foo','bar'],
value: 'foo'
}
}).$mount()
expect(vm.showClearButton).toEqual(true)
})
it( 'should not be displayed on multiple select', () => {
const VueSelect = Vue.extend( vSelect )
const vm = new VueSelect({
propsData: {
options: ['foo','bar'],
value: 'foo',
multiple: true
}
}).$mount()
expect(vm.showClearButton).toEqual(false)
})
it( 'should remove selected value when clicked', () => {
const VueSelect = Vue.extend( vSelect )
const vm = new VueSelect({
propsData: {
options: ['foo','bar'],
value: 'foo'
}
}).$mount()
expect(vm.mutableValue).toEqual('foo')
vm.$el.querySelector( 'button.clear' ).click()
expect(vm.mutableValue).toEqual(null)
})
it( 'should be disabled when component is disabled', () => {
const VueSelect = Vue.extend( vSelect )
const vm = new VueSelect({
propsData: {
options: ['foo','bar'],
value: 'foo',
disabled: true
}
}).$mount()
const buttonEl = vm.$el.querySelector( 'button.clear' )
expect(buttonEl.disabled).toEqual(true);
})
});
}) })