mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-19 09:50:33 +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
|
node_modules
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.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
|
> **Everything you wish the HTML `<select>` element could do, wrapped up into a lightweight, zero
|
||||||
dependency, extensible Vue component.**
|
> 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
|
- Tagging
|
||||||
- Filtering / Searching
|
- Filtering / Searching
|
||||||
- Vuex Support
|
- Vuex Support
|
||||||
- AJAX Support
|
- AJAX Support
|
||||||
- SSR Support
|
- SSR Support
|
||||||
|
- Accessible
|
||||||
- ~20kb Total / ~5kb CSS / ~15kb JS
|
- ~20kb Total / ~5kb CSS / ~15kb JS
|
||||||
- Select Single/Multiple Options
|
- Select Single/Multiple Options
|
||||||
- Customizable with slots and SCSS variables
|
- Customizable with slots and SCSS variables
|
||||||
- Tested with Bootstrap 3/4, Bulma, Foundation
|
|
||||||
- +95% Test Coverage
|
|
||||||
- Zero dependencies
|
- Zero dependencies
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
@@ -20,29 +24,44 @@ dependency, extensible Vue component.**
|
|||||||
Complete documentation and examples available at https://vue-select.org.
|
Complete documentation and examples available at https://vue-select.org.
|
||||||
|
|
||||||
- **[API Documentation](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)**
|
- **[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
|
## Install
|
||||||
|
|
||||||
```bash
|
```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
|
```js
|
||||||
import Vue from 'vue'
|
import Vue from "vue";
|
||||||
import vSelect from 'vue-select'
|
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
|
```js
|
||||||
<v-select v-model="selected" :options="['Vue.js','React']"></v-select>
|
import "vue-select/dist/vue-select.css";
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
You can also include vue-select directly in the browser. Check out the
|
||||||
|
|||||||
@@ -14,11 +14,6 @@ module.exports = merge(baseWebpackConfig, {
|
|||||||
minimizer: [
|
minimizer: [
|
||||||
new TerserPlugin({
|
new TerserPlugin({
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
terserOptions: {
|
|
||||||
compress: {
|
|
||||||
drop_console: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<sandbox hide-help v-slot="config">
|
<sandbox hide-help v-slot="config">
|
||||||
<v-select v-bind="config" />
|
<v-select v-bind="config"/>
|
||||||
</sandbox>
|
</sandbox>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-select
|
<v-select
|
||||||
|
placeholder="choose a country"
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
:options="['Canada', 'United States']"
|
|
||||||
:components="{Deselect}"
|
:components="{Deselect}"
|
||||||
|
:options="['Canada', 'United States']"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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);
|
loading(false);
|
||||||
});
|
});
|
||||||
}, 250),
|
}, 250),
|
||||||
fuseSearch (options, search) {
|
|
||||||
return new Fuse(options, {
|
|
||||||
keys: ['title', 'author.firstName', 'author.lastName'],
|
|
||||||
}).search(search);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</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 {description} = require('./config/meta');
|
||||||
|
const head = require('./config/head');
|
||||||
const meta = {
|
const plugins = require('./config/plugins');
|
||||||
title: 'Vue Select | VueJS Select2/Chosen Component',
|
const themeConfig = require('./config/themeConfig');
|
||||||
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'}],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
title: 'Vue Select',
|
title: 'Vue Select',
|
||||||
description: meta.description,
|
description,
|
||||||
head,
|
head,
|
||||||
plugins: {
|
plugins,
|
||||||
'@vuepress/google-analytics': {
|
themeConfig,
|
||||||
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'],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
};
|
||||||
+19
-14
@@ -1,38 +1,43 @@
|
|||||||
|
<SponsorBanner />
|
||||||
|
|
||||||
# Vue Select
|
# Vue Select
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|
|
||||||

|

|
||||||
|
|
||||||
> Everything you wish the HTML `<select>` element could do, wrapped
|
> Everything you wish the HTML `<select>` element could do, wrapped
|
||||||
up into a lightweight, extensible Vue component.
|
> up into a lightweight, extensible Vue component.
|
||||||
|
|
||||||
Vue Select is a feature rich select/dropdown/typeahead component. It provides a default
|
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;">
|
<div style="max-width:25rem; margin: 0 auto; padding: 1rem 0;">
|
||||||
<country-select />
|
<country-select />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
If you want to get a quick sense of what vue-select can do, check out
|
Vue Select aims to be as lightweight as possible, while maintaining high standards for accessibility,
|
||||||
[the sandbox](sandbox.md).
|
developer experience, and customization. Huge thanks to the [sponsors](sponsors.md) and
|
||||||
|
[contributors](contributors.md) that make Vue Select possible!
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
#### Features
|
|
||||||
- Tagging
|
- Tagging
|
||||||
- Filtering/Searching
|
- Filtering / Searching
|
||||||
- Vuex Support
|
- Vuex Support
|
||||||
- AJAX Support
|
- AJAX Support
|
||||||
- SSR Support
|
- SSR Support
|
||||||
- Select Single/Multiple Options
|
- Accessible
|
||||||
- Tested with Bootstrap 3/4, Bulma, Foundation
|
|
||||||
- +95% Test Coverage
|
|
||||||
- ~20kb Total / ~5kb CSS / ~15kb JS
|
- ~20kb Total / ~5kb CSS / ~15kb JS
|
||||||
|
- Select Single/Multiple Options
|
||||||
|
- Customizable with slots and SCSS variables
|
||||||
- Zero dependencies
|
- Zero dependencies
|
||||||
|
|
||||||
#### Resources
|
## Resources
|
||||||
- **[CodePen Template](http://codepen.io/sagalbot/pen/NpwrQO)**
|
|
||||||
- **[GitHub](https://github.com/sagalbot/vue-select)**
|
- **[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
|
## value
|
||||||
|
|
||||||
Contains the currently selected value. Very similar to a
|
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
|
## clearSearchOnSelect
|
||||||
|
|
||||||
Enables/disables clearing the search text when an option is selected.
|
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
|
## closeOnSelect
|
||||||
|
|
||||||
Close a dropdown when an option is chosen. Set to false to keep the dropdown
|
Close a dropdown when an option is chosen. Set to false to keep the dropdown
|
||||||
@@ -339,12 +395,22 @@ createOption: {
|
|||||||
|
|
||||||
## resetOnOptionsChange
|
## 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
|
```js
|
||||||
|
/**
|
||||||
|
* @type {Boolean|Function}
|
||||||
|
* @param {Array} newOptions
|
||||||
|
* @param {Array} oldOptions
|
||||||
|
* @param {Array} selectedValue
|
||||||
|
*/
|
||||||
resetOnOptionsChange: {
|
resetOnOptionsChange: {
|
||||||
type: Boolean,
|
default: false,
|
||||||
default: false
|
validator: (value) => ['function', 'boolean'].includes(typeof value)
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -389,3 +455,4 @@ selectOnTab: {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
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.
|
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
|
When implementing this slot, you'll likely need to use `appendToBody` to position the dropdown.
|
||||||
<slot name="selected-option" v-bind="(typeof option === 'object')?option:{[label]: option}">
|
Otherwise content in this slot will affect it's positioning.
|
||||||
{{ getOptionLabel(option) }}
|
|
||||||
</slot>
|
- `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
|
- `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
|
- `deselect {Function}` - Method used to deselect a given option when `multiple` is true
|
||||||
- `disabled {Boolean}` - Determine if the component is disabled
|
- `disabled {Boolean}` - Determine if the component is disabled
|
||||||
- `multiple {Boolean}` - If the component supports the selection of multiple values
|
- `multiple {Boolean}` - If the component supports the selection of multiple values
|
||||||
|
|
||||||
```html
|
<SlotSelectedOptionContainer />
|
||||||
<slot v-for="option in valueAsArray" name="selected-option-container"
|
<<< @/.vuepress/components/SlotSelectedOptionContainer.vue
|
||||||
: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>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Component Actions
|
## `spinner`
|
||||||
|
|
||||||
### `spinner`
|
- `loading {Boolean}` - if the component is in a loading state
|
||||||
|
|
||||||
```html
|
<SlotSpinner />
|
||||||
<slot name="spinner">
|
<<< @/.vuepress/components/SlotSpinner.vue
|
||||||
<div class="spinner" v-show="mutableLoading">Loading...</div>
|
|
||||||
</slot>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dropdown
|
</div>
|
||||||
|
|
||||||
### `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>
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -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
|
[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.
|
[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.
|
specifications and best practices for creating accessible components.
|
||||||
|
|
||||||
## Combobox
|
## 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
|
<<< @/.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}
|
||||||
+16
-6
@@ -4,19 +4,29 @@ 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.
|
[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.
|
vue-select provides the scoped `option` slot in order to create custom dropdown templates.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<v-select :options="options" label="title">
|
<v-select :options="options" label="title">
|
||||||
<template v-slot:option="option">
|
<template v-slot:option="option">
|
||||||
<span :class="option.icon"></span>
|
<span :class="option.icon"></span>
|
||||||
{{ option.title }}
|
{{ option.title }}
|
||||||
</template>
|
</template>
|
||||||
</v-select>
|
</v-select>
|
||||||
```
|
```
|
||||||
|
|
||||||
Using the `option` slot with props `"option"` provides the current option variable to the template.
|
Using the `option` slot with props `"option"` provides the current option variable to the template.
|
||||||
|
|
||||||
<CodePen url="NXBwYG" height="500"/>
|
<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" />
|
<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
|
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 run the callback if it had at least one
|
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
|
character. This was a design mistake, as it should be the consumers decision if a search should be
|
||||||
run on an empty string.
|
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.
|
If you want added tags to be pushed to the options array, set `push-tags` to true.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<v-select taggable multiple />
|
<v-select taggable multiple push-tags />
|
||||||
```
|
```
|
||||||
|
|
||||||
<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"
|
"build:preview": "cross-env DEPLOY_PREVIEW=true vuepress build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@octokit/graphql": "^4.3.1",
|
||||||
|
"@popperjs/core": "^2.1.0",
|
||||||
"@vuepress/plugin-active-header-links": "^1.0.0-alpha.47",
|
"@vuepress/plugin-active-header-links": "^1.0.0-alpha.47",
|
||||||
"@vuepress/plugin-google-analytics": "^1.0.0-alpha.47",
|
"@vuepress/plugin-google-analytics": "^1.0.0-alpha.47",
|
||||||
"@vuepress/plugin-nprogress": "^1.0.0-alpha.47",
|
"@vuepress/plugin-nprogress": "^1.0.0-alpha.47",
|
||||||
"@vuepress/plugin-pwa": "^1.0.0-alpha.47",
|
"@vuepress/plugin-pwa": "^1.0.0-alpha.47",
|
||||||
"@vuepress/plugin-register-components": "^1.0.0-alpha.47",
|
"@vuepress/plugin-register-components": "^1.0.0-alpha.47",
|
||||||
"@vuepress/plugin-search": "^1.0.0-alpha.47",
|
"@vuepress/plugin-search": "^1.0.0-alpha.47",
|
||||||
|
"axios": "^0.19.2",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
|
"date-fns": "^2.11.0",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
"fuse.js": "^3.4.4",
|
"fuse.js": "^3.4.4",
|
||||||
"gh-pages": "^0.11.0",
|
"gh-pages": "^0.11.0",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.12.0",
|
||||||
|
"octonode": "^0.9.5",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vuepress": "^1.0.0-alpha.47",
|
"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"
|
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
||||||
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
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@*":
|
"@types/events@*":
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.4.tgz#64db61e0359eb5a8d99b55e05c729f130a678b04"
|
||||||
integrity sha512-W0+n1Y+gK/8G2P/piTkBBN38Qc5Q1ZSO6B5H3QmPCUewaiXOo2GCAWZ4ElZCcNhjJuBSUSLGFUJnmlCn5+nxOQ==
|
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":
|
"@types/q@^1.5.1":
|
||||||
version "1.5.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
|
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:
|
dependencies:
|
||||||
array-uniq "^1.0.1"
|
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:
|
array-uniq@^1.0.1:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
|
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"
|
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
|
||||||
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
|
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:
|
babel-extract-comments@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-extract-comments/-/babel-extract-comments-1.0.0.tgz#0a2aedf81417ed391b85e18b4614e693a0351a21"
|
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"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
|
||||||
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
|
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:
|
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
|
||||||
version "4.11.8"
|
version "4.11.8"
|
||||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||||
@@ -2532,6 +2607,11 @@ dashdash@^1.12.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
assert-plus "^1.0.0"
|
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:
|
date-now@^0.1.4:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
|
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:
|
dependencies:
|
||||||
ms "2.0.0"
|
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:
|
debug@^3.0.0, debug@^3.2.5, debug@^3.2.6:
|
||||||
version "3.2.6"
|
version "3.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
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:
|
dependencies:
|
||||||
ms "^2.1.1"
|
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:
|
decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
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"
|
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||||
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
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:
|
des.js@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
|
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
|
||||||
@@ -2814,6 +2899,11 @@ dot-prop@^4.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-obj "^1.0.0"
|
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:
|
duplexify@^3.4.2, duplexify@^3.6.0:
|
||||||
version "3.7.1"
|
version "3.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
|
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"
|
inherits "^2.0.3"
|
||||||
readable-stream "^2.3.6"
|
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:
|
follow-redirects@^1.0.0:
|
||||||
version "1.8.1"
|
version "1.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.8.1.tgz#24804f9eaab67160b0e840c085885d606371a35b"
|
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"
|
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||||
integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
|
integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
|
||||||
|
|
||||||
har-validator@~5.1.0:
|
har-validator@~5.1.0, har-validator@~5.1.3:
|
||||||
version "5.1.3"
|
version "5.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
|
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
|
||||||
integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
|
integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
|
||||||
@@ -4241,6 +4338,13 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isobject "^3.0.1"
|
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:
|
is-regex@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
|
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"
|
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||||
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
|
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:
|
isstream@~0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||||
@@ -4637,6 +4746,11 @@ lru-cache@^5.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist "^3.0.2"
|
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:
|
make-dir@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
|
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
|
||||||
@@ -5048,6 +5162,11 @@ no-case@^2.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lower-case "^1.1.1"
|
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:
|
node-forge@0.7.5:
|
||||||
version "0.7.5"
|
version "0.7.5"
|
||||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df"
|
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"
|
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
|
||||||
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
|
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:
|
on-finished@~2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
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"
|
lcid "^2.0.0"
|
||||||
mem "^4.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:
|
os-tmpdir@^1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
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"
|
resolved "https://registry.yarnpkg.com/psl/-/psl-1.3.1.tgz#d5aa3873a35ec450bc7db9012ad5a7246f6fc8bd"
|
||||||
integrity sha512-2KLd5fKOdAfShtY2d/8XDWVRnmp3zp40Qt6ge2zBPFARLXOGUf2fHD5eg+TV/5oxBtQKVhjUaKFsAaE4HnwfSA==
|
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:
|
public-encrypt@^4.0.0:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
|
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"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||||
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
||||||
|
|
||||||
punycode@^2.1.0:
|
punycode@^2.1.0, punycode@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||||
@@ -6241,6 +6383,13 @@ randomfill@^1.0.3:
|
|||||||
randombytes "^2.0.5"
|
randombytes "^2.0.5"
|
||||||
safe-buffer "^5.1.0"
|
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:
|
range-parser@^1.2.1, range-parser@~1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||||
@@ -6445,6 +6594,32 @@ repeating@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-finite "^1.0.0"
|
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:
|
request@^2.87.0, request@^2.88.0:
|
||||||
version "2.88.0"
|
version "2.88.0"
|
||||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
|
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"
|
psl "^1.1.24"
|
||||||
punycode "^1.4.1"
|
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:
|
trim-newlines@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
|
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
|
||||||
@@ -7537,6 +7720,20 @@ unique-slug@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
imurmurhash "^0.1.4"
|
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:
|
universalify@^0.1.0:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
@@ -7976,6 +8173,13 @@ wide-align@^1.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
string-width "^1.0.2 || 2"
|
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:
|
workbox-background-sync@^4.3.1:
|
||||||
version "4.3.1"
|
version "4.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-4.3.1.tgz#26821b9bf16e9e37fd1d640289edddc08afd1950"
|
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",
|
"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.",
|
"description": "Everything you wish the HTML <select> element could do, wrapped up into a lightweight, extensible Vue component.",
|
||||||
"author": "Jeff Sagal <sagalbot@gmail.com>",
|
"author": "Jeff Sagal <sagalbot@gmail.com>",
|
||||||
"homepage": "https://vue-select.org",
|
"homepage": "https://vue-select.org",
|
||||||
@@ -11,13 +11,16 @@
|
|||||||
"private": false,
|
"private": false,
|
||||||
"main": "dist/vue-select.js",
|
"main": "dist/vue-select.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"prepare": "npm run build",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "webpack-dev-server --config build/webpack.dev.conf.js --hot --progress -d",
|
"serve": "webpack-dev-server --config build/webpack.dev.conf.js --hot --progress -d",
|
||||||
"serve:docs": "cd docs && yarn serve",
|
"serve:docs": "cd docs && yarn serve",
|
||||||
"build": "cross-env NODE_ENV=production webpack --config build/webpack.prod.conf.js --progress",
|
"build": "cross-env NODE_ENV=production webpack --config build/webpack.prod.conf.js --progress",
|
||||||
"build:docs": "cd docs && yarn build",
|
"build:docs": "cd docs && yarn build",
|
||||||
"build:preview": "cd docs && yarn build",
|
"build:preview": "cd docs && yarn build",
|
||||||
"test": "jest"
|
"test": "jest",
|
||||||
|
"semantic-release": "semantic-release",
|
||||||
|
"commit": "git-cz"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -34,15 +37,20 @@
|
|||||||
"@babel/plugin-transform-runtime": "^7.4.0",
|
"@babel/plugin-transform-runtime": "^7.4.0",
|
||||||
"@babel/preset-env": "^7.4.2",
|
"@babel/preset-env": "^7.4.2",
|
||||||
"@babel/runtime": "^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",
|
"autoprefixer": "^9.4.7",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-loader": "^8.0.5",
|
"babel-loader": "^8.0.5",
|
||||||
|
"bundlewatch": "^0.2.5",
|
||||||
"chokidar": "^2.1.5",
|
"chokidar": "^2.1.5",
|
||||||
|
"commitizen": "^4.0.3",
|
||||||
"coveralls": "^3.0.2",
|
"coveralls": "^3.0.2",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"css-loader": "^2.1.0",
|
"css-loader": "^2.1.0",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
|
"cz-conventional-changelog": "3.1.0",
|
||||||
"html-loader": "^0.5.5",
|
"html-loader": "^0.5.5",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"jest": "^24.1.0",
|
"jest": "^24.1.0",
|
||||||
@@ -53,6 +61,7 @@
|
|||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"postcss-scss": "^2.0.0",
|
"postcss-scss": "^2.0.0",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^7.1.0",
|
||||||
|
"semantic-release": "^17.0.4",
|
||||||
"terser-webpack-plugin": "^1.2.3",
|
"terser-webpack-plugin": "^1.2.3",
|
||||||
"url-loader": "^1.1.2",
|
"url-loader": "^1.1.2",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
@@ -97,8 +106,26 @@
|
|||||||
"!**/node_modules/**"
|
"!**/node_modules/**"
|
||||||
],
|
],
|
||||||
"coverageReporters": [
|
"coverageReporters": [
|
||||||
"html",
|
"text"
|
||||||
"text-summary"
|
]
|
||||||
|
},
|
||||||
|
"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>
|
<template>
|
||||||
<div :dir="dir" class="v-select" :class="stateClasses">
|
<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">
|
<div class="vs__selected-options" ref="selectedOptions">
|
||||||
<slot v-for="option in selectedValue"
|
<slot v-for="option in selectedValue"
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
<slot name="selected-option" v-bind="normalizeOptionForSlot(option)">
|
<slot name="selected-option" v-bind="normalizeOptionForSlot(option)">
|
||||||
{{ getOptionLabel(option) }}
|
{{ getOptionLabel(option) }}
|
||||||
</slot>
|
</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" />
|
<component :is="childComponents.Deselect" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@@ -28,14 +29,16 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vs__actions">
|
<div class="vs__actions" ref="actions">
|
||||||
<button
|
<button
|
||||||
v-show="showClearButton"
|
v-show="showClearButton"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="clearSelection"
|
@click="clearSelection"
|
||||||
type="button"
|
type="button"
|
||||||
class="vs__clear"
|
class="vs__clear"
|
||||||
title="Clear selection"
|
title="Clear Selected"
|
||||||
|
aria-label="Clear Selected"
|
||||||
|
ref="clearButton"
|
||||||
>
|
>
|
||||||
<component :is="childComponents.Deselect" />
|
<component :is="childComponents.Deselect" />
|
||||||
</button>
|
</button>
|
||||||
@@ -49,15 +52,17 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition :name="transition">
|
<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
|
<li
|
||||||
role="option"
|
role="option"
|
||||||
v-for="(option, index) in filteredOptions"
|
v-for="(option, index) in filteredOptions"
|
||||||
:key="getOptionKey(option)"
|
:key="getOptionKey(option)"
|
||||||
|
:id="`vs${uid}__option-${index}`"
|
||||||
class="vs__dropdown-option"
|
class="vs__dropdown-option"
|
||||||
:class="{ 'vs__dropdown-option--selected': isOptionSelected(option), 'vs__dropdown-option--highlight': index === typeAheadPointer, 'vs__dropdown-option--disabled': !selectable(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"
|
@mouseover="selectable(option) ? typeAheadPointer = index : null"
|
||||||
@mousedown.prevent.stop="selectable(option) ? select(option) : null"
|
@mousedown.prevent.stop="selectable(option) ? select(option) : null"
|
||||||
>
|
>
|
||||||
@@ -65,11 +70,14 @@
|
|||||||
{{ getOptionLabel(option) }}
|
{{ getOptionLabel(option) }}
|
||||||
</slot>
|
</slot>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="!filteredOptions.length" class="vs__no-options" @mousedown.stop="">
|
<li v-if="filteredOptions.length === 0" class="vs__no-options" @mousedown.stop="">
|
||||||
<slot name="no-options">Sorry, no matching options.</slot>
|
<slot name="no-options" v-bind="scope.noOptions">Sorry, no matching options.</slot>
|
||||||
</li>
|
</li>
|
||||||
|
<slot name="list-footer" v-bind="scope.listFooter" />
|
||||||
</ul>
|
</ul>
|
||||||
|
<ul v-else :id="`vs${uid}__listbox`" role="listbox" style="display: none; visibility: hidden;"></ul>
|
||||||
</transition>
|
</transition>
|
||||||
|
<slot name="footer" v-bind="scope.footer" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -78,12 +86,20 @@
|
|||||||
import typeAheadPointer from '../mixins/typeAheadPointer'
|
import typeAheadPointer from '../mixins/typeAheadPointer'
|
||||||
import ajax from '../mixins/ajax'
|
import ajax from '../mixins/ajax'
|
||||||
import childComponents from './childComponents';
|
import childComponents from './childComponents';
|
||||||
|
import appendToBody from '../directives/appendToBody';
|
||||||
|
import sortAndStringify from '../utility/sortAndStringify'
|
||||||
|
import uniqueId from '../utility/uniqueId';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name VueSelect
|
||||||
|
*/
|
||||||
export default {
|
export default {
|
||||||
components: {...childComponents},
|
components: {...childComponents},
|
||||||
|
|
||||||
mixins: [pointerScroll, typeAheadPointer, ajax],
|
mixins: [pointerScroll, typeAheadPointer, ajax],
|
||||||
|
|
||||||
|
directives: {appendToBody},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
/**
|
/**
|
||||||
* Contains the currently selected value. Very similar to a
|
* 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.
|
* are displayed but disabled and cannot be selected.
|
||||||
*
|
*
|
||||||
* @type {Function}
|
* @type {Function}
|
||||||
|
* @since 3.3.0
|
||||||
* @param {Object|String} option
|
* @param {Object|String} option
|
||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
@@ -268,12 +285,16 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback to get an option key. If {option}
|
* Generate a unique identifier for each option. If `option`
|
||||||
* is an object and has an {id}, returns {option.id}
|
* is an object and `option.hasOwnProperty('id')` exists,
|
||||||
* by default, otherwise tries to serialize {option}
|
* `option.id` is used by default, otherwise the option
|
||||||
* to JSON.
|
* 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}
|
* @type {Function}
|
||||||
* @param {Object || String} option
|
* @param {Object || String} option
|
||||||
@@ -281,23 +302,21 @@
|
|||||||
*/
|
*/
|
||||||
getOptionKey: {
|
getOptionKey: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default(option) {
|
default (option) {
|
||||||
if (typeof option === 'object' && option.id) {
|
if (typeof option !== 'object') {
|
||||||
return option.id
|
return option;
|
||||||
} 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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
|
* Select the current value if selectOnTab is enabled
|
||||||
|
* @deprecated since 3.3
|
||||||
*/
|
*/
|
||||||
onTab: {
|
onTab: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: function () {
|
default: function () {
|
||||||
if (this.selectOnTab) {
|
if (this.selectOnTab && !this.isComposing) {
|
||||||
this.typeAheadSelect();
|
this.typeAheadSelect();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -399,7 +419,7 @@
|
|||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
filter: {
|
filter: {
|
||||||
"type": Function,
|
type: Function,
|
||||||
default(options, search) {
|
default(options, search) {
|
||||||
return options.filter((option) => {
|
return options.filter((option) => {
|
||||||
let label = this.getOptionLabel(option)
|
let label = this.getOptionLabel(option)
|
||||||
@@ -417,23 +437,37 @@
|
|||||||
*/
|
*/
|
||||||
createOption: {
|
createOption: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default(newOption) {
|
default (option) {
|
||||||
if (typeof this.optionList[0] === 'object') {
|
return (typeof this.optionList[0] === 'object') ? {[this.label]: option} : option;
|
||||||
newOption = {[this.label]: newOption}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('option:created', newOption)
|
|
||||||
return newOption
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When false, updating the options will not reset the select value
|
* When false, updating the options will not reset the selected value. Accepts
|
||||||
* @type {Boolean}
|
* 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: {
|
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
|
* When true, hitting the 'tab' key will select the current select value
|
||||||
* @type {Boolean}
|
* @type {Boolean}
|
||||||
|
* @deprecated since 3.3 - use selectOnKeyCodes instead
|
||||||
*/
|
*/
|
||||||
selectOnTab: {
|
selectOnTab: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keycodes that will select the current option.
|
||||||
|
* @type Array
|
||||||
|
*/
|
||||||
|
selectOnKeyCodes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [13],
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query Selector used to find the search input
|
* Query Selector used to find the search input
|
||||||
* when the 'search' scoped slot is used.
|
* when the 'search' scoped slot is used.
|
||||||
@@ -486,13 +530,69 @@
|
|||||||
searchInputQuerySelector: {
|
searchInputQuerySelector: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '[type=search]'
|
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() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
uid: uniqueId(),
|
||||||
search: '',
|
search: '',
|
||||||
open: false,
|
open: false,
|
||||||
|
isComposing: false,
|
||||||
pushedTags: [],
|
pushedTags: [],
|
||||||
_value: [] // Internal value managed by Vue Select if no `value` prop is passed
|
_value: [] // Internal value managed by Vue Select if no `value` prop is passed
|
||||||
}
|
}
|
||||||
@@ -506,13 +606,17 @@
|
|||||||
* is correct.
|
* is correct.
|
||||||
* @return {[type]} [description]
|
* @return {[type]} [description]
|
||||||
*/
|
*/
|
||||||
options(val) {
|
options (newOptions, oldOptions) {
|
||||||
if (!this.taggable && this.resetOnOptionsChange) {
|
let shouldReset = () => typeof this.resetOnOptionsChange === 'function'
|
||||||
this.clearSelection()
|
? this.resetOnOptionsChange(newOptions, oldOptions, this.selectedValue)
|
||||||
|
: this.resetOnOptionsChange;
|
||||||
|
|
||||||
|
if (!this.taggable && shouldReset()) {
|
||||||
|
this.clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.value && this.isTrackingValues) {
|
if (this.value && this.isTrackingValues) {
|
||||||
this.setInternalValueFromOptions(this.value)
|
this.setInternalValueFromOptions(this.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -534,6 +638,10 @@
|
|||||||
*/
|
*/
|
||||||
multiple() {
|
multiple() {
|
||||||
this.clearSelection()
|
this.clearSelection()
|
||||||
|
},
|
||||||
|
|
||||||
|
open(isOpen) {
|
||||||
|
this.$emit(isOpen ? 'open' : 'close');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -544,7 +652,7 @@
|
|||||||
this.setInternalValueFromOptions(this.value)
|
this.setInternalValueFromOptions(this.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$on('option:created', this.maybePushTag)
|
this.$on('option:created', this.pushTag)
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -570,14 +678,13 @@
|
|||||||
select(option) {
|
select(option) {
|
||||||
if (!this.isOptionSelected(option)) {
|
if (!this.isOptionSelected(option)) {
|
||||||
if (this.taggable && !this.optionExists(option)) {
|
if (this.taggable && !this.optionExists(option)) {
|
||||||
option = this.createOption(option)
|
this.$emit('option:created', option);
|
||||||
}
|
}
|
||||||
if (this.multiple) {
|
if (this.multiple) {
|
||||||
option = this.selectedValue.concat(option)
|
option = this.selectedValue.concat(option)
|
||||||
}
|
}
|
||||||
this.updateValue(option);
|
this.updateValue(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onAfterSelect(option)
|
this.onAfterSelect(option)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -646,31 +753,23 @@
|
|||||||
* @param {Event} e
|
* @param {Event} e
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
toggleDropdown (e) {
|
toggleDropdown ({target}) {
|
||||||
const target = e.target;
|
// don't react to click on deselect/clear buttons,
|
||||||
const toggleTargets = [
|
// they dropdown state will be set in their click handlers
|
||||||
this.$el,
|
const ignoredButtons = [
|
||||||
this.searchEl,
|
...(this.$refs['deselectButtons'] || []),
|
||||||
this.$refs.toggle,
|
...([this.$refs['clearButton']] || [])
|
||||||
];
|
];
|
||||||
|
|
||||||
if (typeof this.$refs.openIndicator !== 'undefined') {
|
if (ignoredButtons.some(ref => ref.contains(target) || ref === target)) {
|
||||||
toggleTargets.push(
|
return;
|
||||||
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 (toggleTargets.indexOf(target) > -1 || target.classList.contains('vs__selected')) {
|
if (this.open) {
|
||||||
if (this.open) {
|
this.searchEl.blur();
|
||||||
this.searchEl.blur(); // dropdown will close on blur
|
} else if (!this.disabled) {
|
||||||
} else {
|
this.open = true;
|
||||||
if (!this.disabled) {
|
this.searchEl.focus();
|
||||||
this.open = true;
|
|
||||||
this.searchEl.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -680,42 +779,22 @@
|
|||||||
* @return {Boolean} True when selected | False otherwise
|
* @return {Boolean} True when selected | False otherwise
|
||||||
*/
|
*/
|
||||||
isOptionSelected(option) {
|
isOptionSelected(option) {
|
||||||
return this.selectedValue.some(value => {
|
return this.selectedValue.some(value => this.optionComparator(value, option))
|
||||||
return this.optionComparator(value, option)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if two option objects are matching.
|
* Determine if two option objects are matching.
|
||||||
*
|
*
|
||||||
* @param value {Object}
|
* @param a {Object}
|
||||||
* @param option {Object}
|
* @param b {Object}
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
optionComparator(value, option) {
|
optionComparator(a, b) {
|
||||||
if (typeof value !== 'object' && typeof option !== 'object') {
|
return this.getOptionKey(a) === this.getOptionKey(b);
|
||||||
// Comparing primitives
|
|
||||||
if (value === option) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Comparing objects
|
|
||||||
if (value === this.reduce(option)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if ((this.getOptionLabel(value) === this.getOptionLabel(option)) || (this.getOptionLabel(value) === option)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (this.reduce(value) === this.reduce(option)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds an option from this.options
|
* Finds an option from the options
|
||||||
* where a reduced value matches
|
* where a reduced value matches
|
||||||
* the passed in value.
|
* the passed in value.
|
||||||
*
|
*
|
||||||
@@ -723,7 +802,24 @@
|
|||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
findOptionFromReducedValue (value) {
|
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}
|
* @return {this.value}
|
||||||
*/
|
*/
|
||||||
maybeDeleteValue() {
|
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;
|
let value = null;
|
||||||
if (this.multiple) {
|
if (this.multiple) {
|
||||||
value = [...this.selectedValue.slice(0, this.selectedValue.length - 1)]
|
value = [...this.selectedValue.slice(0, this.selectedValue.length - 1)]
|
||||||
@@ -759,14 +855,7 @@
|
|||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
optionExists(option) {
|
optionExists(option) {
|
||||||
return this.optionList.some(opt => {
|
return this.optionList.some(_option => this.optionComparator(_option, option))
|
||||||
if (typeof opt === 'object' && this.getOptionLabel(opt) === option) {
|
|
||||||
return true
|
|
||||||
} else if (opt === option) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -786,10 +875,8 @@
|
|||||||
* @param {Object || String} option
|
* @param {Object || String} option
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
maybePushTag(option) {
|
pushTag (option) {
|
||||||
if (this.pushTags) {
|
this.pushedTags.push(option);
|
||||||
this.pushedTags.push(option)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -814,7 +901,8 @@
|
|||||||
if (this.mousedown && !this.searching) {
|
if (this.mousedown && !this.searching) {
|
||||||
this.mousedown = false
|
this.mousedown = false
|
||||||
} else {
|
} else {
|
||||||
if (this.clearSearchOnBlur) {
|
const { clearSearchOnSelect, multiple } = this;
|
||||||
|
if (this.clearSearchOnBlur({ clearSearchOnSelect, multiple })) {
|
||||||
this.search = ''
|
this.search = ''
|
||||||
}
|
}
|
||||||
this.closeSearchOptions()
|
this.closeSearchOptions()
|
||||||
@@ -859,39 +947,46 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search 'input' KeyBoardEvent handler.
|
* Search <input> KeyBoardEvent handler.
|
||||||
* @param e {KeyboardEvent}
|
* @param e {KeyboardEvent}
|
||||||
* @return {Function}
|
* @return {Function}
|
||||||
*/
|
*/
|
||||||
onSearchKeyDown (e) {
|
onSearchKeyDown (e) {
|
||||||
switch (e.keyCode) {
|
const preventAndSelect = e => {
|
||||||
case 8:
|
e.preventDefault();
|
||||||
// delete
|
return !this.isComposing && this.typeAheadSelect();
|
||||||
return this.maybeDeleteValue();
|
};
|
||||||
case 9:
|
|
||||||
// tab
|
const defaults = {
|
||||||
return this.onTab();
|
// backspace
|
||||||
case 13:
|
8: e => this.maybeDeleteValue(),
|
||||||
// enter.prevent
|
// tab
|
||||||
e.preventDefault();
|
9: e => this.onTab(),
|
||||||
return this.typeAheadSelect();
|
// esc
|
||||||
case 27:
|
27: e => this.onEscape(),
|
||||||
// esc
|
// up.prevent
|
||||||
return this.onEscape();
|
38: e => {
|
||||||
case 38:
|
|
||||||
// up.prevent
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return this.typeAheadUp();
|
return this.typeAheadUp();
|
||||||
case 40:
|
},
|
||||||
// down.prevent
|
// down.prevent
|
||||||
|
40: e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return this.typeAheadDown();
|
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: {
|
computed: {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the component needs to
|
* Determine if the component needs to
|
||||||
* track the state of values internally.
|
* track the state of values internally.
|
||||||
@@ -928,7 +1023,7 @@
|
|||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
optionList () {
|
optionList () {
|
||||||
return this.options.concat(this.pushedTags);
|
return this.options.concat(this.pushTags ? this.pushedTags : []);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -946,6 +1041,12 @@
|
|||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
scope () {
|
scope () {
|
||||||
|
const listSlot = {
|
||||||
|
search: this.search,
|
||||||
|
loading: this.loading,
|
||||||
|
searching: this.searching,
|
||||||
|
filteredOptions: this.filteredOptions
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
search: {
|
search: {
|
||||||
attributes: {
|
attributes: {
|
||||||
@@ -954,24 +1055,32 @@
|
|||||||
'tabindex': this.tabindex,
|
'tabindex': this.tabindex,
|
||||||
'readonly': !this.searchable,
|
'readonly': !this.searchable,
|
||||||
'id': this.inputId,
|
'id': this.inputId,
|
||||||
'aria-expanded': this.dropdownOpen,
|
'aria-autocomplete': 'list',
|
||||||
'aria-label': 'Search for option',
|
'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',
|
'ref': 'search',
|
||||||
'role': 'combobox',
|
|
||||||
'type': 'search',
|
'type': 'search',
|
||||||
'autocomplete': 'off',
|
'autocomplete': this.autocomplete,
|
||||||
'value': this.search,
|
'value': this.search,
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
|
'compositionstart': () => this.isComposing = true,
|
||||||
|
'compositionend': () => this.isComposing = false,
|
||||||
'keydown': this.onSearchKeyDown,
|
'keydown': this.onSearchKeyDown,
|
||||||
'blur': this.onSearchBlur,
|
'blur': this.onSearchBlur,
|
||||||
'focus': this.onSearchFocus,
|
'focus': this.onSearchFocus,
|
||||||
'input': (e) => this.search = e.target.value
|
'input': (e) => this.search = e.target.value,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
spinner: {
|
spinner: {
|
||||||
loading: this.mutableLoading
|
loading: this.mutableLoading
|
||||||
},
|
},
|
||||||
|
noOptions: {
|
||||||
|
search: this.search,
|
||||||
|
loading: this.loading,
|
||||||
|
searching: this.searching,
|
||||||
|
},
|
||||||
openIndicator: {
|
openIndicator: {
|
||||||
attributes: {
|
attributes: {
|
||||||
'ref': 'openIndicator',
|
'ref': 'openIndicator',
|
||||||
@@ -979,6 +1088,10 @@
|
|||||||
'class': 'vs__open-indicator',
|
'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
|
* Return the current state of the
|
||||||
* search input
|
* search input
|
||||||
@@ -1065,7 +1170,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let options = this.search.length ? this.filter(optionList, this.search, this) : optionList;
|
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)
|
options.unshift(this.search)
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
@@ -1085,7 +1190,7 @@
|
|||||||
*/
|
*/
|
||||||
showClearButton() {
|
showClearButton() {
|
||||||
return !this.multiple && this.clearable && !this.open && !this.isValueEmpty
|
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() {
|
pixelsToPointerTop() {
|
||||||
let pixelsToPointerTop = 0;
|
let pixelsToPointerTop = 0;
|
||||||
if (this.$refs.dropdownMenu) {
|
if (this.$refs.dropdownMenu && this.dropdownOpen) {
|
||||||
for (let i = 0; i < this.typeAheadPointer; i++) {
|
for (let i = 0; i < this.typeAheadPointer; i++) {
|
||||||
pixelsToPointerTop += this.$refs.dropdownMenu.children[i]
|
pixelsToPointerTop += this.$refs.dropdownMenu.children[i]
|
||||||
.offsetHeight;
|
.offsetHeight;
|
||||||
|
|||||||
@@ -56,15 +56,15 @@ export default {
|
|||||||
* Optionally clear the search input on selection.
|
* Optionally clear the search input on selection.
|
||||||
* @return {void}
|
* @return {void}
|
||||||
*/
|
*/
|
||||||
typeAheadSelect() {
|
typeAheadSelect () {
|
||||||
if( this.filteredOptions[ this.typeAheadPointer ] ) {
|
if (!this.taggable && this.filteredOptions[this.typeAheadPointer]) {
|
||||||
this.select( this.filteredOptions[ this.typeAheadPointer ] );
|
this.select(this.filteredOptions[this.typeAheadPointer]);
|
||||||
} else if (this.taggable && this.search.length){
|
} else if (this.taggable && this.search.length) {
|
||||||
this.select(this.search)
|
this.select(this.createOption(this.search));
|
||||||
}
|
}
|
||||||
|
|
||||||
if( this.clearSearchOnSelect ) {
|
if (this.clearSearchOnSelect) {
|
||||||
this.search = "";
|
this.search = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ $transition-duration: .15s;
|
|||||||
/* Dropdown Default Transition */
|
/* Dropdown Default Transition */
|
||||||
.vs__fade-enter-active,
|
.vs__fade-enter-active,
|
||||||
.vs__fade-leave-active {
|
.vs__fade-leave-active {
|
||||||
|
pointer-events: none;
|
||||||
transition: opacity $transition-duration $transition-timing-function;
|
transition: opacity $transition-duration $transition-timing-function;
|
||||||
}
|
}
|
||||||
.vs__fade-enter,
|
.vs__fade-enter,
|
||||||
|
|||||||
@@ -3,8 +3,16 @@
|
|||||||
$line-height: $vs-component-line-height;
|
$line-height: $vs-component-line-height;
|
||||||
$font-size: 1em;
|
$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-decoration,
|
||||||
.vs__search::-webkit-search-cancel-button,
|
|
||||||
.vs__search::-webkit-search-results-button,
|
.vs__search::-webkit-search-results-button,
|
||||||
.vs__search::-webkit-search-results-decoration,
|
.vs__search::-webkit-search-results-decoration,
|
||||||
.vs__search::-ms-clear {
|
.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);
|
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();
|
const Select = selectWithProps();
|
||||||
|
|
||||||
Select.vm.search = "foo";
|
Select.vm.search = "foo";
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
const events = Select.emitted("search");
|
const events = Select.emitted("search");
|
||||||
|
|
||||||
@@ -24,11 +25,13 @@ describe("Asynchronous Loading", () => {
|
|||||||
expect(events.length).toEqual(1);
|
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();
|
const Select = selectWithProps();
|
||||||
|
|
||||||
Select.vm.search = "foo";
|
Select.vm.search = "foo";
|
||||||
|
await Select.vm.$nextTick();
|
||||||
Select.vm.search = "";
|
Select.vm.search = "";
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
const events = Select.emitted("search");
|
const events = Select.emitted("search");
|
||||||
|
|
||||||
@@ -36,7 +39,7 @@ describe("Asynchronous Loading", () => {
|
|||||||
expect(events.length).toEqual(2);
|
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, {
|
const Select = shallowMount(vSelect, {
|
||||||
listeners: {
|
listeners: {
|
||||||
search: (search, loading) => {
|
search: (search, loading) => {
|
||||||
@@ -47,13 +50,16 @@ describe("Asynchronous Loading", () => {
|
|||||||
|
|
||||||
Select.vm.mutableLoading = true;
|
Select.vm.mutableLoading = true;
|
||||||
Select.vm.search = 'foo';
|
Select.vm.search = 'foo';
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
expect(Select.vm.mutableLoading).toEqual(false);
|
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 });
|
const Select = selectWithProps({ loading: false });
|
||||||
Select.setProps({ loading: true });
|
Select.setProps({ loading: true });
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
expect(Select.vm.mutableLoading).toEqual(true);
|
expect(Select.vm.mutableLoading).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { selectWithProps } from "../helpers";
|
import { selectWithProps } from "../helpers";
|
||||||
|
|
||||||
describe("Removing values", () => {
|
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 });
|
const Select = selectWithProps({ multiple: true });
|
||||||
Select.vm.$data._value = 'one';
|
Select.vm.$data._value = 'one';
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
Select.find(".vs__deselect").trigger("click");
|
Select.find(".vs__deselect").trigger("click");
|
||||||
expect(Select.emitted().input).toEqual([[[]]]);
|
expect(Select.emitted().input).toEqual([[[]]]);
|
||||||
|
|||||||
@@ -129,14 +129,17 @@ describe("Toggling Dropdown", () => {
|
|||||||
expect(Select.vm.stateClasses['vs--open']).toEqual(true);
|
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({
|
const Select = selectWithProps({
|
||||||
noDrop: true,
|
noDrop: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
Select.vm.toggleDropdown({ target: Select.vm.$refs.search });
|
Select.vm.toggleDropdown({ target: Select.vm.$refs.search });
|
||||||
expect(Select.vm.open).toEqual(true);
|
expect(Select.vm.open).toEqual(true);
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
expect(Select.contains('.vs__dropdown-menu')).toBeFalsy();
|
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();
|
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");
|
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 spy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
const Select = selectWithProps({
|
const Select = selectWithProps({
|
||||||
options: [{}]
|
options: [{}]
|
||||||
});
|
});
|
||||||
|
|
||||||
Select.vm.open = true;
|
Select.vm.open = true;
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(
|
expect(spy).toHaveBeenCalledWith(
|
||||||
'[vue-select warn]: Label key "option.label" does not exist in options object {}.' +
|
'[vue-select warn]: Label key "option.label" does not exist in options object {}.' +
|
||||||
"\nhttps://vue-select.org/api/props.html#getoptionlabel"
|
"\nhttps://vue-select.org/api/props.html#getoptionlabel"
|
||||||
@@ -39,4 +41,41 @@ describe("Labels", () => {
|
|||||||
Select.vm.$data._value = "one";
|
Select.vm.$data._value = "one";
|
||||||
expect(Select.vm.searchPlaceholder).not.toBeDefined();
|
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;
|
const getOptionKey = Select.props.getOptionKey.default;
|
||||||
|
|
||||||
it('can serialize strings to a key', () => {
|
it('can serialize strings to a key', () => {
|
||||||
expect(getOptionKey('vue')).toBe('"vue"');
|
expect(getOptionKey('vue')).toBe('vue');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can serialize integers to a key', () => {
|
it('can serialize integers to a key', () => {
|
||||||
expect(getOptionKey(1)).toBe('1');
|
expect(getOptionKey(1)).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can serialize objects to a key', () => {
|
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 VueSelect from "../../src/components/Select";
|
||||||
|
import { mountDefault } from '../helpers';
|
||||||
|
|
||||||
describe("Reset on options change", () => {
|
describe("Reset on options change", () => {
|
||||||
it("should not reset the selected value by default when the options property changes", () => {
|
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"]);
|
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, {
|
const Select = shallowMount(VueSelect, {
|
||||||
propsData: { resetOnOptionsChange: true, options: ["one"] }
|
propsData: { resetOnOptionsChange: true, options: ["one"] }
|
||||||
});
|
});
|
||||||
@@ -21,15 +96,46 @@ describe("Reset on options change", () => {
|
|||||||
Select.vm.$data._value = 'one';
|
Select.vm.$data._value = 'one';
|
||||||
|
|
||||||
Select.setProps({options: ["four", "five", "six"]});
|
Select.setProps({options: ["four", "five", "six"]});
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
expect(Select.vm.selectedValue).toEqual([]);
|
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, {
|
const Select = shallowMount(VueSelect, {
|
||||||
propsData: { value: "one", options: [], reduce(option) { return option.value } }
|
propsData: { value: "one", options: [], reduce(option) { return option.value } }
|
||||||
});
|
});
|
||||||
|
|
||||||
Select.setProps({options: [{ label: "oneLabel", value: "one" }]});
|
Select.setProps({options: [{ label: "oneLabel", value: "one" }]});
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
expect(Select.vm.selectedValue).toEqual([{ label: "oneLabel", value: "one" }]);
|
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 optionToChangeTo = { id: 1, label: "Foo" };
|
||||||
const Select = shallowMount(VueSelect, {
|
const Select = shallowMount(VueSelect, {
|
||||||
propsData: {
|
propsData: {
|
||||||
@@ -222,7 +222,37 @@ describe("When reduce prop is defined", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Select.setProps({ value: optionToChangeTo.id });
|
Select.setProps({ value: optionToChangeTo.id });
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
expect(Select.vm.selectedValue).toEqual([optionToChangeTo]);
|
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";
|
import { selectWithProps } from "../helpers";
|
||||||
|
|
||||||
describe("Selectable prop", () => {
|
describe("Selectable prop", () => {
|
||||||
it("should select selectable option if clicked", () => {
|
it("should select selectable option if clicked", async () => {
|
||||||
const Select = selectWithProps({
|
const Select = selectWithProps({
|
||||||
options: ["one", "two", "three"],
|
options: ["one", "two", "three"],
|
||||||
selectable: (option) => option == "one"
|
selectable: (option) => option === "one"
|
||||||
});
|
});
|
||||||
|
|
||||||
Select.vm.$data.open = true;
|
Select.vm.$data.open = true;
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
Select.find(".vs__dropdown-menu li:first-child").trigger("mousedown");
|
Select.find(".vs__dropdown-menu li:first-child").trigger("mousedown");
|
||||||
|
|
||||||
|
await Select.vm.$nextTick();
|
||||||
expect(Select.vm.selectedValue).toEqual(["one"]);
|
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({
|
const Select = selectWithProps({
|
||||||
options: ["one", "two", "three"],
|
options: ["one", "two", "three"],
|
||||||
selectable: (option) => option == "one"
|
selectable: (option) => option === "one"
|
||||||
});
|
});
|
||||||
|
|
||||||
Select.vm.$data.open = true;
|
Select.vm.$data.open = true;
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
Select.find(".vs__dropdown-menu li:last-child").trigger("mousedown");
|
Select.find(".vs__dropdown-menu li:last-child").trigger("mousedown");
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
expect(Select.vm.selectedValue).toEqual([]);
|
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({
|
const Select = selectWithProps({
|
||||||
options: ["one", "two", "three"],
|
options: ["one", "two", "three"],
|
||||||
selectable: (option) => option !== "two"
|
selectable: (option) => option !== "two"
|
||||||
@@ -33,12 +39,12 @@ describe("Selectable prop", () => {
|
|||||||
|
|
||||||
Select.vm.typeAheadPointer = 1;
|
Select.vm.typeAheadPointer = 1;
|
||||||
|
|
||||||
Select.find({ ref: "search" }).trigger("keyup.down");
|
Select.find({ ref: "search" }).trigger("keydown.down");
|
||||||
|
|
||||||
expect(Select.vm.typeAheadPointer).toEqual(2);
|
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({
|
const Select = selectWithProps({
|
||||||
options: ["one", "two", "three"],
|
options: ["one", "two", "three"],
|
||||||
selectable: (option) => option !== "two"
|
selectable: (option) => option !== "two"
|
||||||
@@ -46,7 +52,7 @@ describe("Selectable prop", () => {
|
|||||||
|
|
||||||
Select.vm.typeAheadPointer = 2;
|
Select.vm.typeAheadPointer = 2;
|
||||||
|
|
||||||
Select.find({ ref: "search" }).trigger("keyup.up");
|
Select.find({ ref: "search" }).trigger("keydown.up");
|
||||||
|
|
||||||
expect(Select.vm.typeAheadPointer).toEqual(0);
|
expect(Select.vm.typeAheadPointer).toEqual(0);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -193,7 +193,17 @@ describe("VS - Selecting Values", () => {
|
|||||||
value: [{ label: "foo", value: "bar" }]
|
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", () => {
|
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', () => {
|
describe('Slot: selected-option', () => {
|
||||||
const Select = mountDefault(
|
it('receives an option object to the selected-option slot', () => {
|
||||||
{value: 'one'},
|
const Select = mountDefault(
|
||||||
{
|
{value: 'one'},
|
||||||
scopedSlots: {
|
{
|
||||||
'selected-option': `<span slot="selected-option" slot-scope="option">{{ option.label }}</span>`,
|
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', () => {
|
it('receives an option object to the option slot in the dropdown menu',
|
||||||
const Select = mountDefault(
|
async () => {
|
||||||
{value: 'one'},
|
const Select = mountDefault(
|
||||||
{
|
{value: 'one'},
|
||||||
scopedSlots: {
|
{
|
||||||
'option': `<span slot="option" slot-scope="option">{{ option.label }}</span>`,
|
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;
|
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 { searchSubmit, selectWithProps } from "../helpers";
|
||||||
|
import Select from '../../src/components/Select';
|
||||||
|
|
||||||
describe("When Tagging Is Enabled", () => {
|
describe("When Tagging Is Enabled", () => {
|
||||||
|
|
||||||
it("can determine if a given option string already exists", () => {
|
it("can determine if a given option string already exists", () => {
|
||||||
const Select = selectWithProps({ taggable: true, options: ["one", "two"] });
|
const Select = selectWithProps({ taggable: true, options: ["one", "two"] });
|
||||||
expect(Select.vm.optionExists("one")).toEqual(true);
|
expect(Select.vm.optionExists("one")).toEqual(true);
|
||||||
@@ -13,8 +15,8 @@ describe("When Tagging Is Enabled", () => {
|
|||||||
options: [{ label: "one" }, { label: "two" }]
|
options: [{ label: "one" }, { label: "two" }]
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Select.vm.optionExists("one")).toEqual(true);
|
expect(Select.vm.optionExists({label: "one"})).toEqual(true);
|
||||||
expect(Select.vm.optionExists("three")).toEqual(false);
|
expect(Select.vm.optionExists({label: "three"})).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can determine if a given option object already exists when using custom labels", () => {
|
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"
|
label: "foo"
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Select.vm.optionExists("one")).toEqual(true);
|
const createOption = (text) => Select.vm.createOption(text);
|
||||||
expect(Select.vm.optionExists("three")).toEqual(false);
|
|
||||||
|
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", () => {
|
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"]);
|
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", () => {
|
it("should add a freshly created option/tag to the options list when pushTags is true and filterable is false", () => {
|
||||||
const Select = selectWithProps({
|
const Select = selectWithProps({
|
||||||
filterable: false,
|
filterable: false,
|
||||||
@@ -139,7 +157,7 @@ describe("When Tagging Is Enabled", () => {
|
|||||||
expect(Select.vm.selectedValue).toEqual([two]);
|
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" };
|
let two = { label: "two" };
|
||||||
const Select = selectWithProps({
|
const Select = selectWithProps({
|
||||||
taggable: true,
|
taggable: true,
|
||||||
@@ -147,12 +165,13 @@ describe("When Tagging Is Enabled", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Select.vm.search = "two";
|
Select.vm.search = "two";
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
searchSubmit(Select);
|
searchSubmit(Select);
|
||||||
expect(Select.vm.selectedValue).toEqual([two]);
|
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" };
|
let two = { label: "two" };
|
||||||
const Select = selectWithProps({
|
const Select = selectWithProps({
|
||||||
taggable: true,
|
taggable: true,
|
||||||
@@ -161,6 +180,7 @@ describe("When Tagging Is Enabled", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Select.vm.search = "two";
|
Select.vm.search = "two";
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
searchSubmit(Select);
|
searchSubmit(Select);
|
||||||
expect(Select.vm.selectedValue).toEqual([two]);
|
expect(Select.vm.selectedValue).toEqual([two]);
|
||||||
@@ -212,9 +232,11 @@ describe("When Tagging Is Enabled", () => {
|
|||||||
multiple: true,
|
multiple: true,
|
||||||
options: [{ label: "two" }]
|
options: [{ label: "two" }]
|
||||||
});
|
});
|
||||||
|
const spy = jest.spyOn(Select.vm, 'select');
|
||||||
|
|
||||||
searchSubmit(Select, "one");
|
searchSubmit(Select, "one");
|
||||||
expect(Select.vm.selectedValue).toEqual([{ label: "one" }]);
|
expect(Select.vm.selectedValue).toEqual([{ label: "one" }]);
|
||||||
|
expect(spy).lastCalledWith({label: 'one'});
|
||||||
expect(Select.vm.search).toEqual("");
|
expect(Select.vm.search).toEqual("");
|
||||||
|
|
||||||
searchSubmit(Select, "one");
|
searchSubmit(Select, "one");
|
||||||
|
|||||||
@@ -120,11 +120,12 @@ describe("Moving the Typeahead Pointer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Measuring pixel distances", () => {
|
describe("Measuring pixel distances", () => {
|
||||||
it("should calculate pointerHeight as the offsetHeight of the pointer element if it exists", () => {
|
it("should calculate pointerHeight as the offsetHeight of the pointer element if it exists", async () => {
|
||||||
const Select = mountDefault();
|
const Select = mountDefault();
|
||||||
|
|
||||||
// Drop down must be open for $refs to exist
|
// Drop down must be open for $refs to exist
|
||||||
Select.vm.open = true;
|
Select.vm.open = true;
|
||||||
|
await Select.vm.$nextTick();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Since JSDom doesn't render layouts, set the offsetHeight explicitly
|
* 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