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:
@@ -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!
|
||||
@@ -0,0 +1,2 @@
|
||||
github: [sagalbot]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -2,6 +2,7 @@
|
||||
node_modules
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!src/**/*
|
||||
!dist/**/*
|
||||
.DS_Store
|
||||
@@ -1,8 +0,0 @@
|
||||
language: node_js
|
||||
cache: yarn
|
||||
|
||||
node_js:
|
||||
- "8"
|
||||
|
||||
script:
|
||||
- yarn test --coverage --coverageReporters=text-lcov | coveralls
|
||||
@@ -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!
|
||||
@@ -1,18 +1,22 @@
|
||||
# vue-select      
|
||||
# vue-select    [](https://coveralls.io/github/sagalbot/vue-select?branch=master) 
|
||||
|
||||
> **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
|
||||
|
||||
@@ -14,11 +14,6 @@ module.exports = merge(baseWebpackConfig, {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
sourceMap: true,
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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&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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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&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;
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = process.env.hasOwnProperty('DEPLOY_PREVIEW');
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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')
|
||||
];
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const clientDynamicModules = require('./clientDynamicModules');
|
||||
|
||||
module.exports = {
|
||||
clientDynamicModules: async () => await clientDynamicModules(),
|
||||
};
|
||||
+23
-18
@@ -1,38 +1,43 @@
|
||||
<SponsorBanner />
|
||||
|
||||
# Vue Select
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
> 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
@@ -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
@@ -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">×</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>
|
||||
|
||||
@@ -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 />
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 no–op 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
sidebarDepth: 0
|
||||
---
|
||||
|
||||
<SponsorMe />
|
||||
|
||||
## Sponsors
|
||||
|
||||
<Sponsors />
|
||||
+213
-9
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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([[[]]]);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
@@ -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
@@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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'}))
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import uniqueId from '../../../src/utility/uniqueId';
|
||||
|
||||
test('it generates a unique number', () => {
|
||||
expect(uniqueId()).not.toEqual(uniqueId());
|
||||
});
|
||||
Reference in New Issue
Block a user