mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-22 10:30:34 +03:00
component
- switch to `v-if` (#98) - update array string filter to be case-insensitve testing - update karma to hide output from skipped tests - skip scroll down test until http://github.com/vuejs/vue-loader/issues/434 is resolved build - add dev.html to be used as entry point for development
This commit is contained in:
@@ -27,7 +27,7 @@ module.exports = merge(baseWebpackConfig, {
|
|||||||
// https://github.com/ampedandwired/html-webpack-plugin
|
// https://github.com/ampedandwired/html-webpack-plugin
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: 'index.html',
|
filename: 'index.html',
|
||||||
template: 'index.html',
|
template: 'dev.html',
|
||||||
inject: true
|
inject: true
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Vue Select Dev</title>
|
||||||
|
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.5/css/bootstrap.min.css"> -->
|
||||||
|
<style>
|
||||||
|
html,body,#app {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
#app {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.select {
|
||||||
|
width: 25em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<v-select class="select" multiple taggable v-model="value" :options="options"></v-select>
|
||||||
|
<!-- <v-select class="select" multiple push-tags taggable :options="[{label: 'Foo', value: 'foo'}]"></v-select> -->
|
||||||
|
<!-- <v-select class="select" multiple push-tags></v-select> -->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
|
|
||||||
<span class="selected-tag" v-for="option in valueAsArray" v-bind:key="option.index">
|
<span class="selected-tag" v-for="option in valueAsArray" v-bind:key="option.index">
|
||||||
{{ getOptionLabel(option) }}
|
{{ getOptionLabel(option) }}
|
||||||
<button v-if="multiple" @click="select(option)" type="button" class="close">
|
<button v-if="multiple" @click="deselect(option)" type="button" class="close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@@ -199,14 +199,14 @@
|
|||||||
:style="{ width: isValueEmpty ? '100%' : 'auto' }"
|
:style="{ width: isValueEmpty ? '100%' : 'auto' }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<i 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">
|
||||||
<div class="spinner" v-show="mutableLoading">Loading...</div>
|
<div class="spinner" v-show="mutableLoading">Loading...</div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul ref="dropdownMenu" v-show="open" :transition="transition" class="dropdown-menu" :style="{ 'max-height': maxHeight }">
|
<ul ref="dropdownMenu" v-if="dropdownOpen" :transition="transition" 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)">
|
||||||
{{ getOptionLabel(option) }}
|
{{ getOptionLabel(option) }}
|
||||||
@@ -397,6 +397,11 @@
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noDrop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -481,7 +486,7 @@
|
|||||||
*/
|
*/
|
||||||
select(option) {
|
select(option) {
|
||||||
if (this.isOptionSelected(option)) {
|
if (this.isOptionSelected(option)) {
|
||||||
this.deselect(option)
|
this.taggable ? null : this.deselect(option)
|
||||||
} else {
|
} else {
|
||||||
if (this.taggable && !this.optionExists(option)) {
|
if (this.taggable && !this.optionExists(option)) {
|
||||||
option = this.createOption(option)
|
option = this.createOption(option)
|
||||||
@@ -632,12 +637,21 @@
|
|||||||
*/
|
*/
|
||||||
dropdownClasses() {
|
dropdownClasses() {
|
||||||
return {
|
return {
|
||||||
open: this.open,
|
open: this.dropdownOpen,
|
||||||
searchable: this.searchable,
|
searchable: this.searchable,
|
||||||
loading: this.mutableLoading
|
loading: this.mutableLoading
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current state of the
|
||||||
|
* dropdown menu.
|
||||||
|
* @return {Boolean} True if open
|
||||||
|
*/
|
||||||
|
dropdownOpen() {
|
||||||
|
return this.noDrop ? false : this.open
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the placeholder string if it's set
|
* Return the placeholder string if it's set
|
||||||
* & there is no value selected.
|
* & there is no value selected.
|
||||||
@@ -658,12 +672,11 @@
|
|||||||
* @return {array}
|
* @return {array}
|
||||||
*/
|
*/
|
||||||
filteredOptions() {
|
filteredOptions() {
|
||||||
|
|
||||||
let options = this.mutableOptions.filter((option) => {
|
let options = this.mutableOptions.filter((option) => {
|
||||||
if( typeof option === 'object' ) {
|
if( typeof option === 'object' ) {
|
||||||
return option[this.label].indexOf(this.search) > -1
|
return option[this.label].toLowerCase().indexOf(this.search.toLowerCase()) > -1
|
||||||
}
|
}
|
||||||
return option.indexOf(this.search) > -1
|
return option.toLowerCase().indexOf(this.search.toLowerCase()) > -1
|
||||||
})
|
})
|
||||||
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)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// flow
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
watch: {
|
watch: {
|
||||||
typeAheadPointer() {
|
typeAheadPointer() {
|
||||||
@@ -30,8 +32,10 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
pixelsToPointerTop() {
|
pixelsToPointerTop() {
|
||||||
let pixelsToPointerTop = 0
|
let pixelsToPointerTop = 0
|
||||||
for (let i = 0; i < this.typeAheadPointer; i++) {
|
if( this.$refs.dropdownMenu ) {
|
||||||
pixelsToPointerTop += this.$refs.dropdownMenu.children[i].offsetHeight
|
for (let i = 0; i < this.typeAheadPointer; i++) {
|
||||||
|
pixelsToPointerTop += this.$refs.dropdownMenu.children[i].offsetHeight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return pixelsToPointerTop
|
return pixelsToPointerTop
|
||||||
},
|
},
|
||||||
@@ -50,7 +54,7 @@ module.exports = {
|
|||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
pointerHeight() {
|
pointerHeight() {
|
||||||
let element = this.$refs.dropdownMenu.children[this.typeAheadPointer]
|
let element = this.$refs.dropdownMenu ? this.$refs.dropdownMenu.children[this.typeAheadPointer] : false
|
||||||
return element ? element.offsetHeight : 0
|
return element ? element.offsetHeight : 0
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -60,8 +64,8 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
viewport() {
|
viewport() {
|
||||||
return {
|
return {
|
||||||
top: this.$refs.dropdownMenu.scrollTop,
|
top: this.$refs.dropdownMenu ? this.$refs.dropdownMenu.scrollTop: 0,
|
||||||
bottom: this.$refs.dropdownMenu.offsetHeight + this.$refs.dropdownMenu.scrollTop
|
bottom: this.$refs.dropdownMenu ? this.$refs.dropdownMenu.offsetHeight + this.$refs.dropdownMenu.scrollTop : 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -71,7 +75,7 @@ module.exports = {
|
|||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
scrollTo(position) {
|
scrollTo(position) {
|
||||||
return this.$refs.dropdownMenu.scrollTop = position
|
return this.$refs.dropdownMenu ? this.$refs.dropdownMenu.scrollTop = position : null
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,9 @@ module.exports = function (config) {
|
|||||||
webpackMiddleware: {
|
webpackMiddleware: {
|
||||||
noInfo: true
|
noInfo: true
|
||||||
},
|
},
|
||||||
|
specReporter: {
|
||||||
|
suppressSkipped: true
|
||||||
|
},
|
||||||
coverageReporter: {
|
coverageReporter: {
|
||||||
dir: './coverage',
|
dir: './coverage',
|
||||||
reporters: [
|
reporters: [
|
||||||
|
|||||||
+113
-26
@@ -1,15 +1,15 @@
|
|||||||
|
// flow
|
||||||
/* global describe, it, expect */
|
/* global describe, it, expect */
|
||||||
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import vSelect from 'src/components/Select.vue'
|
import vSelect from 'src/components/Select.vue'
|
||||||
// import vSelect from '../../../dist/vue-select'
|
|
||||||
import pointerScroll from 'src/mixins/pointerScroll.js'
|
import pointerScroll from 'src/mixins/pointerScroll.js'
|
||||||
|
|
||||||
Vue.component('v-select', vSelect)
|
|
||||||
|
|
||||||
// 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')
|
||||||
|
|
||||||
|
Vue.component('v-select', vSelect)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simulate a DOM event.
|
* Simulate a DOM event.
|
||||||
* @param target
|
* @param target
|
||||||
@@ -25,6 +25,13 @@ function trigger(target, event, process) {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate a Mouse event.
|
||||||
|
* @param target
|
||||||
|
* @param event
|
||||||
|
* @param process
|
||||||
|
* @returns {Event}
|
||||||
|
*/
|
||||||
function triggerMouse(target, event, process) {
|
function triggerMouse(target, event, process) {
|
||||||
var e = document.createEvent('MouseEvent')
|
var e = document.createEvent('MouseEvent')
|
||||||
e.initEvent('event', true, true)
|
e.initEvent('event', true, true)
|
||||||
@@ -33,6 +40,13 @@ function triggerMouse(target, event, process) {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate a Focus event.
|
||||||
|
* @param target
|
||||||
|
* @param event
|
||||||
|
* @param process
|
||||||
|
* @returns {Event}
|
||||||
|
*/
|
||||||
function triggerFocusEvent(target, event, process) {
|
function triggerFocusEvent(target, event, process) {
|
||||||
var e = document.createEvent('FocusEvent')
|
var e = document.createEvent('FocusEvent')
|
||||||
e.initEvent('event', true, true)
|
e.initEvent('event', true, true)
|
||||||
@@ -256,6 +270,35 @@ describe('Select.vue', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Filtering Options', () => {
|
||||||
|
it('should filter an array of strings', () => {
|
||||||
|
const vm = new Vue({
|
||||||
|
template: `<div><v-select ref="select" :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(['bar','baz'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter without case-sensitivity', () => {
|
||||||
|
const vm = new Vue({
|
||||||
|
template: `<div><v-select ref="select" :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(['Bar','Baz'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can filter an array of objects based on the objects label key', () => {
|
||||||
|
const vm = new Vue({
|
||||||
|
template: `<div><v-select ref="select" :options="[{label: 'Foo', value: 'foo'}, {label: 'Bar', value: 'bar'}, {label: 'Baz', value: 'baz'}]" v-model="value"></v-select></div>`,
|
||||||
|
data: {value: 'foo'}
|
||||||
|
}).$mount()
|
||||||
|
vm.$refs.select.search = 'ba'
|
||||||
|
expect(JSON.stringify(vm.$refs.select.filteredOptions)).toEqual(JSON.stringify([{label: 'Bar', value: 'bar'}, {label: 'Baz', value: 'baz'}]))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Toggling Dropdown', () => {
|
describe('Toggling Dropdown', () => {
|
||||||
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({
|
||||||
@@ -481,23 +524,26 @@ describe('Select.vue', () => {
|
|||||||
expect(vm.$children[0].scrollTo).toHaveBeenCalledWith(1)
|
expect(vm.$children[0].scrollTo).toHaveBeenCalledWith(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should scroll down if the pointer is below the current viewport bounds', () => {
|
/**
|
||||||
|
* @link https://github.com/vuejs/vue-loader/issues/434
|
||||||
|
* @todo vue-loader/inject-loader fails when used twice in the same file,
|
||||||
|
* so the mock here is abastracted to a separate file.
|
||||||
|
*/
|
||||||
|
xit('should scroll down if the pointer is below the current viewport bounds', () => {
|
||||||
let methods = Object.assign(pointerScroll.methods, {
|
let methods = Object.assign(pointerScroll.methods, {
|
||||||
pixelsToPointerBottom() {
|
pixelsToPointerBottom() {
|
||||||
return 2
|
return 2
|
||||||
},
|
},
|
||||||
viewport() {
|
viewport() {
|
||||||
return {top: 0, bottom: 1}
|
return {top: 0, bottom: 1}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
let mock = Mock({
|
|
||||||
'../mixins/pointerScroll': {methods}
|
|
||||||
})
|
})
|
||||||
const vm = new Vue({
|
const vm = new Vue({
|
||||||
template: `<div><v-select :options="['one', 'two', 'three']"></v-select></div>`,
|
template: `<div><v-select :options="['one', 'two', 'three']"></v-select></div>`,
|
||||||
components: {
|
components: {
|
||||||
'v-select': mock
|
'v-select': Mock({
|
||||||
|
'../mixins/pointerScroll': {methods}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}).$mount()
|
}).$mount()
|
||||||
|
|
||||||
@@ -510,19 +556,23 @@ describe('Select.vue', () => {
|
|||||||
describe('Measuring pixel distances', () => {
|
describe('Measuring pixel distances', () => {
|
||||||
it('should calculate pointerHeight as the offsetHeight of the pointer element if it exists', () => {
|
it('should calculate pointerHeight as the offsetHeight of the pointer element if it exists', () => {
|
||||||
const vm = new Vue({
|
const vm = new Vue({
|
||||||
template: '<div><v-select :options="[\'one\', \'two\', \'three\']""></v-select></div>',
|
template: `<div><v-select :options="['one', 'two', 'three']"></v-select></div>`,
|
||||||
components: {vSelect},
|
|
||||||
}).$mount()
|
}).$mount()
|
||||||
|
|
||||||
// Fresh instances start with the pointer at -1
|
// dropdown must be open for $refs to exist
|
||||||
vm.$children[0].typeAheadPointer = -1
|
vm.$children[0].open = true
|
||||||
expect(vm.$children[0].pointerHeight()).toEqual(0)
|
|
||||||
|
|
||||||
vm.$children[0].typeAheadPointer = 100
|
Vue.nextTick(() => {
|
||||||
expect(vm.$children[0].pointerHeight()).toEqual(0)
|
// Fresh instances start with the pointer at -1
|
||||||
|
vm.$children[0].typeAheadPointer = -1
|
||||||
|
expect(vm.$children[0].pointerHeight()).toEqual(0)
|
||||||
|
|
||||||
vm.$children[0].typeAheadPointer = 1
|
vm.$children[0].typeAheadPointer = 100
|
||||||
expect(vm.$children[0].pointerHeight()).toEqual(vm.$children[0].$refs.dropdownMenu.children[1].offsetHeight)
|
expect(vm.$children[0].pointerHeight()).toEqual(0)
|
||||||
|
|
||||||
|
vm.$children[0].typeAheadPointer = 1
|
||||||
|
expect(vm.$children[0].pointerHeight()).toEqual(vm.$children[0].$refs.dropdownMenu.children[1].offsetHeight)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -727,7 +777,6 @@ describe('Select.vue', () => {
|
|||||||
let two = 'two'
|
let two = 'two'
|
||||||
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>',
|
||||||
components: {vSelect},
|
|
||||||
data: {
|
data: {
|
||||||
value: null,
|
value: null,
|
||||||
options: ['one', two]
|
options: ['one', two]
|
||||||
@@ -747,7 +796,6 @@ describe('Select.vue', () => {
|
|||||||
let two = {label: 'two'}
|
let two = {label: 'two'}
|
||||||
const vm = new Vue({
|
const vm = new Vue({
|
||||||
template: '<div><v-select :options="options" taggable></v-select></div>',
|
template: '<div><v-select :options="options" taggable></v-select></div>',
|
||||||
components: {vSelect},
|
|
||||||
data: {
|
data: {
|
||||||
options: [{label: 'one'}, two]
|
options: [{label: 'one'}, two]
|
||||||
}
|
}
|
||||||
@@ -780,6 +828,45 @@ describe('Select.vue', () => {
|
|||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should not allow duplicate tags when using string options', (done) => {
|
||||||
|
const vm = new Vue({
|
||||||
|
template: `<div><v-select ref="select" taggable multiple></v-select></div>`,
|
||||||
|
}).$mount()
|
||||||
|
vm.$refs.select.search = 'one'
|
||||||
|
searchSubmit(vm)
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(vm.$refs.select.mutableValue).toEqual(['one'])
|
||||||
|
expect(vm.$refs.select.search).toEqual('')
|
||||||
|
vm.$refs.select.search = 'one'
|
||||||
|
searchSubmit(vm)
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(vm.$refs.select.mutableValue).toEqual(['one'])
|
||||||
|
expect(vm.$refs.select.search).toEqual('')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not allow duplicate tags when using object options', (done) => {
|
||||||
|
const vm = new Vue({
|
||||||
|
template: `<div><v-select ref="select" taggable multiple></v-select></div>`,
|
||||||
|
}).$mount()
|
||||||
|
vm.$refs.select.search = 'one'
|
||||||
|
searchSubmit(vm)
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(vm.$refs.select.mutableValue).toEqual(['one'])
|
||||||
|
expect(vm.$refs.select.search).toEqual('')
|
||||||
|
vm.$refs.select.search = 'one'
|
||||||
|
searchSubmit(vm)
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
expect(vm.$refs.select.mutableValue).toEqual(['one'])
|
||||||
|
expect(vm.$refs.select.search).toEqual('')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Asynchronous Loading', () => {
|
describe('Asynchronous Loading', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user