mirror of
https://github.com/tenrok/vue-select.git
synced 2026-06-22 10:30:34 +03:00
Merge pull request #971 from sagalbot/fix-956-event-delegation
WIP Event delegation for #956
This commit is contained in:
+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,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,4 @@
|
|||||||
|
<template>
|
||||||
|
<!-- tag on 188/comma & 13/return -->
|
||||||
|
<v-select no-drop taggable multiple :select-on-key-codes="[188, 13]" />
|
||||||
|
</template>
|
||||||
@@ -123,6 +123,13 @@ module.exports = {
|
|||||||
['guide/loops', 'Using in Loops'],
|
['guide/loops', 'Using in Loops'],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Customizing',
|
||||||
|
collapsable: false,
|
||||||
|
children: [
|
||||||
|
['guide/keydown', 'Keydown Events'],
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'API',
|
title: 'API',
|
||||||
collapsable: false,
|
collapsable: false,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
+61
-22
@@ -79,6 +79,9 @@
|
|||||||
import ajax from '../mixins/ajax'
|
import ajax from '../mixins/ajax'
|
||||||
import childComponents from './childComponents';
|
import childComponents from './childComponents';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name VueSelect
|
||||||
|
*/
|
||||||
export default {
|
export default {
|
||||||
components: {...childComponents},
|
components: {...childComponents},
|
||||||
|
|
||||||
@@ -302,11 +305,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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -449,12 +453,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.
|
||||||
@@ -467,6 +481,21 @@
|
|||||||
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,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -474,6 +503,7 @@
|
|||||||
return {
|
return {
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -840,39 +870,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();
|
// delete
|
||||||
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.
|
||||||
@@ -944,10 +981,12 @@
|
|||||||
'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: {
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@ describe("Selectable prop", () => {
|
|||||||
it("should select selectable option if clicked", () => {
|
it("should select selectable option if clicked", () => {
|
||||||
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;
|
||||||
@@ -16,7 +16,7 @@ describe("Selectable prop", () => {
|
|||||||
it("should not select not selectable option if clicked", () => {
|
it("should not select not selectable option if clicked", () => {
|
||||||
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user