2
0
mirror of https://github.com/tenrok/vue-select.git synced 2026-05-26 04:34:04 +03:00

Merge branch 'master' into fix-969-add-get-selected-option-class

This commit is contained in:
Jeff
2020-03-16 08:44:31 -07:00
93 changed files with 7225 additions and 1642 deletions
+12
View File
@@ -0,0 +1,12 @@
## Pull Requests
Looks like you want to help out on vue-select.. awesome! Here's a few things to keep in mind when contributing.
- Vue Select uses semantic release to automate the release process. This means that good commit
messages are critical. If you're unfamiliar with [conventional changelog](https://github.com/ajoslin/conventional-changelog) you can run `yarn commit` to generate a commit message.
- It's almost always better to ask before you jump into a PR if you want to add new functionality
. It's not fun for anyone when you spend time on something that doesn't end up in the component.
- If your PR fixes or references an open issue, be sure to reference it in your message.
- If you're adding new functionality, make sure your code has good test coverage.
:tada: Thanks for contributing, and an even bigger thanks for reading these guidelines!
+2
View File
@@ -0,0 +1,2 @@
github: [sagalbot]
+28
View File
@@ -0,0 +1,28 @@
name: Release
on:
push:
branches:
- master
jobs:
release:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Test with Coverage
run: yarn test
- name: Build
run: yarn build
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
+44
View File
@@ -0,0 +1,44 @@
name: Test & Build
on: [pull_request]
jobs:
test:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Test with Coverage
run: yarn test --coverage --coverageReporters=lcov
- name: Report Coverage
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
build:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: |
yarn install --frozen-lockfile
cd docs
yarn install --frozen-lockfile
- name: Build Dist
run: yarn build
- name: Bundlewatch
run: npx bundlewatch
- name: Build Docs
run: yarn build:docs
+1
View File
@@ -2,6 +2,7 @@
node_modules
# local env files
.env
.env.local
.env.*.local
+4
View File
@@ -0,0 +1,4 @@
*
!src/**/*
!dist/**/*
.DS_Store
-8
View File
@@ -1,8 +0,0 @@
language: node_js
cache: yarn
node_js:
- "8"
script:
- yarn test --coverage --coverageReporters=text-lcov | coveralls
-10
View File
@@ -1,10 +0,0 @@
## Pull Requests
Looks like you want to help out on vue-select.. awesome! Here's a few things to keep in mind when contributing.
1. If your PR contains multiple commits, try to keep those commits succinct, with descriptive messages. This makes it easier to understand your thought process.
2. **Don't run the build** before submitting. The build is only run and committed immediately before a new release.
3. If your PR fixes or references an open issue, be sure to reference it in your message.
4. If you're adding new functionality, make sure your code has good test coverage.
:tada: Thanks for contributing, and an even bigger thanks for reading these guidelines!
+35 -16
View File
@@ -1,18 +1,22 @@
# vue-select ![Current Release](https://img.shields.io/github/release/sagalbot/vue-select.svg?style=flat-square) ![Bundle Size](https://flat.badgen.net/bundlephobia/min/vue-select) ![Monthly Downloads](https://img.shields.io/npm/dm/vue-select.svg?style=flat-square) ![Code Coverage](https://img.shields.io/coveralls/github/sagalbot/vue-select.svg?style=flat-square) ![Maintainability Score](https://img.shields.io/codeclimate/maintainability/sagalbot/vue-select.svg?style=flat-square) ![MIT License](https://img.shields.io/github/license/sagalbot/vue-select.svg?style=flat-square)
# vue-select ![Current Release](https://img.shields.io/github/release/sagalbot/vue-select.svg?style=flat-square) ![Bundle Size](https://flat.badgen.net/bundlephobia/min/vue-select) ![Monthly Downloads](https://img.shields.io/npm/dm/vue-select.svg?style=flat-square) [![Coverage Status](https://coveralls.io/repos/github/sagalbot/vue-select/badge.svg?branch=master)](https://coveralls.io/github/sagalbot/vue-select?branch=master) ![MIT License](https://img.shields.io/github/license/sagalbot/vue-select.svg?style=flat-square)
> **Everything you wish the HTML `<select>` element could do, wrapped up into a lightweight, zero
dependency, extensible Vue component.**
> **Everything you wish the HTML `<select>` element could do, wrapped up into a lightweight, zero
> dependency, extensible Vue component.**
Vue Select is a feature rich select/dropdown/typeahead component. It provides a default
template that fits most use cases for a filterable select dropdown. The component is designed to be as
lightweight as possible, while maintaining high standards for accessibility,
developer experience, and customization.
- Tagging
- Filtering / Searching
- Vuex Support
- AJAX Support
- SSR Support
- Accessible
- ~20kb Total / ~5kb CSS / ~15kb JS
- Select Single/Multiple Options
- Customizable with slots and SCSS variables
- Tested with Bootstrap 3/4, Bulma, Foundation
- +95% Test Coverage
- Zero dependencies
## Documentation
@@ -20,32 +24,47 @@ dependency, extensible Vue component.**
Complete documentation and examples available at https://vue-select.org.
- **[API Documentation](https://vue-select.org)**
- **[Sandbox Demo](https://vue-select.org/sandbox.html)**
- **[CodePen Template](http://codepen.io/sagalbot/pen/NpwrQO)**
- **[GitHub Projects](https://github.com/sagalbot/vue-select/projects)**
## Sponsors :tada:
It takes a lot of effort to maintain this project. If it has saved you development time, please consider [sponsoring the project](https://github.com/sponsors/sagalbot)
with GitHub sponsors!
Huge thanks to the [sponsors](https://github.com/sponsors/sagalbot) and [contributors](https://github.com/sagalbot/vue-select/graphs/contributors) that make Vue Select possible.
## Install
```bash
$ npm install vue-select
yarn add vue-select
# or use npm
npm install vue-select
```
Register the component
Then, import and register the component:
```js
import Vue from 'vue'
import vSelect from 'vue-select'
import Vue from "vue";
import vSelect from "vue-select";
Vue.component('v-select', vSelect)
Vue.component("v-select", vSelect);
```
You may now use the component in your markup
The component itself does not include any CSS. You'll need to include it separately:
```html
<v-select v-model="selected" :options="['Vue.js','React']"></v-select>
```js
import "vue-select/dist/vue-select.css";
```
You can also include vue-select directly in the browser. Check out the
Alternatively, you can import the scss for complete control of the component styles:
```scss
@import "vue-select/src/scss/vue-select.scss";
```
You can also include vue-select directly in the browser. Check out the
[documentation for loading from CDN.](https://vue-select.org/guide/install.html#in-the-browser).
## License
-5
View File
@@ -14,11 +14,6 @@ module.exports = merge(baseWebpackConfig, {
minimizer: [
new TerserPlugin({
sourceMap: true,
terserOptions: {
compress: {
drop_console: true,
},
},
}),
],
}
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div id="app">
<sandbox hide-help v-slot="config">
<v-select v-bind="config" />
<v-select v-bind="config"/>
</sandbox>
</div>
</template>
+3
View File
@@ -0,0 +1,3 @@
# personal access token used to fetch
# sponsor listings when working locally
GITHUB_TOKEN=
@@ -0,0 +1,16 @@
<template>
<v-select>
<template v-slot:no-options="{ search, searching }">
<template v-if="searching">
No results found for <em>{{ search }}</em>.
</template>
<em style="opacity: 0.5;" v-else>Start typing to search for a country.</em>
</template>
</v-select>
</template>
<script>
export default {
name: 'BetterNoOptions',
};
</script>
@@ -1,9 +1,10 @@
<template>
<div>
<v-select
placeholder="choose a country"
v-model="selected"
:options="['Canada', 'United States']"
:components="{Deselect}"
:options="['Canada', 'United States']"
/>
</div>
</template>
@@ -0,0 +1,63 @@
<template>
<ul>
<li v-for="{ login, avatar_url, html_url, contributions } in contributors">
<img :src="`${avatar_url}&s=75`" :alt="`${login}'s Avatar`" />
<div>
<a :href="html_url">@{{ login }}</a>
<br /><a
class="contributions-link"
:href="
`https://github.com/sagalbot/vue-select/commits?author=${login}`
"
>{{ contributions }} contributions</a
>
</div>
</li>
</ul>
</template>
<script>
import { CONTRIBUTORS } from "@dynamic/constants";
export default {
data: () => ({
contributors: CONTRIBUTORS.filter(
({ login }) => login !== "semantic-release-bot"
)
})
};
</script>
<style scoped>
ul {
list-style: none;
padding: 0;
max-width: 100%;
margin-top: 2rem;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
img {
width: 75px;
height: 75px;
margin-right: 1rem;
border-radius: 100%;
}
li {
display: inline-flex;
align-items: center;
margin-bottom: 2rem;
margin-right: 1rem;
}
a {
display: inline-block;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.contributions-link {
color: #2c5282;
}
</style>
@@ -0,0 +1,25 @@
<template>
<v-select
taggable
multiple
no-drop
:map-keydown="handlers"
placeholder="enter an email"
/>
</template>
<script>
export default {
name: 'CustomHandlers',
methods: {
handlers: (map, vm) => ({
...map, 50: e => {
e.preventDefault();
if( e.key === '@' && vm.search.length > 0 ) {
vm.search = `${vm.search}@gmail.com`;
}
},
}),
},
};
</script>
+31
View File
@@ -0,0 +1,31 @@
<template>
<v-select :filter="fuseSearch" :options="books" :getOptionLabel="option => option.title">
<template #option="{author, title}">
{{ title }} <br>
<cite>{{ author.firstName }} {{ author.lastName }}</cite>
</template>
</v-select>
</template>
<script>
import Fuse from 'fuse.js';
import books from '../data/books';
export default {
computed: {
books: () => books,
},
methods: {
fuseSearch (options, search) {
const fuse = new Fuse(options, {
keys: ['title', 'author.firstName', 'author.lastName'],
shouldSort: true,
});
return search.length ? fuse.search(search) : fuse.list;
},
},
};
</script>
<style scoped>
</style>
@@ -0,0 +1,74 @@
<template>
<v-select
:options="paginated"
:filterable="false"
@open="onOpen"
@close="onClose"
@search="query => search = query"
>
<template #list-footer v-if="hasNextPage">
<li ref="load" class="loader">
Loading more options...
</li>
</template>
</v-select>
</template>
<script>
import countries from '../data/countries';
export default {
name: "InfiniteScroll",
data: () => ({
observer: null,
limit: 10,
search: ''
}),
mounted () {
/**
* You could do this directly in data(), but since these docs
* are server side rendered, IntersectionObserver doesn't exist
* in that environment, so we need to do it in mounted() instead.
*/
this.observer = new IntersectionObserver(this.infiniteScroll);
},
computed: {
filtered () {
return countries.filter(country => country.includes(this.search));
},
paginated () {
return this.filtered.slice(0, this.limit);
},
hasNextPage () {
return this.paginated.length < this.filtered.length;
},
},
methods: {
async onOpen () {
if (this.hasNextPage) {
await this.$nextTick();
this.observer.observe(this.$refs.load)
}
},
onClose () {
this.observer.disconnect();
},
async infiniteScroll ([{isIntersecting, target}]) {
if (isIntersecting) {
const ul = target.offsetParent;
const scrollTop = target.offsetParent.scrollTop;
this.limit += 10;
await this.$nextTick();
ul.scrollTop = scrollTop;
}
}
}
}
</script>
<style scoped>
.loader {
text-align: center;
color: #bbbbbb;
}
</style>
@@ -0,0 +1,21 @@
<template>
<v-select
multiple
placeholder="Choose up to 3 books!"
label="title"
v-model="selected"
:options="books"
:selectable="() => selected.length < 3"
/>
</template>
<script>
import books from '../data/books';
export default {
data() {
return { selected: [] }
},
computed: {
books: () => books,
}
}
</script>
@@ -0,0 +1,43 @@
<template>
<table>
<tr>
<th>Name</th>
<th>Country</th>
</tr>
<tr v-for="person in people">
<td>{{ person.name }}</td>
<td>
<v-select
:options="options"
:value="person.country"
@input="country => updateCountry(person, country)"
/>
</td>
</tr>
</table>
</template>
<script>
import countries from '../data/countries';
export default {
data: () => ({
people: [{name: 'John', country: ''}, {name: 'Jane', country: ''}],
}),
methods: {
updateCountry (person, country) {
person.country = country;
},
},
computed: {
options: () => countries,
},
};
</script>
<style scoped>
table {
display: table;
width: 100%;
}
</style>
+49
View File
@@ -0,0 +1,49 @@
<template>
<v-select :options="paginated" @search="query => search = query" :filterable="false">
<li slot="list-footer" class="pagination">
<button @click="offset -= 10" :disabled="!hasPrevPage">Prev</button>
<button @click="offset += 10" :disabled="!hasNextPage">Next</button>
</li>
</v-select>
</template>
<script>
import countries from '../data/countries';
export default {
data: () => ({
countries,
search: '',
offset: 0,
limit: 10,
}),
computed: {
filtered () {
return this.countries.filter(country => country.includes(this.search));
},
paginated () {
return this.filtered.slice(this.offset, this.limit + this.offset);
},
hasNextPage () {
const nextOffset = this.offset + 10;
return Boolean(this.filtered.slice(nextOffset, this.limit + nextOffset).length);
},
hasPrevPage () {
const prevOffset = this.offset - 10;
return Boolean(this.filtered.slice(prevOffset, this.limit + prevOffset).length);
}
},
};
</script>
<style scoped>
.pagination {
display: flex;
margin: .25rem .25rem 0;
}
.pagination button {
flex-grow: 1;
}
.pagination button:hover {
cursor: pointer;
}
</style>
@@ -0,0 +1,83 @@
<template>
<div>
<v-select :options="countries" append-to-body :calculate-position="withPopper" />
<label for="position" style="display: block; margin: 1rem 0;">
<input
type="checkbox"
id="position"
v-model="placement"
true-value="top"
false-value="bottom"
>
Position dropdown above
</label>
</div>
</template>
<script>
import countries from '../data/countries'
import { createPopper } from '@popperjs/core';
export default {
data: () => ({countries, placement: 'top'}),
methods: {
withPopper (dropdownList, component, {width}) {
/**
* We need to explicitly define the dropdown width since
* it is usually inherited from the parent with CSS.
*/
dropdownList.style.width = width;
/**
* Here we position the dropdownList relative to the $refs.toggle Element.
*
* The 'offset' modifier aligns the dropdown so that the $refs.toggle and
* the dropdownList overlap by 1 pixel.
*
* The 'toggleClass' modifier adds a 'drop-up' class to the Vue Select
* wrapper so that we can set some styles for when the dropdown is placed
* above.
*/
const popper = createPopper(component.$refs.toggle, dropdownList, {
placement: this.placement,
modifiers: [
{
name: 'offset', options: {
offset: [0, -1]
}
},
{
name: 'toggleClass',
enabled: true,
phase: 'write',
fn ({state}) {
component.$el.classList.toggle('drop-up', state.placement === 'top')
},
}]
});
/**
* To prevent memory leaks Popper needs to be destroyed.
* If you return function, it will be called just before dropdown is removed from DOM.
*/
return () => popper.destroy();
}
}
};
</script>
<style>
.v-select.drop-up.vs--open .vs__dropdown-toggle {
border-radius: 0 0 4px 4px;
border-top-color: transparent;
border-bottom-color: rgba(60, 60, 60, 0.26);
}
[data-popper-placement='top'] {
border-radius: 4px 4px 0 0;
border-top-style: solid;
border-bottom-style: none;
box-shadow: 0 -3px 6px rgba(0, 0, 0, 0.15)
}
</style>
-5
View File
@@ -226,11 +226,6 @@ export default {
loading(false);
});
}, 250),
fuseSearch (options, search) {
return new Fuse(options, {
keys: ['title', 'author.firstName', 'author.lastName'],
}).search(search);
},
},
};
</script>
+7
View File
@@ -0,0 +1,7 @@
<template>
<v-select append-to-body>
<template #footer>
<div style="opacity: .8">Bottom of the component, in the footer slot!</div>
</template>
</v-select>
</template>
+7
View File
@@ -0,0 +1,7 @@
<template>
<v-select>
<template #header>
<div style="opacity: .8">Top of the component, in the header slot!</div>
</template>
</v-select>
</template>
@@ -0,0 +1,7 @@
<template>
<v-select>
<template #list-footer>
<li style="text-align: center">Bottom of the list!</li>
</template>
</v-select>
</template>
@@ -0,0 +1,7 @@
<template>
<v-select>
<template #list-header>
<li style="text-align: center">Top of the list!</li>
</template>
</v-select>
</template>
@@ -0,0 +1,7 @@
<template>
<v-select>
<template #no-options="{ search, searching, loading }">
This is the no options slot.
</template>
</v-select>
</template>
@@ -0,0 +1,7 @@
<template>
<v-select>
<template #open-indicator="{ attributes }">
<span v-bind="attributes">🔽</span>
</template>
</v-select>
</template>
+24
View File
@@ -0,0 +1,24 @@
<template>
<v-select :options="books" label="title">
<template #option="{ title, author }">
<h3 style="margin: 0">{{ title }}</h3>
<em>{{ author.firstName }} {{ author.lastName }}</em>
</template>
</v-select>
</template>
<script>
export default {
data: () => ({
books: [
{
title: "Old Man's War",
author: {
firstName: "John",
lastName: "Scalzi"
}
}
]
})
}
</script>
+12
View File
@@ -0,0 +1,12 @@
<template>
<v-select>
<template #search="{ attributes, events }">
<input
maxlength="1"
class="vs__search"
v-bind="attributes"
v-on="events"
>
</template>
</v-select>
</template>
@@ -0,0 +1,26 @@
<template>
<v-select v-model="selected" :options="books" label="title">
<template #selected-option="{ title, author }">
<div style="display: flex; align-items: baseline;">
<strong>{{ title }}</strong>
<em style="margin-left: .5rem;">by {{ author.firstName }} {{ author.lastName }}</em>
</div>
</template>
</v-select>
</template>
<script>
const book = {
title: "Old Man's War",
author: {
firstName: "John",
lastName: "Scalzi"
}
};
export default {
data: () => ({
books: [book],
selected: book
})
}
</script>
@@ -0,0 +1,23 @@
<template>
<v-select :options="books" label="title">
<template #selected-option-container="{ option, deselect, multiple, disabled }">
<div class="vs__selected">{{ option.title }}</div>
</template>
</v-select>
</template>
<script>
export default {
data: () => ({
books: [
{
title: "Old Man's War",
author: {
firstName: "John",
lastName: "Scalzi"
}
}
]
})
}
</script>
@@ -0,0 +1,9 @@
<template>
<v-select :loading="true">
<template #spinner="{ loading }">
<div v-if="loading" style="border-left-color: rgba(88,151,251,0.71)" class="vs__spinner">
The .vs__spinner class will hide the text for me.
</div>
</template>
</v-select>
</template>
@@ -0,0 +1,25 @@
<template>
<p class="sponsor">
Are you using Vue Select on a lot of projects? Please consider
<a href="https://github.com/sponsors/sagalbot" target="_blank">
sponsoring @sagalbot!
</a>
</p>
</template>
<style scoped>
.sponsor {
display: block;
border: 2px solid #e2e8f0;
background: #f7fafc;
border-radius: 10px;
padding: 0.25rem;
font-weight: 600;
text-align: center;
margin: 0 auto;
color: #8492a4;
}
p a {
color: #48BB78;
}
</style>
+146
View File
@@ -0,0 +1,146 @@
<template>
<div class="sponsor-me">
<div class="avatar">
<a href="https://github.com/sponsors/sagalbot">
<img
src="https://avatars2.githubusercontent.com/u/692538?s=400&u=a5ab0d164266bd2d59ce1a514835627b4cc4f24f&v=4"
alt="Jeff Sagal's Avatar"
/>
</a>
</div>
<div class="cta">
<p style="font-size: 1.2rem; font-weight: 600;">
Hi! I'm Jeff Sagal, the author of Vue Select.
</p>
<p>
I've spent hundreds of hours working alongside contributors to make Vue
Select the best it can be. I've researched UX and accessibility
patterns, squashed bugs, reviewed code, tested-cross browser, and spent
many evenings and weekends working on these docs.
</p>
<p>
If it's saved you time on your projects, please consider supporting me
on GitHub sponsors.
</p>
<div class="links">
<a
href="https://github.com/sponsors/sagalbot"
class="button"
target="_blank"
>
<svg viewBox="0 0 20 20">
<path
d="M10 0a10 10 0 00-3.16 19.49c.5.1.68-.22.68-.48l-.01-1.7c-2.78.6-3.37-1.34-3.37-1.34-.46-1.16-1.11-1.47-1.11-1.47-.9-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.1-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.1.39-1.99 1.03-2.69a3.6 3.6 0 01.1-2.64s.84-.27 2.75 1.02a9.58 9.58 0 015 0c1.91-1.3 2.75-1.02 2.75-1.02.55 1.37.2 2.4.1 2.64.64.7 1.03 1.6 1.03 2.69 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85l-.01 2.75c0 .26.18.58.69.48A10 10 0 0010 0"
/>
</svg>
Sponsor Vue Select!
</a>
<a href="https://twitter.com/sagalbot" target="_blank" class="social-link">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M23.954 4.569a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.691 8.094 4.066 6.13 1.64 3.161a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.061a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.937 4.937 0 004.604 3.417 9.868 9.868 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.054 0 13.999-7.496 13.999-13.986 0-.209 0-.42-.015-.63a9.936 9.936 0 002.46-2.548l-.047-.02z"
/>
</svg>
Follow @sagalbot
</a>
</div>
</div>
</div>
</template>
<style scoped>
.sponsor-me {
display: flex;
border: 2px solid #c3dafe;
background: #ebf4ff;
border-radius: 10px;
/*align-items: top;*/
padding: 2rem 1rem;
margin: 3rem 0;
flex-direction: column;
}
.avatar {
display: flex;
align-items: center;
justify-content: space-evenly;
padding: 1rem;
}
.cta {
/*text-align: center;*/
}
@media (min-width: 1150px) {
.sponsor-me {
flex-direction: row;
}
.avatar {
display: flex;
justify-content: flex-start;
flex-direction: column;
margin: 0 2rem;
}
.cta {
text-align: left;
}
}
p {
margin-top: 0;
}
.avatar img {
max-width: 150px;
border-radius: 100%;
}
.button {
display: inline-flex;
text-align: center;
align-items: center;
padding: 0.75rem 1rem;
background: #63b3ed;
color: #fff;
font-weight: 600;
border-radius: 5px;
margin-top: 0.5rem;
transition: background-color 0.25s, box-shadow 0.25s;
}
.button svg {
max-width: 25px;
fill: currentColor;
margin-right: 0.5rem;
}
a.button:hover {
box-shadow: inset 0px 0px 3px #3182ce;
background: #90cdf4;
text-decoration: none;
text-shadow: 0 1px 1px #63b3ed;
}
.links {
display: flex;
flex-direction: column;
align-items: center;
}
.links a {
margin: 1rem;
}
@media (min-width: 1150px) {
.links {
flex-direction: row;
justify-content: flex-start;
}
.links a {
margin: 0 2rem 0 0;
}
}
.social-link {
display: flex;
align-items: center;
color: #3182ce;
}
.social-link svg {
fill: currentColor;
width: 20px;
margin-right: 0.5rem;
}
.social-link:hover {
color: #2c5282;
text-decoration: none;
}
</style>
+56
View File
@@ -0,0 +1,56 @@
<template>
<ul>
<li v-for="{ createdAt, login, avatarUrl } in sponsors">
<img :src="avatarUrl + '&s=150'" :alt="`@${login}'s avatar`" />
<p>
<a :href="`https://github.com/${login}`">@{{ login }}</a> <br />
Sponsor since {{ createdAt }}
</p>
</li>
</ul>
</template>
<script>
import { SPONSORS } from "@dynamic/constants";
import { format } from "date-fns";
export default {
data: () => ({
sponsors: SPONSORS.map(({ createdAt, sponsor }) => ({
createdAt: format(new Date(createdAt), "LLL yyyy"),
...sponsor
}))
})
};
</script>
<style scoped>
ul {
list-style: none;
padding: 0;
max-width: 100%;
margin-top: 2rem;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
img {
width: 75px;
height: 75px;
margin-right: 1rem;
border-radius: 100%;
}
li {
display: inline-flex;
align-items: center;
margin-bottom: 2rem;
/*max-width: 220px;*/
}
a {
display: inline-block;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
+4
View File
@@ -0,0 +1,4 @@
<template>
<!-- tag on 188/comma & 13/return -->
<v-select no-drop taggable multiple :select-on-key-codes="[188, 13]" />
</template>
@@ -0,0 +1,16 @@
<template>
<v-select
placeholder="Choose a book to read"
label="title"
:options="books"
:selectable="option => ! option.author.lastName.includes('Woodhouse')"
/>
</template>
<script>
import books from '../data/books';
export default {
computed: {
books: () => books,
}
}
</script>
+8 -132
View File
@@ -1,137 +1,13 @@
const isDeployPreview = process.env.hasOwnProperty('DEPLOY_PREVIEW');
const meta = {
title: 'Vue Select | VueJS Select2/Chosen Component',
description: 'Everything you wish the HTML select element could do, wrapped up into a lightweight, extensible Vue component.',
url: 'https://vue-select.org',
};
let head = [
[
'link',
{
href: '//fonts.googleapis.com/css?family=Source+Sans+Pro:400,600|Roboto Mono',
rel: 'stylesheet',
type: 'text/css',
}],
[
'link',
{
href: '//fonts.googleapis.com/css?family=Dosis:300&amp;text=Vue Select',
rel: 'stylesheet',
type: 'text/css',
}],
['link', {rel: 'icon', href: `/vue-logo.png`}],
['meta', {name: 'theme-color', content: '#3eaf7c'}],
['meta', {name: 'apple-mobile-web-app-capable', content: 'yes'}],
['meta', {name: 'apple-mobile-web-app-status-bar-style', content: 'black'}],
[
'link',
{rel: 'apple-touch-icon', href: `/icons/apple-touch-icon-152x152.png`}],
[
'link',
{rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c'}],
[
'meta',
{
name: 'msapplication-TileImage',
content: '/icons/msapplication-icon-144x144.png',
}],
['meta', {name: 'msapplication-TileColor', content: '#000000'}],
['meta', {name: 'title', content: meta.title}],
['meta', {name: 'description', content: meta.description}],
['link', {rel: 'icon', href: meta.icon, type: 'image/png'}],
['meta', {property: 'og:image', content: meta.icon}],
['meta', {property: 'twitter:image', content: meta.icon}],
['meta', {name: 'description', content: meta.description}],
['meta', {property: 'og:description', content: ''}],
['meta', {property: 'twitter:description', content: meta.description}],
['meta', {property: 'twitter:title', content: meta.title}],
['meta', {property: 'og:title', content: meta.title}],
['meta', {property: 'og:site_name', content: meta.title}],
['meta', {property: 'og:url', content: meta.url}],
];
if (isDeployPreview) {
head.push(
['meta', {name: 'robots', content: 'noindex'}],
['meta', {name: 'googlebot', content: 'noindex'}],
);
}
const {description} = require('./config/meta');
const head = require('./config/head');
const plugins = require('./config/plugins');
const themeConfig = require('./config/themeConfig');
module.exports = {
title: 'Vue Select',
description: meta.description,
description,
head,
plugins: {
'@vuepress/google-analytics': {
ga: isDeployPreview ? '' : 'UA-12818324-8',
},
'@vuepress/pwa': {
serviceWorker: false,
updatePopup: true,
},
'@vuepress/plugin-register-components': {},
'@vuepress/plugin-active-header-links': {},
'@vuepress/plugin-search': {},
'@vuepress/plugin-nprogress': {},
},
themeConfig: {
repo: 'sagalbot/vue-select',
editLinks: true,
docsDir: 'docs',
nav: [
{text: 'Home', link: '/'},
{text: 'Sandbox', link: '/sandbox'},
],
sidebar: {
'/': [
{
title: 'Getting Started',
collapsable: false,
children: [
['guide/install', 'Installation'],
['guide/options', 'Dropdown Options'],
['guide/values', 'Selecting Values'],
['guide/upgrading', 'Upgrading 2.x to 3.x'],
],
},
{
title: 'Templating & Styling',
collapsable: false,
children: [
['guide/components', 'Child Components'],
['guide/css', 'CSS & Selectors'],
['guide/slots', 'Slots'],
],
},
{
title: 'Accessibility',
collapsable: false,
children: [
['guide/accessibility', 'WAI-ARIA Spec'],
['guide/localization', 'Localization'],
],
},
{
title: 'Digging Deeper',
collapsable: false,
children: [
['guide/validation', 'Validation'],
['guide/vuex', 'Vuex'],
['guide/ajax', 'AJAX'],
],
},
{
title: 'API',
collapsable: false,
children: [
['api/props', 'Props'],
['api/slots', 'Slots'],
['api/events', 'Events'],
],
},
],
},
},
plugins,
themeConfig,
};
+57
View File
@@ -0,0 +1,57 @@
const isDeployPreview = require('./isDeployPreview');
const meta = require('./meta');
const head = [
[
'link',
{
href: '//fonts.googleapis.com/css?family=Source+Sans+Pro:400,600|Roboto Mono',
rel: 'stylesheet',
type: 'text/css',
}],
[
'link',
{
href: '//fonts.googleapis.com/css?family=Dosis:300&amp;text=Vue Select',
rel: 'stylesheet',
type: 'text/css',
}],
['link', {rel: 'icon', href: `/vue-logo.png`}],
['meta', {name: 'theme-color', content: '#3eaf7c'}],
['meta', {name: 'apple-mobile-web-app-capable', content: 'yes'}],
['meta', {name: 'apple-mobile-web-app-status-bar-style', content: 'black'}],
[
'link',
{rel: 'apple-touch-icon', href: `/icons/apple-touch-icon-152x152.png`}],
[
'link',
{rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c'}],
[
'meta',
{
name: 'msapplication-TileImage',
content: '/icons/msapplication-icon-144x144.png',
}],
['meta', {name: 'msapplication-TileColor', content: '#000000'}],
['meta', {name: 'title', content: meta.title}],
['meta', {name: 'description', content: meta.description}],
['link', {rel: 'icon', href: meta.icon, type: 'image/png'}],
['meta', {property: 'og:image', content: meta.icon}],
['meta', {property: 'twitter:image', content: meta.icon}],
['meta', {name: 'description', content: meta.description}],
['meta', {property: 'og:description', content: ''}],
['meta', {property: 'twitter:description', content: meta.description}],
['meta', {property: 'twitter:title', content: meta.title}],
['meta', {property: 'og:title', content: meta.title}],
['meta', {property: 'og:site_name', content: meta.title}],
['meta', {property: 'og:url', content: meta.url}],
];
if (isDeployPreview) {
head.push(
['meta', {name: 'robots', content: 'noindex'}],
['meta', {name: 'googlebot', content: 'noindex'}],
);
}
module.exports = head;
+1
View File
@@ -0,0 +1 @@
module.exports = process.env.hasOwnProperty('DEPLOY_PREVIEW');
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
title: 'Vue Select | VueJS Select2/Chosen Component',
description: 'Everything you wish the HTML select element could do, wrapped up into a lightweight, extensible Vue component.',
url: 'https://vue-select.org',
icon: '/vue-logo.png'
};
+22
View File
@@ -0,0 +1,22 @@
const isDeployPreview = require("./isDeployPreview");
module.exports = [
[
"@vuepress/google-analytics",
{
ga: isDeployPreview ? "" : "UA-12818324-8"
}
],
[
"@vuepress/pwa",
{
serviceWorker: false,
updatePopup: true
}
],
"@vuepress/plugin-register-components",
"@vuepress/plugin-active-header-links",
"@vuepress/plugin-search",
"@vuepress/plugin-nprogress",
require('../github/index')
];
+78
View File
@@ -0,0 +1,78 @@
module.exports = {
repo: 'sagalbot/vue-select',
editLinks: true,
docsDir: 'docs',
nav: [
{text: 'Sandbox', link: '/sandbox'},
],
sidebar: {
'/': [
{
title: 'Community',
collapsable: false,
children: [
['sponsors', 'Sponsors 🎉'],
['contributors', 'Contributors'],
],
},
{
title: 'Getting Started',
collapsable: false,
children: [
['guide/install', 'Installation'],
['guide/options', 'Dropdown Options'],
['guide/values', 'Selecting Values'],
['guide/upgrading', 'Upgrading 2.x to 3.x'],
],
},
{
title: 'Templating & Styling',
collapsable: false,
children: [
['guide/components', 'Child Components'],
['guide/css', 'CSS & Selectors'],
['guide/slots', 'Slots'],
],
},
{
title: 'Accessibility',
collapsable: false,
children: [
['guide/accessibility', 'WAI-ARIA Spec'],
['guide/localization', 'Localization'],
],
},
{
title: 'Use Cases',
collapsable: false,
children: [
['guide/validation', 'Validation'],
['guide/selectable', 'Limiting Selections'],
['guide/pagination', 'Pagination'],
['guide/infinite-scroll', 'Infinite Scroll'],
['guide/vuex', 'Vuex'],
['guide/ajax', 'AJAX'],
['guide/loops', 'Using in Loops'],
],
},
{
title: 'Customizing',
collapsable: false,
children: [
['guide/keydown', 'Keydown Events'],
['guide/positioning', 'Dropdown Position'],
['guide/filtering', 'Option Filtering'],
],
},
{
title: 'API',
collapsable: false,
children: [
['api/props', 'Props'],
['api/slots', 'Slots'],
['api/events', 'Events'],
],
},
],
},
};
@@ -0,0 +1,57 @@
require("dotenv").config();
const axios = require("axios");
const { graphql } = require("@octokit/graphql");
module.exports = async () => ({
name: "constants.js",
content: `
export const SPONSORS = ${JSON.stringify(await getSponsors())};
export const CONTRIBUTORS = ${JSON.stringify(await getContributors())};
`
});
/**
* Get a list of vue select contributors.
* @return {Promise<T>}
*/
async function getContributors() {
const { data } = await axios.get(
"https://api.github.com/repos/sagalbot/vue-select/contributors?per_page=100"
);
return data;
}
/**
* Get a list of the current sponsors. Requires GITHUB_TOKEN to be set.
* @return {Promise<*[]|ProfileNode[]|postcss.ChildNode[]|Array<parser.Node>|[]>}
*/
async function getSponsors() {
const query = `
{
user(login: "sagalbot") {
sponsorshipsAsMaintainer(first: 100) {
nodes {
createdAt
sponsor {
login
avatarUrl
}
}
}
}
}
`;
try {
const { user } = await graphql(query, {
headers: {
authorization: `token ${process.env.GITHUB_TOKEN || ""}`
}
});
return user.sponsorshipsAsMaintainer.nodes;
} catch (e) {
console.log(`${e.status} ${e.name} - Couldn't fetch sponsor data.`);
return [];
}
}
+5
View File
@@ -0,0 +1,5 @@
const clientDynamicModules = require('./clientDynamicModules');
module.exports = {
clientDynamicModules: async () => await clientDynamicModules(),
};
+23 -18
View File
@@ -1,38 +1,43 @@
<SponsorBanner />
# Vue Select
![Current Release](https://img.shields.io/github/release/sagalbot/vue-select.svg?style=flat-square)
![Bundle Size](https://flat.badgen.net/bundlephobia/min/vue-select)
![Monthly Downloads](https://img.shields.io/npm/dm/vue-select.svg?style=flat-square)
![Code Coverage](https://img.shields.io/coveralls/github/sagalbot/vue-select.svg?style=flat-square)
![Maintainability Score](https://img.shields.io/codeclimate/maintainability/sagalbot/vue-select.svg?style=flat-square)
![MIT License](https://img.shields.io/github/license/sagalbot/vue-select.svg?style=flat-square)
![Bundle Size](https://flat.badgen.net/bundlephobia/min/vue-select)
![Monthly Downloads](https://img.shields.io/npm/dm/vue-select.svg?style=flat-square)
![Coverage Status](https://coveralls.io/repos/github/sagalbot/vue-select/badge.svg?branch=master)
![MIT License](https://img.shields.io/github/license/sagalbot/vue-select.svg?style=flat-square)
> Everything you wish the HTML `<select>` element could do, wrapped
up into a lightweight, extensible Vue component.
> Everything you wish the HTML `<select>` element could do, wrapped
> up into a lightweight, extensible Vue component.
Vue Select is a feature rich select/dropdown/typeahead component. It provides a default
template that fits the 80% use case for a select dropdown. Here it is by default:
template that fits most use cases for a filterable select dropdown. The component is designed to be as
lightweight as possible, while maintaining high standards for accessibility,
developer experience, and customization.
<div style="max-width:25rem; margin: 0 auto; padding: 1rem 0;">
<country-select />
</div>
If you want to get a quick sense of what vue-select can do, check out
[the sandbox](sandbox.md).
Vue Select aims to be as lightweight as possible, while maintaining high standards for accessibility,
developer experience, and customization. Huge thanks to the [sponsors](sponsors.md) and
[contributors](contributors.md) that make Vue Select possible!
## Features
#### Features
- Tagging
- Filtering/Searching
- Filtering / Searching
- Vuex Support
- AJAX Support
- SSR Support
- Select Single/Multiple Options
- Tested with Bootstrap 3/4, Bulma, Foundation
- +95% Test Coverage
- Accessible
- ~20kb Total / ~5kb CSS / ~15kb JS
- Select Single/Multiple Options
- Customizable with slots and SCSS variables
- Zero dependencies
#### Resources
- **[CodePen Template](http://codepen.io/sagalbot/pen/NpwrQO)**
## Resources
- **[GitHub](https://github.com/sagalbot/vue-select)**
- **[Projects](https://github.com/sagalbot/vue-select/projects)**
- **[CodePen Template](http://codepen.io/sagalbot/pen/NpwrQO)**
+70 -3
View File
@@ -1,3 +1,18 @@
## appendToBody <Badge text="v3.7.0+" />
Append the dropdown element to the end of the body
and size/position it dynamically. Use it if you have
overflow or z-index issues.
See [Dropdown Position](../guide/positioning.md) for more details.
```js
appendToBody: {
type: Boolean,
default: false
},
```
## value
Contains the currently selected value. Very similar to a
@@ -109,6 +124,34 @@ transition: {
},
```
## calculatePosition <Badge text="v3.7.0+" />
When `appendToBody` is true, this function is responsible for positioning the drop down list.
If a function is returned from `calculatePosition`, it will be called when the drop down list
is removed from the DOM. This allows for any garbage collection you may need to do.
See [Dropdown Position](../guide/positioning.md) for more details.
```js
calculatePosition: {
type: Function,
/**
* @param dropdownList {HTMLUListElement}
* @param component {Vue} current instance of vue select
* @param width {string} calculated width in pixels of the dropdown menu
* @param top {string} absolute position top value in pixels relative to the document
* @param left {string} absolute position left value in pixels relative to the document
* @return {function|void}
*/
default(dropdownList, component, {width, top, left}) {
dropdownList.style.top = top;
dropdownList.style.left = left;
dropdownList.style.width = width;
}
}
```
## clearSearchOnSelect
Enables/disables clearing the search text when an option is selected.
@@ -120,6 +163,19 @@ clearSearchOnSelect: {
},
```
## clearSearchOnBlur
Enables/disables clearing the search text when the search input is blurred.
```js
clearSearchOnBlur: {
type: Function,
default: function ({ clearSearchOnSelect, multiple }) {
return clearSearchOnSelect && !multiple
}
},
```
## closeOnSelect
Close a dropdown when an option is chosen. Set to false to keep the dropdown
@@ -339,12 +395,22 @@ createOption: {
## resetOnOptionsChange
When false, updating the options will not reset the select value
When false, updating the options will not reset the selected value.
Since `v3.4+` the prop accepts either a `boolean` or `function` that returns a `boolean`.
If defined as a function, it will receive the params listed below.
```js
/**
* @type {Boolean|Function}
* @param {Array} newOptions
* @param {Array} oldOptions
* @param {Array} selectedValue
*/
resetOnOptionsChange: {
type: Boolean,
default: false
default: false,
validator: (value) => ['function', 'boolean'].includes(typeof value)
},
```
@@ -389,3 +455,4 @@ selectOnTab: {
type: Boolean,
default: false
}
```
+166 -43
View File
@@ -3,63 +3,186 @@ Vue Select leverages scoped slots to allow for total customization of the presen
Slots can be used to change the look and feel of the UI, or to simply swap out text.
:::
## Selected Option(s)
<style>
.slot-docs h2 {
border-top: 1px solid #f0f0f0;
border-bottom: none;
margin-top: 2rem;
padding-top: 2rem;
}
.slot-docs h2:first-child {
border-top: none;
margin-top: 0;
}
</style>
### `selected-option`
<div class="slot-docs">
#### Scope:
## `footer` <Badge text="3.8.0+" />
- `option {Object}` - A selected option
Displayed at the bottom of the component, below `.vs__dropdown-toggle`.
```html
<slot name="selected-option" v-bind="(typeof option === 'object')?option:{[label]: option}">
{{ getOptionLabel(option) }}
</slot>
When implementing this slot, you'll likely need to use `appendToBody` to position the dropdown.
Otherwise content in this slot will affect it's positioning.
- `search {string}` - the current search query
- `loading {boolean}` - is the component loading
- `searching {boolean}` - is the component searching
- `filteredOptions {array}` - options filtered by the search text
- `deselect {function}` - function to deselect an option
<SlotFooter />
<<< @/.vuepress/components/SlotFooter.vue
## `header` <Badge text="3.8.0+" />
Displayed at the top of the component, above `.vs__dropdown-toggle`.
- `search {string}` - the current search query
- `loading {boolean}` - is the component loading
- `searching {boolean}` - is the component searching
- `filteredOptions {array}` - options filtered by the search text
- `deselect {function}` - function to deselect an option
<SlotHeader />
<<< @/.vuepress/components/SlotHeader.vue
## `list-footer` <Badge text="3.8.0+" />
Displayed as the last item in the dropdown. No content by default. Parent element is the `<ul>`,
so this slot should contain a root `<li>`.
- `search {string}` - the current search query
- `loading {boolean}` - is the component loading
- `searching {boolean}` - is the component searching
- `filteredOptions {array}` - options filtered by the search text
<SlotListFooter />
<<< @/.vuepress/components/SlotListFooter.vue
## `list-header` <Badge text="3.8.0+" />
Displayed as the first item in the dropdown. No content by default. Parent element is the `<ul>`,
so this slot should contain a root `<li>`.
- `search {string}` - the current search query
- `loading {boolean}` - is the component loading
- `searching {boolean}` - is the component searching
- `filteredOptions {array}` - options filtered by the search text
<SlotListHeader />
<<< @/.vuepress/components/SlotListHeader.vue
## `no-options`
The no options slot is displayed above `list-footer` in the dropdown when
`filteredOptions.length === 0`.
- `search {string}` - the current search query
- `loading {boolean}` - is the component loading
- `searching {boolean}` - is the component searching
<SlotNoOptions />
<<< @/.vuepress/components/SlotNoOptions.vue
## `open-indicator`
The open indicator is the caret icon on the component used to indicate dropdown status.
```js
attributes: {
'ref': 'openIndicator',
'role': 'presentation',
'class': 'vs__open-indicator',
}
```
### `selected-option-container`
<SlotOpenIndicator />
<<< @/.vuepress/components/SlotOpenIndicator.vue
#### Scope:
## `option`
The current option within the dropdown, contained within `<li>`.
- `option {Object}` - The currently iterated option from `filteredOptions`
<SlotOption />
<<< @/.vuepress/components/SlotOption.vue
## `search`
The search input has a lot of bindings, but they're grouped into `attributes` and `events`. Most
of the time, you will just be binding those two with `v-on="events"` and `v-bind="attributes"`.
If you want the default styling, you'll need to add `.vs__search` to the input you provide.
```js
/**
* Attributes to be bound to a search input.
*/
attributes: {
'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`,
'aria-activedescendant': this.typeAheadPointer > -1
? `vs${this.uid}__option-${this.typeAheadPointer}`
: '',
'ref': 'search',
'type': 'search',
'autocomplete': this.autocomplete,
'value': this.search,
},
/**
* Events that this element should handle.
*/
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,
}
```
<SlotSearch />
<<< @/.vuepress/components/SlotSearch.vue{5-6}
## `selected-option`
The text displayed within `selected-option-container`.
This slot doesn't exist if `selected-option-container` is implemented.
- `option {Object}` - A selected option
<SlotSelectedOption />
<<< @/.vuepress/components/SlotSelectedOption.vue
## `selected-option-container`
This is the root element where `v-for="option in selectedValue"`. Most of the time you'll want to
use `selected-option`, but this container is useful if you want to disable the deselect button,
or have fine grain control over the markup.
- `option {Object}` - Currently iterated selected option
- `deselect {Function}` - Method used to deselect a given option when `multiple` is true
- `disabled {Boolean}` - Determine if the component is disabled
- `multiple {Boolean}` - If the component supports the selection of multiple values
```html
<slot v-for="option in valueAsArray" name="selected-option-container"
:option="(typeof option === 'object')?option:{[label]: option}" :deselect="deselect" :multiple="multiple" :disabled="disabled">
<span class="selected-tag" v-bind:key="option.index">
<slot name="selected-option" v-bind="(typeof option === 'object')?option:{[label]: option}">
{{ getOptionLabel(option) }}
</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>
```
<SlotSelectedOptionContainer />
<<< @/.vuepress/components/SlotSelectedOptionContainer.vue
## Component Actions
## `spinner`
### `spinner`
- `loading {Boolean}` - if the component is in a loading state
```html
<slot name="spinner">
<div class="spinner" v-show="mutableLoading">Loading...</div>
</slot>
```
<SlotSpinner />
<<< @/.vuepress/components/SlotSpinner.vue
## Dropdown
### `option`
#### Scope:
- `option {Object}` - The currently iterated option from `filteredOptions`
```html
<slot name="option" v-bind="(typeof option === 'object')?option:{[label]: option}">
{{ getOptionLabel(option) }}
</slot>
```
</div>
+10
View File
@@ -0,0 +1,10 @@
---
sidebarDepth: 0
---
# Contributors
Vue Select is supported by a community of awesome contributors! Without their contributions,
the package would not be what it is today.
<Contributors />
+1 -1
View File
@@ -2,7 +2,7 @@ Vue Select aims to follow the WAI-ARIA best practices for the
[combobox](https://www.w3.org/TR/wai-aria-practices-1.1/#combobox) and
[listbox](https://www.w3.org/TR/wai-aria-practices-1.1/#Listbox) widgets.
The UX of the component isdesigned around the HTML `<select>` element, while following the WAI-ARIA
The UX of the component is designed around the HTML `<select>` element, while following the WAI-ARIA
specifications and best practices for creating accessible components.
## Combobox
+21
View File
@@ -34,4 +34,25 @@ all instances of Vue Select, or add your own classname if you just want to affec
<<< @/.vuepress/components/CssSpecificity.vue
## Dropdown Transition
By default, the dropdown transitions with a `.15s` cubic-bezier opacity fade in/out. The component
uses the [VueJS transition system](https://vuejs.org/v2/guide/transitions.html). By default, the
transition name is `vs__fade`. There's a couple ways to override or change this transition.
1. Use the `transition` prop. Applying this prop will change the name of the animation classes and
negate the default CSS. If you want to remove it entirely, you can set it to an empty string.
```html
<v-select transition="" />
```
2. You can also override the default CSS for the `vs__fade` transition. Again, if you
wanted to eliminate the transition entirely:
```css
.vs__fade-enter-active,
.vs__fade-leave-active {
transition: none;
}
```
+19
View File
@@ -0,0 +1,19 @@
Vue Select provides two props accepting `functions` that can be used to implement custom filtering
algorithms.
- `filter` <Badge text="v2.5.0+" />
- `filterBy` <Badge text="v2.5.0+" />
By default, the component will perform a very basic check to see if an options label includes
the current search text. If you're using scoped slots, you might have information within the
option templates that should be searchable. Or maybe you just want a better search algorithm that
can do fuzzy search matching.
## Filtering with Fuse.js
You can use the `filter` and `filterBy` props to hook right into something like
[Fuse.js](https://fusejs.io/) that can handle searching multiple object keys with fuzzy matchings.
<FuseFilter />
<<< @/.vuepress/components/FuseFilter.vue
+23
View File
@@ -0,0 +1,23 @@
Vue Select doesn't ship with first party support for infinite scroll, but it's possible to implement
by hooking into the `open`, `close`, and `search` events, along with the `filterable` prop, and the
`list-footer` slot.
Let's break down the example below, starting with the `data`.
- `observer` - a new `IntersectionObserver` with `infiniteScroll` set as the callback
- `limit` - the number of options to display
- `search` - since we've disabled Vue Selects filtering, we'll need to filter options ourselves
When Vue Select opens, the `open` event is emitted and `onOpen` will be called. We wait for
`$nextTick()` so that the `$ref` we need will exist, then begin observing it for intersection.
The observer is set to call `infiniteScroll` when the `<li>` is completely visible within the list.
Some fancy destructuring is done here to get the first `ObservedEntry`, and specifically the
`isIntersecting` & `target` properties. If the `<li>` is intersecting, we increase the `limit`, and
ensure that the scroll position remains where it was before the list size changed. Again, it's
important to wait for `$nextTick` here so that the DOM elements have been inserted before setting
the scroll position.
<InfiniteScroll />
<<< @/.vuepress/components/InfiniteScroll.vue
+73
View File
@@ -0,0 +1,73 @@
### Customizing Keydown Behaviour
---
## selectOnKeyCodes <Badge text="v3.3.0+" />
`selectOnKeyCodes {Array}` is an array of keyCodes that will trigger a typeAheadSelect. Any keyCodes
in this array will prevent the default event action and trigger a typeahead select. By default,
it's just `[13]` for return. For example, maybe you want to tag on a comma keystroke:
<TagOnComma />
<<< @/.vuepress/components/TagOnComma.vue
## mapKeyDown <Badge text="v3.3.0+" />
Vue Select provides the `map-keydown` Function prop to allow for customizing the components response to
keydown events while the search input has focus.
```js
/**
* @param map {Object} Mapped keyCode to handlers { <keyCode>:<callback> }
* @param vm {VueSelect}
* @return {Object}
*/
(map, vm) => map,
```
By default, the prop is a noop returning the same object `map` object it receives. This object
maps keyCodes to handlers: `{ <keyCode>: <callback> }`. Modifying this object can override default
functionality, or add handlers for different keys that the component doesn't normally listen for.
Note that any keyCodes you've added to `selectOnKeyCodes` will be passed to `map-keydown` as well,
so `map-keydown` will always take precedence.
**Default Handlers**
```js
// delete
8: e => this.maybeDeleteValue()
// tab
9: e => this.onTab()
// enter
13: e => {
e.preventDefault();
return this.typeAheadSelect();
}
// esc
27: e => this.onEscape()
// up
38: e => {
e.preventDefault();
return this.typeAheadUp();
}
// down
40: e => {
e.preventDefault();
return this.typeAheadDown();
}
```
### Example: Autocomplete Email Addresses
This is example listens for the `@` key, and autocompletes an email address with `@gmail.com`.
<CustomHandlers />
<<< @/.vuepress/components/CustomHandlers.vue
+17
View File
@@ -0,0 +1,17 @@
### Using Vue Select in v-for Loops
---
There may be times that you are including Vue Select within loops of data, such as a table. This can
pose some challenges when emitting events from the component, as you won't know which Vue Select
instance emitted it. This can make it difficult to wire up with things like Vuex.
Fortunately, you can solve this problem with an anonymous function. The example below doesn't use
Vuex just to keep things succinct, but the same solution would apply. The `@input` is handled
with an inline anonymous function, allowing the selected country to be passed to the `updateCountry`
method with the `country` and the `person` object.
<LoopedSelect />
<<< @/.vuepress/components/LoopedSelect.vue
+18
View File
@@ -0,0 +1,18 @@
::: tip <Badge text="3.8.0+" />
Pagination is supported using slots available with Vue Select 3.8 and above.
:::
Pagination can be a super helpful tool when working with large sets of data. If you have 1,000
options, the component is going to render 1,000 DOM nodes. That's a lot of nodes to insert/remove,
and chances are your user is only interested in a few of them anyways.
To implement pagination with Vue Select, you can take advantage of the `list-footer` slot. It
appears below all other options in the drop down list.
To make pagination work properly with filtering, you'll have to handle it yourself in the parent.
You can use the `filterable` boolean to turn off Vue Select's filtering, and then hook into the
`search` event to use the current search query in the parent component.
<Paginated />
<<< @/.vuepress/components/Paginated.vue
+33
View File
@@ -0,0 +1,33 @@
## Default
With the default CSS, Vue Select uses absolute positioning to render the dropdown menu. The root
`.v-select` container (the components `$el`) is used as the `relative` parent for the dropdown. The
dropdown will be displayed below the `$el` regardless of the available space.
This works for most cases, but you might run into issues placing into a modal or near the bottom of
the viewport. If you need more fine grain control, you can use calculated positioning.
## Calculated <Badge text="v3.7.0+" />
If you want more control over how the dropdown is rendered, or if you're running into z-index issues,
you may use the `appendToBody` boolean prop. When enabled, Vue Select will append the dropdown to
the document, outside of the `.v-select` container, and position it with Javscript.
When `appendToBody` is true, the positioning will be handled by the `calculatePosition` prop. This
function is responsible for setting top/left absolute positioning values for the dropdown. The
default implementation places the dropdown in the same position that it would normally appear.
## Popper.js Integration <Badge text="v3.7.0+" />
[Popper.js](https://popper.js.org/) is an awesome, 3kb utility for calculating positions of just
about any DOM element relative to another.
By using the `appendToBody` and `calculatePosition` props, we're able to integrate directly with
popper to calculate positioning for us.
<PositionedWithPopper />
Check out the [Popper Docs](https://popper.js.org/docs/v2/modifiers/) to see the full `modifiers`
API being used below.
<<< @/.vuepress/components/PositionedWithPopper.vue{25-59}
+44
View File
@@ -0,0 +1,44 @@
## Selectable Prop <Badge text="v3.3.0+" />
The `selectable` prop determines if an option is selectable or not. If `selectable` returns false
for a given option, it will be displayed with a `vs__dropdown-option--disabled` class. The option
will be disabled and unable to be selected.
```js
selectable: {
type: Function,
/**
* @param {Object|String} option
* @return {boolean}
*/
default: option => true,
},
```
### Example
Here `selectable` is used to prevent books by a certain author from being chosen. In this case,
the options passed to the component are objects:
```json
{
title: "Right Ho Jeeves",
author: { firstName: "P.D", lastName: "Woodhouse" },
}
```
This object will be passed to `selectable`, so we can check if the author should be selectable or not.
<UnselectableExample />
<<< @/.vuepress/components/UnselectableExample.vue{6}
## Limiting the Number of Selections
`selectable` can also be used a bit more creatively to limit the number selections that can be made
within the component. In this case, the user can select any author, but may only select a maximum
of three books.
<LimitSelectionQuantity />
<<< @/.vuepress/components/LimitSelectionQuantity.vue{8}
+18 -8
View File
@@ -1,22 +1,32 @@
::: tip 🚧
This section of the guide is a work in progress! Check back soon for an update.
Vue Select currently offers quite a few scoped slots, and you can check out the
Vue Select currently offers quite a few scoped slots, and you can check out the
[API Docs for Slots](../api/slots.md) in the meantime while a good guide is put together.
:::
#### Scoped Slot `option`
### Scoped Slot `option`
vue-select provides the scoped `option` slot in order to create custom dropdown templates.
```html
<v-select :options="options" label="title">
<template v-slot:option="option">
<span :class="option.icon"></span>
{{ option.title }}
</template>
</v-select>
```
<template v-slot:option="option">
<span :class="option.icon"></span>
{{ option.title }}
</template>
</v-select>
```
Using the `option` slot with props `"option"` provides the current option variable to the template.
<CodePen url="NXBwYG" height="500"/>
### Improving the default `no-options` text
The `no-options` slot is displayed in the dropdown when `filteredOptions === 0`. By default, it
displays _Sorry, no matching options_. You can add more contextual information by using the slot
in your own apps.
<BetterNoOptions />
<<< @/.vuepress/components/BetterNoOptions.vue
+3 -3
View File
@@ -69,10 +69,10 @@ has always provided the same parameters and can be used in it's place.
<v-select @search="doSomeAjax" />
```
### `onSearch` with null search string
### `@search` with null search string
The `onSearch` callback is now fired anytime the search string changes. In v2.x, the component
would first check if the search string was empty, and only run the callback if it had at least one
The `@search` event is now fired anytime the search string changes. In v2.x, the component
would first check if the search string was empty, and only emit the event if it had at least one
character. This was a design mistake, as it should be the consumers decision if a search should be
run on an empty string.
+1 -1
View File
@@ -127,7 +127,7 @@ To allow input that's not present within the options, set the `taggable` prop to
If you want added tags to be pushed to the options array, set `push-tags` to true.
```html
<v-select taggable multiple />
<v-select taggable multiple push-tags />
```
<v-select taggable multiple push-tags />
+6
View File
@@ -8,16 +8,22 @@
"build:preview": "cross-env DEPLOY_PREVIEW=true vuepress build"
},
"devDependencies": {
"@octokit/graphql": "^4.3.1",
"@popperjs/core": "^2.1.0",
"@vuepress/plugin-active-header-links": "^1.0.0-alpha.47",
"@vuepress/plugin-google-analytics": "^1.0.0-alpha.47",
"@vuepress/plugin-nprogress": "^1.0.0-alpha.47",
"@vuepress/plugin-pwa": "^1.0.0-alpha.47",
"@vuepress/plugin-register-components": "^1.0.0-alpha.47",
"@vuepress/plugin-search": "^1.0.0-alpha.47",
"axios": "^0.19.2",
"cross-env": "^5.2.0",
"date-fns": "^2.11.0",
"dotenv": "^8.2.0",
"fuse.js": "^3.4.4",
"gh-pages": "^0.11.0",
"node-sass": "^4.12.0",
"octonode": "^0.9.5",
"sass-loader": "^7.1.0",
"vue": "^2.6.10",
"vuepress": "^1.0.0-alpha.47",
+9
View File
@@ -0,0 +1,9 @@
---
sidebarDepth: 0
---
<SponsorMe />
## Sponsors
<Sponsors />
+213 -9
View File
@@ -715,6 +715,59 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@octokit/endpoint@^5.5.0":
version "5.5.3"
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-5.5.3.tgz#0397d1baaca687a4c8454ba424a627699d97c978"
integrity sha512-EzKwkwcxeegYYah5ukEeAI/gYRLv2Y9U5PpIsseGSFDk+G3RbipQGBs8GuYS1TLCtQaqoO66+aQGtITPalxsNQ==
dependencies:
"@octokit/types" "^2.0.0"
is-plain-object "^3.0.0"
universal-user-agent "^5.0.0"
"@octokit/graphql@^4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.3.1.tgz#9ee840e04ed2906c7d6763807632de84cdecf418"
integrity sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==
dependencies:
"@octokit/request" "^5.3.0"
"@octokit/types" "^2.0.0"
universal-user-agent "^4.0.0"
"@octokit/request-error@^1.0.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801"
integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==
dependencies:
"@octokit/types" "^2.0.0"
deprecation "^2.0.0"
once "^1.4.0"
"@octokit/request@^5.3.0":
version "5.3.2"
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.3.2.tgz#1ca8b90a407772a1ee1ab758e7e0aced213b9883"
integrity sha512-7NPJpg19wVQy1cs2xqXjjRq/RmtSomja/VSWnptfYwuBxLdbYh2UjhGi0Wx7B1v5Iw5GKhfFDQL7jM7SSp7K2g==
dependencies:
"@octokit/endpoint" "^5.5.0"
"@octokit/request-error" "^1.0.1"
"@octokit/types" "^2.0.0"
deprecation "^2.0.0"
is-plain-object "^3.0.0"
node-fetch "^2.3.0"
once "^1.4.0"
universal-user-agent "^5.0.0"
"@octokit/types@^2.0.0":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.5.0.tgz#f1bbd147e662ae2c79717d518aac686e58257773"
integrity sha512-KEnLwOfdXzxPNL34fj508bhi9Z9cStyN7qY1kOfVahmqtAfrWw6Oq3P4R+dtsg0lYtZdWBpUrS/Ixmd5YILSww==
dependencies:
"@types/node" ">= 8"
"@popperjs/core@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.1.0.tgz#09a7a352a40508156e1256efdc015593feca28e0"
integrity sha512-ntN5t5spqhQv28cLfmmt1dYabsudzR5A7PU15gr/gzcT/gzqAOnYFQPaLPFraDa7ZCJG2eJ1JsO7pgXbYXGIrw==
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
@@ -739,6 +792,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.4.tgz#64db61e0359eb5a8d99b55e05c729f130a678b04"
integrity sha512-W0+n1Y+gK/8G2P/piTkBBN38Qc5Q1ZSO6B5H3QmPCUewaiXOo2GCAWZ4ElZCcNhjJuBSUSLGFUJnmlCn5+nxOQ==
"@types/node@>= 8":
version "13.9.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.1.tgz#96f606f8cd67fb018847d9b61e93997dabdefc72"
integrity sha512-E6M6N0blf/jiZx8Q3nb0vNaswQeEyn0XlupO+xN6DtJ6r6IT4nXrTry7zhIfYvFCl3/8Cu6WIysmUBKiqV0bqQ==
"@types/q@^1.5.1":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
@@ -1321,6 +1379,11 @@ array-union@^1.0.1, array-union@^1.0.2:
dependencies:
array-uniq "^1.0.1"
array-uniq@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.2.tgz#5fcc373920775723cfd64d65c64bef53bf9eba6d"
integrity sha1-X8w3OSB3VyPP1k1lxkvvU7+eum0=
array-uniq@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
@@ -1430,6 +1493,13 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
axios@^0.19.2:
version "0.19.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
dependencies:
follow-redirects "1.5.10"
babel-extract-comments@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/babel-extract-comments/-/babel-extract-comments-1.0.0.tgz#0a2aedf81417ed391b85e18b4614e693a0351a21"
@@ -1553,6 +1623,11 @@ bluebird@^3.1.1, bluebird@^3.5.5:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
bluebird@^3.5.0:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
@@ -2532,6 +2607,11 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
date-fns@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.11.0.tgz#ec2b44977465b9dcb370021d5e6c019b19f36d06"
integrity sha512-8P1cDi8ebZyDxUyUprBXwidoEtiQAawYPGvpfb+Dg0G6JrQ+VozwOmm91xYC0vAv1+0VmLehEPb+isg4BGUFfA==
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
@@ -2549,6 +2629,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9:
dependencies:
ms "2.0.0"
debug@=3.1.0, debug@~3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@^3.0.0, debug@^3.2.5, debug@^3.2.6:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@@ -2563,13 +2650,6 @@ debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "^2.1.1"
debug@~3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -2672,6 +2752,11 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
deprecation@^2.0.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
des.js@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
@@ -2814,6 +2899,11 @@ dot-prop@^4.1.1:
dependencies:
is-obj "^1.0.0"
dotenv@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
duplexify@^3.4.2, duplexify@^3.6.0:
version "3.7.1"
resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
@@ -3260,6 +3350,13 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
follow-redirects@1.5.10:
version "1.5.10"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
dependencies:
debug "=3.1.0"
follow-redirects@^1.0.0:
version "1.8.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.8.1.tgz#24804f9eaab67160b0e840c085885d606371a35b"
@@ -3601,7 +3698,7 @@ har-schema@^2.0.0:
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
har-validator@~5.1.0:
har-validator@~5.1.0, har-validator@~5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
@@ -4241,6 +4338,13 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-plain-object@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.0.tgz#47bfc5da1b5d50d64110806c199359482e75a928"
integrity sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==
dependencies:
isobject "^4.0.0"
is-regex@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
@@ -4324,6 +4428,11 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
isobject@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -4637,6 +4746,11 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
macos-release@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==
make-dir@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@@ -5048,6 +5162,11 @@ no-case@^2.2.0:
dependencies:
lower-case "^1.1.1"
node-fetch@^2.3.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-forge@0.7.5:
version "0.7.5"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df"
@@ -5342,6 +5461,16 @@ obuf@^1.0.0, obuf@^1.1.2:
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
octonode@^0.9.5:
version "0.9.5"
resolved "https://registry.yarnpkg.com/octonode/-/octonode-0.9.5.tgz#0237ea289a2d6642068f6383a420bf97cfca993e"
integrity sha512-l+aX9jNVkaagh7u/q2QpNKdL8XUagdztl+ebXxBRU6FJ1tpRxAH/ygIuWh0h7eS491BsyH6bb0QZIQEC2+u5oA==
dependencies:
bluebird "^3.5.0"
deep-extend "^0.6.0"
randomstring "^1.1.5"
request "^2.72.0"
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@@ -5414,6 +5543,14 @@ os-locale@^3.0.0:
lcid "^2.0.0"
mem "^4.0.0"
os-name@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801"
integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==
dependencies:
macos-release "^2.2.0"
windows-release "^3.1.0"
os-tmpdir@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@@ -6113,6 +6250,11 @@ psl@^1.1.24:
resolved "https://registry.yarnpkg.com/psl/-/psl-1.3.1.tgz#d5aa3873a35ec450bc7db9012ad5a7246f6fc8bd"
integrity sha512-2KLd5fKOdAfShtY2d/8XDWVRnmp3zp40Qt6ge2zBPFARLXOGUf2fHD5eg+TV/5oxBtQKVhjUaKFsAaE4HnwfSA==
psl@^1.1.28:
version "1.7.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
public-encrypt@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@@ -6160,7 +6302,7 @@ punycode@^1.2.4, punycode@^1.4.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
punycode@^2.1.0:
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
@@ -6241,6 +6383,13 @@ randomfill@^1.0.3:
randombytes "^2.0.5"
safe-buffer "^5.1.0"
randomstring@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/randomstring/-/randomstring-1.1.5.tgz#6df0628f75cbd5932930d9fe3ab4e956a18518c3"
integrity sha1-bfBij3XL1ZMpMNn+OrTpVqGFGMM=
dependencies:
array-uniq "1.0.2"
range-parser@^1.2.1, range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@@ -6445,6 +6594,32 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
request@^2.72.0:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
caseless "~0.12.0"
combined-stream "~1.0.6"
extend "~3.0.2"
forever-agent "~0.6.1"
form-data "~2.3.2"
har-validator "~5.1.3"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.19"
oauth-sign "~0.9.0"
performance-now "^2.1.0"
qs "~6.5.2"
safe-buffer "^5.1.2"
tough-cookie "~2.5.0"
tunnel-agent "^0.6.0"
uuid "^3.3.2"
request@^2.87.0, request@^2.88.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
@@ -7410,6 +7585,14 @@ tough-cookie@~2.4.3:
psl "^1.1.24"
punycode "^1.4.1"
tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
dependencies:
psl "^1.1.28"
punycode "^2.1.1"
trim-newlines@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@@ -7537,6 +7720,20 @@ unique-slug@^2.0.0:
dependencies:
imurmurhash "^0.1.4"
universal-user-agent@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557"
integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==
dependencies:
os-name "^3.1.0"
universal-user-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-5.0.0.tgz#a3182aa758069bf0e79952570ca757de3579c1d9"
integrity sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==
dependencies:
os-name "^3.1.0"
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@@ -7976,6 +8173,13 @@ wide-align@^1.1.0:
dependencies:
string-width "^1.0.2 || 2"
windows-release@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f"
integrity sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==
dependencies:
execa "^1.0.0"
workbox-background-sync@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-4.3.1.tgz#26821b9bf16e9e37fd1d640289edddc08afd1950"
+32 -5
View File
@@ -1,6 +1,6 @@
{
"name": "vue-select",
"version": "3.2.0",
"version": "3.9.1",
"description": "Everything you wish the HTML <select> element could do, wrapped up into a lightweight, extensible Vue component.",
"author": "Jeff Sagal <sagalbot@gmail.com>",
"homepage": "https://vue-select.org",
@@ -11,13 +11,16 @@
"private": false,
"main": "dist/vue-select.js",
"license": "MIT",
"prepare": "npm run build",
"scripts": {
"serve": "webpack-dev-server --config build/webpack.dev.conf.js --hot --progress -d",
"serve:docs": "cd docs && yarn serve",
"build": "cross-env NODE_ENV=production webpack --config build/webpack.prod.conf.js --progress",
"build:docs": "cd docs && yarn build",
"build:preview": "cd docs && yarn build",
"test": "jest"
"test": "jest",
"semantic-release": "semantic-release",
"commit": "git-cz"
},
"repository": {
"type": "git",
@@ -34,15 +37,20 @@
"@babel/plugin-transform-runtime": "^7.4.0",
"@babel/preset-env": "^7.4.2",
"@babel/runtime": "^7.4.2",
"@vue/test-utils": "^1.0.0-beta.29",
"@semantic-release/git": "^9.0.0",
"@semantic-release/github": "^7.0.4",
"@vue/test-utils": "^1.0.0-beta.31",
"autoprefixer": "^9.4.7",
"babel-core": "^7.0.0-bridge.0",
"babel-loader": "^8.0.5",
"bundlewatch": "^0.2.5",
"chokidar": "^2.1.5",
"commitizen": "^4.0.3",
"coveralls": "^3.0.2",
"cross-env": "^5.2.0",
"css-loader": "^2.1.0",
"cssnano": "^4.1.10",
"cz-conventional-changelog": "3.1.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.1.0",
@@ -53,6 +61,7 @@
"postcss-loader": "^3.0.0",
"postcss-scss": "^2.0.0",
"sass-loader": "^7.1.0",
"semantic-release": "^17.0.4",
"terser-webpack-plugin": "^1.2.3",
"url-loader": "^1.1.2",
"vue": "^2.6.10",
@@ -97,8 +106,26 @@
"!**/node_modules/**"
],
"coverageReporters": [
"html",
"text-summary"
"text"
]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"bundlewatch": {
"files": [
{
"path": "./dist/vue-select.js",
"compression": "none",
"maxSize": "21 KB"
},
{
"path": "./dist/vue-select.css",
"compression": "none",
"maxSize": "6 KB"
}
]
}
}
+23
View File
@@ -0,0 +1,23 @@
module.exports = {
release: {
branch: "master"
},
plugins: [
"@semantic-release/npm",
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/github",
{
assets: ["dist/**"]
}
],
[
"@semantic-release/git",
{
assets: ["package.json"],
message: "chore(🚀): ${nextRelease.version}"
}
]
]
};
+254 -149
View File
@@ -4,7 +4,8 @@
<template>
<div :dir="dir" class="v-select" :class="stateClasses">
<div ref="toggle" @mousedown.prevent="toggleDropdown" class="vs__dropdown-toggle">
<slot name="header" v-bind="scope.header" />
<div :id="`vs${uid}__combobox`" ref="toggle" @mousedown.prevent="toggleDropdown" class="vs__dropdown-toggle" role="combobox" :aria-expanded="dropdownOpen.toString()" :aria-owns="`vs${uid}__listbox`" aria-label="Search for option">
<div class="vs__selected-options" ref="selectedOptions">
<slot v-for="option in selectedValue"
@@ -17,7 +18,7 @@
<slot name="selected-option" v-bind="normalizeOptionForSlot(option)">
{{ getOptionLabel(option) }}
</slot>
<button v-if="multiple" :disabled="disabled" @click="deselect(option)" type="button" class="vs__deselect" aria-label="Deselect option">
<button v-if="multiple" :disabled="disabled" @click="deselect(option)" type="button" class="vs__deselect" :title="`Deselect ${getOptionLabel(option)}`" :aria-label="`Deselect ${getOptionLabel(option)}`" ref="deselectButtons">
<component :is="childComponents.Deselect" />
</button>
</span>
@@ -28,14 +29,16 @@
</slot>
</div>
<div class="vs__actions">
<div class="vs__actions" ref="actions">
<button
v-show="showClearButton"
:disabled="disabled"
@click="clearSelection"
type="button"
class="vs__clear"
title="Clear selection"
title="Clear Selected"
aria-label="Clear Selected"
ref="clearButton"
>
<component :is="childComponents.Deselect" />
</button>
@@ -49,15 +52,17 @@
</slot>
</div>
</div>
<transition :name="transition">
<ul ref="dropdownMenu" v-if="dropdownOpen" class="vs__dropdown-menu" role="listbox" @mousedown.prevent="onMousedown" @mouseup="onMouseUp">
<ul ref="dropdownMenu" v-if="dropdownOpen" :id="`vs${uid}__listbox`" class="vs__dropdown-menu" role="listbox" @mousedown.prevent="onMousedown" @mouseup="onMouseUp" v-append-to-body>
<slot name="list-header" v-bind="scope.listHeader" />
<li
role="option"
v-for="(option, index) in filteredOptions"
:key="getOptionKey(option)"
:id="`vs${uid}__option-${index}`"
class="vs__dropdown-option"
:class="{ 'vs__dropdown-option--selected': isOptionSelected(option), 'vs__dropdown-option--highlight': index === typeAheadPointer, 'vs__dropdown-option--disabled': !selectable(option) }"
:aria-selected="index === typeAheadPointer ? true : null"
@mouseover="selectable(option) ? typeAheadPointer = index : null"
@mousedown.prevent.stop="selectable(option) ? select(option) : null"
>
@@ -65,11 +70,14 @@
{{ getOptionLabel(option) }}
</slot>
</li>
<li v-if="!filteredOptions.length" class="vs__no-options" @mousedown.stop="">
<slot name="no-options">Sorry, no matching options.</slot>
<li v-if="filteredOptions.length === 0" class="vs__no-options" @mousedown.stop="">
<slot name="no-options" v-bind="scope.noOptions">Sorry, no matching options.</slot>
</li>
<slot name="list-footer" v-bind="scope.listFooter" />
</ul>
<ul v-else :id="`vs${uid}__listbox`" role="listbox" style="display: none; visibility: hidden;"></ul>
</transition>
<slot name="footer" v-bind="scope.footer" />
</div>
</template>
@@ -78,12 +86,20 @@
import typeAheadPointer from '../mixins/typeAheadPointer'
import ajax from '../mixins/ajax'
import childComponents from './childComponents';
import appendToBody from '../directives/appendToBody';
import sortAndStringify from '../utility/sortAndStringify'
import uniqueId from '../utility/uniqueId';
/**
* @name VueSelect
*/
export default {
components: {...childComponents},
mixins: [pointerScroll, typeAheadPointer, ajax],
directives: {appendToBody},
props: {
/**
* Contains the currently selected value. Very similar to a
@@ -225,10 +241,11 @@
},
/**
* Decides wether an option is selectable or not. Not selectable options
* 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}
*/
@@ -268,12 +285,16 @@
},
/**
* 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.
* Generate a unique identifier for each option. If `option`
* is an object and `option.hasOwnProperty('id')` exists,
* `option.id` is used by default, otherwise the option
* will be serialized to JSON.
*
* The key must be unique for an option.
* If you are supplying a lot of options, you should
* provide your own keys, as JSON.stringify can be
* slow with lots of objects.
*
* The result of this function *must* be unique.
*
* @type {Function}
* @param {Object || String} option
@@ -281,23 +302,21 @@
*/
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'
)
return null
}
default (option) {
if (typeof option !== 'object') {
return option;
}
}
try {
return option.hasOwnProperty('id') ? option.id : sortAndStringify(option);
} catch (e) {
const warning = `[vue-select warn]: Could not stringify this 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';
return console.warn(warning, option, e);
}
},
},
/**
@@ -321,11 +340,12 @@
/**
* Select the current value if selectOnTab is enabled
* @deprecated since 3.3
*/
onTab: {
type: Function,
default: function () {
if (this.selectOnTab) {
if (this.selectOnTab && !this.isComposing) {
this.typeAheadSelect();
}
},
@@ -399,7 +419,7 @@
* @return {Boolean}
*/
filter: {
"type": Function,
type: Function,
default(options, search) {
return options.filter((option) => {
let label = this.getOptionLabel(option)
@@ -417,23 +437,37 @@
*/
createOption: {
type: Function,
default(newOption) {
if (typeof this.optionList[0] === 'object') {
newOption = {[this.label]: newOption}
}
this.$emit('option:created', newOption)
return newOption
}
default (option) {
return (typeof this.optionList[0] === 'object') ? {[this.label]: option} : option;
},
},
/**
* When false, updating the options will not reset the select value
* @type {Boolean}
* 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: {
type: Boolean,
default: false
default: false,
validator: (value) => ['function', 'boolean'].includes(typeof value)
},
/**
* If search text should clear on blur
* @return {Boolean} True when single and clearSearchOnSelect
*/
clearSearchOnBlur: {
type: Function,
default: function ({ clearSearchOnSelect, multiple }) {
return clearSearchOnSelect && !multiple
}
},
/**
@@ -468,12 +502,22 @@
/**
* 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.
@@ -486,13 +530,69 @@
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,
},
/**
* Append the dropdown element to the end of the body
* and size/position it dynamically. Use it if you have
* overflow or z-index issues.
* @type {Boolean}
*/
appendToBody: {
type: Boolean,
default: false
},
/**
* When `appendToBody` is true, this function is responsible for
* positioning the drop down list.
*
* If a function is returned from `calculatePosition`, it will
* be called when the drop down list is removed from the DOM.
* This allows for any garbage collection you may need to do.
*
* @since v3.7.0
* @see http://vue-select.org/guide/positioning.html
*/
calculatePosition: {
type: Function,
/**
* @param dropdownList {HTMLUListElement}
* @param component {Vue} current instance of vue select
* @param width {string} calculated width in pixels of the dropdown menu
* @param top {string} absolute position top value in pixels relative to the document
* @param left {string} absolute position left value in pixels relative to the document
* @return {function|void}
*/
default(dropdownList, component, {width, top, left}) {
dropdownList.style.top = top;
dropdownList.style.left = left;
dropdownList.style.width = width;
}
}
},
data() {
return {
uid: uniqueId(),
search: '',
open: false,
isComposing: false,
pushedTags: [],
_value: [] // Internal value managed by Vue Select if no `value` prop is passed
}
@@ -506,13 +606,17 @@
* is correct.
* @return {[type]} [description]
*/
options(val) {
if (!this.taggable && this.resetOnOptionsChange) {
this.clearSelection()
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)
this.setInternalValueFromOptions(this.value);
}
},
@@ -534,6 +638,10 @@
*/
multiple() {
this.clearSelection()
},
open(isOpen) {
this.$emit(isOpen ? 'open' : 'close');
}
},
@@ -544,7 +652,7 @@
this.setInternalValueFromOptions(this.value)
}
this.$on('option:created', this.maybePushTag)
this.$on('option:created', this.pushTag)
},
methods: {
@@ -570,14 +678,13 @@
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)
},
@@ -646,31 +753,23 @@
* @param {Event} e
* @return {void}
*/
toggleDropdown (e) {
const target = e.target;
const toggleTargets = [
this.$el,
this.searchEl,
this.$refs.toggle,
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 (typeof this.$refs.openIndicator !== 'undefined') {
toggleTargets.push(
this.$refs.openIndicator.$el,
// the line below is a bit gross, but required to support IE11 without adding polyfills
...Array.prototype.slice.call(this.$refs.openIndicator.$el.childNodes),
);
if (ignoredButtons.some(ref => ref.contains(target) || ref === target)) {
return;
}
if (toggleTargets.indexOf(target) > -1 || target.classList.contains('vs__selected')) {
if (this.open) {
this.searchEl.blur(); // dropdown will close on blur
} else {
if (!this.disabled) {
this.open = true;
this.searchEl.focus();
}
}
if (this.open) {
this.searchEl.blur();
} else if (!this.disabled) {
this.open = true;
this.searchEl.focus();
}
},
@@ -680,42 +779,22 @@
* @return {Boolean} True when selected | False otherwise
*/
isOptionSelected(option) {
return this.selectedValue.some(value => {
return this.optionComparator(value, option)
})
return this.selectedValue.some(value => this.optionComparator(value, option))
},
/**
* Determine if two option objects are matching.
*
* @param value {Object}
* @param option {Object}
* @param a {Object}
* @param b {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;
optionComparator(a, b) {
return this.getOptionKey(a) === this.getOptionKey(b);
},
/**
* Finds an option from this.options
* Finds an option from the options
* where a reduced value matches
* the passed in value.
*
@@ -723,7 +802,24 @@
* @returns {*}
*/
findOptionFromReducedValue (value) {
return this.options.find(option => JSON.stringify(this.reduce(option)) === JSON.stringify(value)) || value;
const predicate = option => JSON.stringify(this.reduce(option)) === JSON.stringify(value);
const matches = [
...this.options,
...this.pushedTags,
].filter(predicate);
if (matches.length === 1) {
return matches[0];
}
/**
* This second loop is needed to cover an edge case where `taggable` + `reduce`
* were used in conjunction with a `create-option` that doesn't create a
* 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;
},
/**
@@ -742,7 +838,7 @@
* @return {this.value}
*/
maybeDeleteValue() {
if (!this.searchEl.value.length && this.selectedValue && this.clearable) {
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)]
@@ -759,14 +855,7 @@
* @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
})
return this.optionList.some(_option => this.optionComparator(_option, option))
},
/**
@@ -786,10 +875,8 @@
* @param {Object || String} option
* @return {void}
*/
maybePushTag(option) {
if (this.pushTags) {
this.pushedTags.push(option)
}
pushTag (option) {
this.pushedTags.push(option);
},
/**
@@ -814,7 +901,8 @@
if (this.mousedown && !this.searching) {
this.mousedown = false
} else {
if (this.clearSearchOnBlur) {
const { clearSearchOnSelect, multiple } = this;
if (this.clearSearchOnBlur({ clearSearchOnSelect, multiple })) {
this.search = ''
}
this.closeSearchOptions()
@@ -859,39 +947,46 @@
},
/**
* Search 'input' KeyBoardEvent handler.
* Search <input> KeyBoardEvent handler.
* @param e {KeyboardEvent}
* @return {Function}
*/
onSearchKeyDown (e) {
switch (e.keyCode) {
case 8:
// delete
return this.maybeDeleteValue();
case 9:
// tab
return this.onTab();
case 13:
// enter.prevent
e.preventDefault();
return this.typeAheadSelect();
case 27:
// esc
return this.onEscape();
case 38:
// up.prevent
const preventAndSelect = e => {
e.preventDefault();
return !this.isComposing && this.typeAheadSelect();
};
const defaults = {
// backspace
8: e => this.maybeDeleteValue(),
// tab
9: e => this.onTab(),
// esc
27: e => this.onEscape(),
// up.prevent
38: e => {
e.preventDefault();
return this.typeAheadUp();
case 40:
// down.prevent
},
// 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.
@@ -928,7 +1023,7 @@
* @return {Array}
*/
optionList () {
return this.options.concat(this.pushedTags);
return this.options.concat(this.pushTags ? this.pushedTags : []);
},
/**
@@ -946,6 +1041,12 @@
* @returns {Object}
*/
scope () {
const listSlot = {
search: this.search,
loading: this.loading,
searching: this.searching,
filteredOptions: this.filteredOptions
};
return {
search: {
attributes: {
@@ -954,24 +1055,32 @@
'tabindex': this.tabindex,
'readonly': !this.searchable,
'id': this.inputId,
'aria-expanded': this.dropdownOpen,
'aria-label': 'Search for option',
'aria-autocomplete': 'list',
'aria-labelledby': `vs${this.uid}__combobox`,
'aria-controls': `vs${this.uid}__listbox`,
'aria-activedescendant': this.typeAheadPointer > -1 ? `vs${this.uid}__option-${this.typeAheadPointer}` : '',
'ref': 'search',
'role': 'combobox',
'type': 'search',
'autocomplete': 'off',
'autocomplete': this.autocomplete,
'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
'input': (e) => this.search = e.target.value,
},
},
spinner: {
loading: this.mutableLoading
},
noOptions: {
search: this.search,
loading: this.loading,
searching: this.searching,
},
openIndicator: {
attributes: {
'ref': 'openIndicator',
@@ -979,6 +1088,10 @@
'class': 'vs__open-indicator',
},
},
listHeader: listSlot,
listFooter: listSlot,
header: { ...listSlot, deselect: this.deselect },
footer: { ...listSlot, deselect: this.deselect }
};
},
@@ -1012,14 +1125,6 @@
}
},
/**
* 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
@@ -1065,7 +1170,7 @@
}
let options = this.search.length ? this.filter(optionList, this.search, this) : optionList;
if (this.taggable && this.search.length && !this.optionExists(this.search)) {
if (this.taggable && this.search.length && !this.optionExists(this.createOption(this.search))) {
options.unshift(this.search)
}
return options
@@ -1085,7 +1190,7 @@
*/
showClearButton() {
return !this.multiple && this.clearable && !this.open && !this.isValueEmpty
}
},
},
}
+26
View File
@@ -0,0 +1,26 @@
export default {
inserted (el, bindings, {context}) {
if (context.appendToBody) {
const {height, top, left} = context.$refs.toggle.getBoundingClientRect();
el.unbindPosition = context.calculatePosition(el, context, {
width: context.$refs.toggle.clientWidth + 'px',
top: (window.scrollY + top + height) + 'px',
left: (window.scrollX + left) + 'px',
});
document.body.appendChild(el);
}
},
unbind (el, bindings, {context}) {
if (context.appendToBody) {
if (el.unbindPosition && typeof el.unbindPosition === 'function') {
el.unbindPosition();
}
if (el.parentNode) {
el.parentNode.removeChild(el);
}
}
},
}
+1 -1
View File
@@ -30,7 +30,7 @@ export default {
*/
pixelsToPointerTop() {
let pixelsToPointerTop = 0;
if (this.$refs.dropdownMenu) {
if (this.$refs.dropdownMenu && this.dropdownOpen) {
for (let i = 0; i < this.typeAheadPointer; i++) {
pixelsToPointerTop += this.$refs.dropdownMenu.children[i]
.offsetHeight;
+7 -7
View File
@@ -56,15 +56,15 @@ export default {
* Optionally clear the search input on selection.
* @return {void}
*/
typeAheadSelect() {
if( this.filteredOptions[ this.typeAheadPointer ] ) {
this.select( this.filteredOptions[ this.typeAheadPointer ] );
} else if (this.taggable && this.search.length){
this.select(this.search)
typeAheadSelect () {
if (!this.taggable && this.filteredOptions[this.typeAheadPointer]) {
this.select(this.filteredOptions[this.typeAheadPointer]);
} else if (this.taggable && this.search.length) {
this.select(this.createOption(this.search));
}
if( this.clearSearchOnSelect ) {
this.search = "";
if (this.clearSearchOnSelect) {
this.search = '';
}
},
}
+1
View File
@@ -23,6 +23,7 @@ $transition-duration: .15s;
/* Dropdown Default Transition */
.vs__fade-enter-active,
.vs__fade-leave-active {
pointer-events: none;
transition: opacity $transition-duration $transition-timing-function;
}
.vs__fade-enter,
+9 -1
View File
@@ -3,8 +3,16 @@
$line-height: $vs-component-line-height;
$font-size: 1em;
/**
* Super weird bug... If this declaration is grouped
* below, the cancel button will still appear in chrome.
* If it's up here on it's own, it'll hide it.
*/
.vs__search::-webkit-search-cancel-button {
display: none;
}
.vs__search::-webkit-search-decoration,
.vs__search::-webkit-search-cancel-button,
.vs__search::-webkit-search-results-button,
.vs__search::-webkit-search-results-decoration,
.vs__search::-ms-clear {
+15
View File
@@ -0,0 +1,15 @@
/**
* @param sortable {object}
* @return {string}
*/
function sortAndStringify(sortable) {
const ordered = {};
Object.keys(sortable).sort().forEach(key => {
ordered[key] = sortable[key];
});
return JSON.stringify(ordered);
}
export default sortAndStringify;
+12
View File
@@ -0,0 +1,12 @@
let idCount = 0;
/**
* Dead simple unique ID implementation.
* Thanks lodash!
* @return {number}
*/
function uniqueId() {
return ++idCount;
}
export default uniqueId;
+10 -4
View File
@@ -13,10 +13,11 @@ describe("Asynchronous Loading", () => {
expect(Select.vm.mutableLoading).toEqual(true);
});
it("should trigger the search event when the search text changes", () => {
it("should trigger the search event when the search text changes", async () => {
const Select = selectWithProps();
Select.vm.search = "foo";
await Select.vm.$nextTick();
const events = Select.emitted("search");
@@ -24,11 +25,13 @@ describe("Asynchronous Loading", () => {
expect(events.length).toEqual(1);
});
it("should trigger the search event if the search text is empty", () => {
it("should trigger the search event if the search text is empty", async () => {
const Select = selectWithProps();
Select.vm.search = "foo";
await Select.vm.$nextTick();
Select.vm.search = "";
await Select.vm.$nextTick();
const events = Select.emitted("search");
@@ -36,7 +39,7 @@ describe("Asynchronous Loading", () => {
expect(events.length).toEqual(2);
});
it("can set loading to false from the @search event callback", () => {
it("can set loading to false from the @search event callback", async () => {
const Select = shallowMount(vSelect, {
listeners: {
search: (search, loading) => {
@@ -47,13 +50,16 @@ describe("Asynchronous Loading", () => {
Select.vm.mutableLoading = true;
Select.vm.search = 'foo';
await Select.vm.$nextTick();
expect(Select.vm.mutableLoading).toEqual(false);
});
it("will sync mutable loading with the loading prop", () => {
it('will sync mutable loading with the loading prop', async () => {
const Select = selectWithProps({ loading: false });
Select.setProps({ loading: true });
await Select.vm.$nextTick();
expect(Select.vm.mutableLoading).toEqual(true);
});
});
+2 -1
View File
@@ -1,9 +1,10 @@
import { selectWithProps } from "../helpers";
describe("Removing values", () => {
it("can remove the given tag when its close icon is clicked", () => {
it("can remove the given tag when its close icon is clicked", async () => {
const Select = selectWithProps({ multiple: true });
Select.vm.$data._value = 'one';
await Select.vm.$nextTick();
Select.find(".vs__deselect").trigger("click");
expect(Select.emitted().input).toEqual([[[]]]);
+5 -2
View File
@@ -129,14 +129,17 @@ describe("Toggling Dropdown", () => {
expect(Select.vm.stateClasses['vs--open']).toEqual(true);
});
it("should not display the dropdown if noDrop is true", () => {
it("should not display the dropdown if noDrop is true", async () => {
const Select = selectWithProps({
noDrop: true,
});
Select.vm.toggleDropdown({ target: Select.vm.$refs.search });
expect(Select.vm.open).toEqual(true);
await Select.vm.$nextTick();
expect(Select.contains('.vs__dropdown-menu')).toBeFalsy();
expect(Select.contains('.vs__dropdown-option')).toBeFalsy();
expect(Select.contains('.vs__no-options')).toBeFalsy();
expect(Select.vm.stateClasses['vs--open']).toBeFalsy();
});
+84
View File
@@ -0,0 +1,84 @@
import { mountDefault } from '../helpers';
describe('Custom Keydown Handlers', () => {
it('can use the map-keydown prop to trigger custom behaviour', () => {
const onKeyDown = jest.fn();
const Select = mountDefault({
mapKeydown: (defaults, vm) => ({ ...defaults, 32: onKeyDown }),
});
Select.find({ ref: 'search' }).trigger('keydown.space');
expect(onKeyDown.mock.calls.length).toBe(1);
});
it('selectOnKeyCodes should trigger a selection for custom keycodes', () => {
const Select = mountDefault({
selectOnKeyCodes: [32],
});
const spy = jest.spyOn(Select.vm, 'typeAheadSelect');
Select.find({ ref: 'search' }).trigger('keydown.space');
expect(spy).toHaveBeenCalledTimes(1);
});
it('even works when combining selectOnKeyCodes with map-keydown', () => {
const onKeyDown = jest.fn();
const Select = mountDefault({
mapKeydown: (defaults, vm) => ({ ...defaults, 32: onKeyDown }),
selectOnKeyCodes: [9],
});
const spy = jest.spyOn(Select.vm, 'typeAheadSelect');
Select.find({ ref: 'search' }).trigger('keydown.space');
expect(onKeyDown.mock.calls.length).toBe(1);
Select.find({ ref: 'search' }).trigger('keydown.tab');
expect(spy).toHaveBeenCalledTimes(1);
});
describe('CompositionEvent support', () => {
it('will not select a value with enter if the user is composing', () => {
const Select = mountDefault();
const spy = jest.spyOn(Select.vm, 'typeAheadSelect');
Select.find({ ref: 'search' }).trigger('compositionstart');
Select.find({ ref: 'search' }).trigger('keydown.enter');
expect(spy).toHaveBeenCalledTimes(0);
Select.find({ ref: 'search' }).trigger('compositionend');
Select.find({ ref: 'search' }).trigger('keydown.enter');
expect(spy).toHaveBeenCalledTimes(1);
});
it('will not select a value with tab if the user is composing', () => {
const Select = mountDefault({ selectOnTab: true });
const spy = jest.spyOn(Select.vm, 'typeAheadSelect');
Select.find({ ref: 'search' }).trigger('compositionstart');
Select.find({ ref: 'search' }).trigger('keydown.tab');
expect(spy).toHaveBeenCalledTimes(0);
Select.find({ ref: 'search' }).trigger('compositionend');
Select.find({ ref: 'search' }).trigger('keydown.tab');
expect(spy).toHaveBeenCalledTimes(1);
});
it('will not emit input event if value has not changed with backspace', () => {
const Select = mountDefault();
Select.vm.$data._value = 'one';
Select.find({ ref: 'search' }).trigger('keydown.backspace');
expect(Select.emitted().input.length).toBe(1);
Select.find({ ref: 'search' }).trigger('keydown.backspace');
Select.find({ ref: 'search' }).trigger('keydown.backspace');
expect(Select.emitted().input.length).toBe(1);
});
});
});
+40 -1
View File
@@ -12,13 +12,15 @@ describe("Labels", () => {
expect(Select.find(".vs__selected").text()).toBe("Foo");
});
it("will console.warn when options contain objects without a valid label key", () => {
it("will console.warn when options contain objects without a valid label key", async () => {
const spy = jest.spyOn(console, "warn").mockImplementation(() => {});
const Select = selectWithProps({
options: [{}]
});
Select.vm.open = true;
await Select.vm.$nextTick();
expect(spy).toHaveBeenCalledWith(
'[vue-select warn]: Label key "option.label" does not exist in options object {}.' +
"\nhttps://vue-select.org/api/props.html#getoptionlabel"
@@ -39,4 +41,41 @@ describe("Labels", () => {
Select.vm.$data._value = "one";
expect(Select.vm.searchPlaceholder).not.toBeDefined();
});
describe('getOptionLabel', () => {
it('will return undefined if the option lacks the label key', () => {
const getOptionLabel = VueSelect.props.getOptionLabel.default.bind({ label: 'label' });
expect(getOptionLabel({name: 'vue'})).toEqual(undefined);
});
it('will return a string value for a valid key', () => {
const getOptionLabel = VueSelect.props.getOptionLabel.default.bind({ label: 'label' });
expect(getOptionLabel({label: 'vue'})).toEqual('vue');
});
/**
* this test fails because of a bug where Vue executes the default contents
* of a slot, even if it is implemented by the consumer.
* @see https://github.com/vuejs/vue/issues/10224
* @see https://github.com/vuejs/vue/pull/10229
*/
xit('will not call getOptionLabel if both scoped option slots are used and a filter is provided', () => {
const spy = spyOn(VueSelect.props.getOptionLabel, 'default');
const Select = shallowMount(VueSelect, {
propsData: {
options: [{name: 'one'}],
filter: () => {},
},
scopedSlots: {
'option': '<span class="option">{{ props.name }}</span>',
'selected-option': '<span class="selected">{{ props.name }}</span>',
},
});
Select.vm.select({name: 'one'});
expect(spy).toHaveBeenCalledTimes(0);
expect(Select.find('.selected').exists()).toBeTruthy();
});
});
});
+31
View File
@@ -0,0 +1,31 @@
import Select from '../../src/components/Select';
describe('Comparing Options', () => {
const comparator = Select.methods.optionComparator.bind({
getOptionKey: Select.props.getOptionKey.default,
});
it('can compare numbers', () => {
expect(comparator(1, 2)).toBeFalsy();
expect(comparator(1, 1)).toBeTruthy();
});
it('can compare strings', () => {
expect(comparator('one', 'one')).toBeTruthy();
expect(comparator('one', 'two')).toBeFalsy();
});
it('can compare objects', () => {
// compare ID keys
expect(comparator({label: 'halo', id: 1}, {label: 'halo', id: 2}))
.toBeFalsy();
// compare objects
expect(comparator({label: 'halo', value: 1}, {label: 'halo', value: 1}))
.toBeTruthy();
// compare objects with different orders
expect(comparator({value: 1, label: 'halo'}, {label: 'halo', value: 1}))
.toBeTruthy();
});
});
+2 -2
View File
@@ -5,11 +5,11 @@ describe('Serializing Option Keys', () => {
const getOptionKey = Select.props.getOptionKey.default;
it('can serialize strings to a key', () => {
expect(getOptionKey('vue')).toBe('"vue"');
expect(getOptionKey('vue')).toBe('vue');
});
it('can serialize integers to a key', () => {
expect(getOptionKey(1)).toBe('1');
expect(getOptionKey(1)).toBe(1);
});
it('can serialize objects to a key', () => {
+109 -3
View File
@@ -1,5 +1,6 @@
import { shallowMount } from "@vue/test-utils";
import { mount, shallowMount } from '@vue/test-utils';
import VueSelect from "../../src/components/Select";
import { mountDefault } from '../helpers';
describe("Reset on options change", () => {
it("should not reset the selected value by default when the options property changes", () => {
@@ -13,7 +14,81 @@ describe("Reset on options change", () => {
expect(Select.vm.selectedValue).toEqual(["one"]);
});
it("should reset the selected value when the options property changes", () => {
describe('resetOnOptionsChange as a function', () => {
it('will yell at you if resetOnOptionsChange is not a function or boolean', () => {
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
mountDefault({resetOnOptionsChange: 1});
expect(spy.mock.calls[0][0]).toContain('Invalid prop: custom validator check failed for prop "resetOnOptionsChange"')
mountDefault({resetOnOptionsChange: 'one'});
expect(spy.mock.calls[1][0]).toContain('Invalid prop: custom validator check failed for prop "resetOnOptionsChange"')
mountDefault({resetOnOptionsChange: []});
expect(spy.mock.calls[2][0]).toContain('Invalid prop: custom validator check failed for prop "resetOnOptionsChange"')
mountDefault({resetOnOptionsChange: {}});
expect(spy.mock.calls[3][0]).toContain('Invalid prop: custom validator check failed for prop "resetOnOptionsChange"')
});
it('should receive the new options, old options, and current value', async () => {
let resetOnOptionsChange = jest.fn(option => option);
const Select = mountDefault(
{resetOnOptionsChange, options: ['bear'], value: 'selected'},
);
Select.setProps({options: ['lake', 'kite']});
await Select.vm.$nextTick();
expect(resetOnOptionsChange).toHaveBeenCalledTimes(1);
expect(resetOnOptionsChange)
.toHaveBeenCalledWith(['lake', 'kite'], ['bear'], ['selected']);
});
it('should allow resetOnOptionsChange to be a function that returns true', async () => {
let resetOnOptionsChange = () => true;
const Select = shallowMount(VueSelect, {
propsData: {resetOnOptionsChange, options: ['one'], value: 'one'},
});
const spy = jest.spyOn(Select.vm, 'clearSelection');
Select.setProps({options: ['one', 'two']});
await Select.vm.$nextTick();
expect(spy).toHaveBeenCalledTimes(1);
});
it('should allow resetOnOptionsChange to be a function that returns false', () => {
let resetOnOptionsChange = () => false;
const Select = shallowMount(VueSelect, {
propsData: {resetOnOptionsChange, options: ['one'], value: 'one'},
});
const spy = jest.spyOn(Select.vm, 'clearSelection');
Select.setProps({options: ['one', 'two']});
expect(spy).not.toHaveBeenCalled();
});
it('should reset the options if the selectedValue does not exist in the new options', async () => {
let resetOnOptionsChange = (options, old, val) => val.some(val => options.includes(val));
const Select = shallowMount(VueSelect, {
propsData: {resetOnOptionsChange, options: ['one'], value: 'one'},
});
const spy = jest.spyOn(Select.vm, 'clearSelection');
Select.setProps({options: ['one', 'two']});
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual(['one']);
Select.setProps({options: ['two']});
await Select.vm.$nextTick();
expect(spy).toHaveBeenCalledTimes(1);
});
});
it("should reset the selected value when the options property changes", async () => {
const Select = shallowMount(VueSelect, {
propsData: { resetOnOptionsChange: true, options: ["one"] }
});
@@ -21,15 +96,46 @@ describe("Reset on options change", () => {
Select.vm.$data._value = 'one';
Select.setProps({options: ["four", "five", "six"]});
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual([]);
});
it("should return correct selected value when the options property changes and a new option matches", () => {
it("should return correct selected value when the options property changes and a new option matches", async () => {
const Select = shallowMount(VueSelect, {
propsData: { value: "one", options: [], reduce(option) { return option.value } }
});
Select.setProps({options: [{ label: "oneLabel", value: "one" }]});
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual([{ label: "oneLabel", value: "one" }]);
});
it('clearSearchOnBlur returns false when multiple is true', () => {
const Select = mountDefault({});
let clearSearchOnBlur = jest.spyOn(Select.vm, 'clearSearchOnBlur');
Select.find({ref: 'search'}).trigger('click');
Select.setData({search: 'one'});
Select.find({ref: 'search'}).trigger('blur');
expect(clearSearchOnBlur).toHaveBeenCalledTimes(1);
expect(clearSearchOnBlur).toHaveBeenCalledWith({
clearSearchOnSelect: true,
multiple: false,
});
expect(Select.vm.search).toBe('');
});
it('clearSearchOnBlur accepts a function', () => {
let clearSearchOnBlur = jest.fn(() => false);
const Select = mountDefault({clearSearchOnBlur});
Select.find({ref: 'search'}).trigger('click');
Select.setData({search: 'one'});
Select.find({ref: 'search'}).trigger('blur');
expect(clearSearchOnBlur).toHaveBeenCalledTimes(1);
expect(Select.vm.search).toBe('one');
});
});
+31 -1
View File
@@ -211,7 +211,7 @@ describe("When reduce prop is defined", () => {
});
it("reacts correctly when value property changes", () => {
it("reacts correctly when value property changes", async () => {
const optionToChangeTo = { id: 1, label: "Foo" };
const Select = shallowMount(VueSelect, {
propsData: {
@@ -222,7 +222,37 @@ describe("When reduce prop is defined", () => {
});
Select.setProps({ value: optionToChangeTo.id });
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual([optionToChangeTo]);
});
describe('Reducing Tags', () => {
it('tracks values that have been created by the user', async () => {
const Parent = mount({
data: () => ({selected: null, options: []}),
template: `
<v-select
v-model="selected"
:options="options"
taggable
:reduce="name => name.value"
:create-option="label => ({ label, value: -1 })"
/>
`,
components: {'v-select': VueSelect},
});
const Select = Parent.vm.$children[0];
// When
Select.search = 'hello';
Select.typeAheadSelect();
await Select.$nextTick();
// Then
expect(Select.selectedValue).toEqual([{label: 'hello', value: -1}]);
expect(Select.$refs.selectedOptions.textContent.trim()).toEqual('hello');
expect(Parent.vm.selected).toEqual(-1);
});
});
});
+14 -8
View File
@@ -1,31 +1,37 @@
import { selectWithProps } from "../helpers";
describe("Selectable prop", () => {
it("should select selectable option if clicked", () => {
it("should select selectable option if clicked", async () => {
const Select = selectWithProps({
options: ["one", "two", "three"],
selectable: (option) => option == "one"
selectable: (option) => option === "one"
});
Select.vm.$data.open = true;
await Select.vm.$nextTick();
Select.find(".vs__dropdown-menu li:first-child").trigger("mousedown");
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual(["one"]);
})
it("should not select not selectable option if clicked", () => {
it("should not select not selectable option if clicked", async () => {
const Select = selectWithProps({
options: ["one", "two", "three"],
selectable: (option) => option == "one"
selectable: (option) => option === "one"
});
Select.vm.$data.open = true;
await Select.vm.$nextTick();
Select.find(".vs__dropdown-menu li:last-child").trigger("mousedown");
await Select.vm.$nextTick();
expect(Select.vm.selectedValue).toEqual([]);
});
it("should skip non-selectable option on down arrow keyUp", () => {
it("should skip non-selectable option on down arrow keyDown", () => {
const Select = selectWithProps({
options: ["one", "two", "three"],
selectable: (option) => option !== "two"
@@ -33,12 +39,12 @@ describe("Selectable prop", () => {
Select.vm.typeAheadPointer = 1;
Select.find({ ref: "search" }).trigger("keyup.down");
Select.find({ ref: "search" }).trigger("keydown.down");
expect(Select.vm.typeAheadPointer).toEqual(2);
})
it("should skip non-selectable option on up arrow keyUp", () => {
it("should skip non-selectable option on up arrow keyDown", () => {
const Select = selectWithProps({
options: ["one", "two", "three"],
selectable: (option) => option !== "two"
@@ -46,7 +52,7 @@ describe("Selectable prop", () => {
Select.vm.typeAheadPointer = 2;
Select.find({ ref: "search" }).trigger("keyup.up");
Select.find({ ref: "search" }).trigger("keydown.up");
expect(Select.vm.typeAheadPointer).toEqual(0);
})
+11 -1
View File
@@ -193,7 +193,17 @@ describe("VS - Selecting Values", () => {
value: [{ label: "foo", value: "bar" }]
}
});
expect(Select.vm.isOptionSelected("foo")).toEqual(true);
expect(Select.vm.isOptionSelected({ label: "foo", value: "bar" })).toEqual(true);
});
it('can select two options with the same label', () => {
const options = [{label: 'one', id: 1}, {label: 'one', id: 2}];
const Select = mountDefault({options, multiple: true});
Select.vm.select({label: 'one', id: 1});
Select.vm.select({label: 'one', id: 2});
expect(Select.vm.selectedValue).toEqual(options);
});
describe("input Event", () => {
+100 -19
View File
@@ -10,32 +10,113 @@ describe('Scoped Slots', () => {
},
});
expect(Select.find({ ref: 'selectedOptions' }).text()).toEqual('one')
expect(Select.find({ref: 'selectedOptions'}).text()).toEqual('one');
});
it('receives an option object to the selected-option slot', () => {
const Select = mountDefault(
{value: 'one'},
{
scopedSlots: {
'selected-option': `<span slot="selected-option" slot-scope="option">{{ option.label }}</span>`,
},
});
describe('Slot: selected-option', () => {
it('receives an option object to the selected-option slot', () => {
const Select = mountDefault(
{value: 'one'},
{
scopedSlots: {
'selected-option': `<span slot="selected-option" slot-scope="option">{{ option.label }}</span>`,
},
});
expect(Select.find('.vs__selected').text()).toEqual('one')
expect(Select.find('.vs__selected').text()).toEqual('one');
});
it('opens the dropdown when clicking an option in selected-option slot',
() => {
const Select = mountDefault(
{value: 'one'},
{
scopedSlots: {
'selected-option': `<span class="my-option" slot-scope="option">{{ option.label }}</span>`,
},
});
Select.find('.my-option').trigger('mousedown');
expect(Select.vm.open).toEqual(true);
});
});
it('receives an option object to the option slot in the dropdown menu', () => {
const Select = mountDefault(
{value: 'one'},
{
scopedSlots: {
'option': `<span slot="option" slot-scope="option">{{ option.label }}</span>`,
},
});
it('receives an option object to the option slot in the dropdown menu',
async () => {
const Select = mountDefault(
{value: 'one'},
{
scopedSlots: {
'option': `<span slot="option" slot-scope="option">{{ option.label }}</span>`,
},
});
Select.vm.open = true;
await Select.vm.$nextTick();
expect(Select.find({ref: 'dropdownMenu'}).text()).toEqual('onetwothree');
});
it('noOptions slot receives the current search text', async () => {
const noOptions = jest.fn();
const Select = mountDefault({}, {
scopedSlots: {'no-options': noOptions},
});
Select.vm.search = 'something not there';
Select.vm.open = true;
await Select.vm.$nextTick();
expect(Select.find({ref: 'dropdownMenu'}).text()).toEqual('onetwothree')
expect(noOptions).toHaveBeenCalledWith({
loading: false,
search: 'something not there',
searching: true,
})
});
test('header slot props', async () => {
const header = jest.fn();
const Select = mountDefault({}, {
scopedSlots: {header: header},
});
await Select.vm.$nextTick();
expect(Object.keys(header.mock.calls[0][0])).toEqual([
'search', 'loading', 'searching', 'filteredOptions', 'deselect',
]);
});
test('footer slot props', async () => {
const footer = jest.fn();
const Select = mountDefault({}, {
scopedSlots: {footer: footer},
});
await Select.vm.$nextTick();
expect(Object.keys(footer.mock.calls[0][0])).toEqual([
'search', 'loading', 'searching', 'filteredOptions', 'deselect',
]);
});
test('list-header slot props', async () => {
const header = jest.fn();
const Select = mountDefault({}, {
scopedSlots: {'list-header': header},
});
Select.vm.open = true;
await Select.vm.$nextTick();
expect(Object.keys(header.mock.calls[0][0])).toEqual([
'search', 'loading', 'searching', 'filteredOptions',
]);
});
test('list-footer slot props', async () => {
const footer = jest.fn();
const Select = mountDefault({}, {
scopedSlots: {'list-footer': footer},
});
Select.vm.open = true;
await Select.vm.$nextTick();
expect(Object.keys(footer.mock.calls[0][0])).toEqual([
'search', 'loading', 'searching', 'filteredOptions',
]);
});
});
+28 -6
View File
@@ -1,6 +1,8 @@
import { searchSubmit, selectWithProps } from "../helpers";
import Select from '../../src/components/Select';
describe("When Tagging Is Enabled", () => {
it("can determine if a given option string already exists", () => {
const Select = selectWithProps({ taggable: true, options: ["one", "two"] });
expect(Select.vm.optionExists("one")).toEqual(true);
@@ -13,8 +15,8 @@ describe("When Tagging Is Enabled", () => {
options: [{ label: "one" }, { label: "two" }]
});
expect(Select.vm.optionExists("one")).toEqual(true);
expect(Select.vm.optionExists("three")).toEqual(false);
expect(Select.vm.optionExists({label: "one"})).toEqual(true);
expect(Select.vm.optionExists({label: "three"})).toEqual(false);
});
it("can determine if a given option object already exists when using custom labels", () => {
@@ -24,8 +26,10 @@ describe("When Tagging Is Enabled", () => {
label: "foo"
});
expect(Select.vm.optionExists("one")).toEqual(true);
expect(Select.vm.optionExists("three")).toEqual(false);
const createOption = (text) => Select.vm.createOption(text);
expect(Select.vm.optionExists(createOption("one"))).toEqual(true);
expect(Select.vm.optionExists(createOption("three"))).toEqual(false);
});
it("can add the current search text as the first item in the options list", () => {
@@ -80,6 +84,20 @@ describe("When Tagging Is Enabled", () => {
expect(Select.vm.optionList).toEqual(["one", "two", "three"]);
});
it("should pushTags even if the consumer has defined a createOption callback", () => {
const Select = selectWithProps({
pushTags: true,
taggable: true,
createOption: option => option,
options: ["one", "two"]
});
searchSubmit(Select, "three");
expect(Select.vm.pushedTags).toEqual(["three"]);
expect(Select.vm.optionList).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 Select = selectWithProps({
filterable: false,
@@ -139,7 +157,7 @@ describe("When Tagging Is Enabled", () => {
expect(Select.vm.selectedValue).toEqual([two]);
});
it("should select an existing option if the search string matches an objects label from options", () => {
it("should select an existing option if the search string matches an objects label from options", async () => {
let two = { label: "two" };
const Select = selectWithProps({
taggable: true,
@@ -147,12 +165,13 @@ describe("When Tagging Is Enabled", () => {
});
Select.vm.search = "two";
await Select.vm.$nextTick();
searchSubmit(Select);
expect(Select.vm.selectedValue).toEqual([two]);
});
it("should select an existing option if the search string matches an objects label from options when filter-options is false", () => {
it("should select an existing option if the search string matches an objects label from options when filter-options is false", async () => {
let two = { label: "two" };
const Select = selectWithProps({
taggable: true,
@@ -161,6 +180,7 @@ describe("When Tagging Is Enabled", () => {
});
Select.vm.search = "two";
await Select.vm.$nextTick();
searchSubmit(Select);
expect(Select.vm.selectedValue).toEqual([two]);
@@ -212,9 +232,11 @@ describe("When Tagging Is Enabled", () => {
multiple: true,
options: [{ label: "two" }]
});
const spy = jest.spyOn(Select.vm, 'select');
searchSubmit(Select, "one");
expect(Select.vm.selectedValue).toEqual([{ label: "one" }]);
expect(spy).lastCalledWith({label: 'one'});
expect(Select.vm.search).toEqual("");
searchSubmit(Select, "one");
+2 -1
View File
@@ -120,11 +120,12 @@ describe("Moving the Typeahead Pointer", () => {
});
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", async () => {
const Select = mountDefault();
// Drop down must be open for $refs to exist
Select.vm.open = true;
await Select.vm.$nextTick();
/**
* Since JSDom doesn't render layouts, set the offsetHeight explicitly
@@ -0,0 +1,14 @@
import sortAndStringify from '../../../src/utility/sortAndStringify';
test('it will stringify an object', () => {
expect(sortAndStringify({hello: 'world'})).toEqual('{"hello":"world"}');
});
test('it will sort attributes alphabetically', () => {
expect(sortAndStringify({b: 'b', a: 'a'})).toEqual('{"a":"a","b":"b"}');
});
test('comparing two objects with unsorted keys', () => {
expect(sortAndStringify({b: 'b', a: 'a'}))
.toEqual(sortAndStringify({a: 'a', b: 'b'}))
});
+5
View File
@@ -0,0 +1,5 @@
import uniqueId from '../../../src/utility/uniqueId';
test('it generates a unique number', () => {
expect(uniqueId()).not.toEqual(uniqueId());
});
+4435 -1166
View File
File diff suppressed because it is too large Load Diff