Merge pull request #3157 from SergioCrisostomo/refactor-select
Refactor select, fix tab/keyboard navigation
This commit is contained in:
commit
4ce11f84c9
10 changed files with 1088 additions and 779 deletions
|
@ -1,6 +1,8 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const entry = require('./locale');
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
module.exports = {
|
||||
|
@ -40,7 +42,7 @@ module.exports = {
|
|||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
new UglifyJsPlugin({
|
||||
parallel: true,
|
||||
sourceMap: true,
|
||||
})
|
||||
|
|
|
@ -3,6 +3,7 @@ const webpack = require('webpack');
|
|||
const merge = require('webpack-merge');
|
||||
const webpackBaseConfig = require('./webpack.base.config.js');
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
|
@ -32,7 +33,7 @@ module.exports = merge(webpackBaseConfig, {
|
|||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
new UglifyJsPlugin({
|
||||
parallel: true,
|
||||
sourceMap: true,
|
||||
}),
|
||||
|
|
101
package-lock.json
generated
101
package-lock.json
generated
|
@ -13387,6 +13387,30 @@
|
|||
"integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-es": {
|
||||
"version": "3.3.9",
|
||||
"resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz",
|
||||
"integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"commander": "2.13.0",
|
||||
"source-map": "0.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz",
|
||||
"integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "2.8.29",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
|
||||
|
@ -13405,14 +13429,54 @@
|
|||
"dev": true
|
||||
},
|
||||
"uglifyjs-webpack-plugin": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz",
|
||||
"integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.3.tgz",
|
||||
"integrity": "sha512-as/50351uuJGiQbhVvE510SCqM/YOWghCzIFJeEOu5oVE0QOZ3/vu2QcnVvu0Lz+vNd0rKsiCFAlbcw0i/YH2w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"source-map": "0.5.7",
|
||||
"uglify-js": "2.8.29",
|
||||
"webpack-sources": "1.1.0"
|
||||
"cacache": "10.0.4",
|
||||
"find-cache-dir": "1.0.0",
|
||||
"schema-utils": "0.4.5",
|
||||
"serialize-javascript": "1.4.0",
|
||||
"source-map": "0.6.1",
|
||||
"uglify-es": "3.3.9",
|
||||
"webpack-sources": "1.1.0",
|
||||
"worker-farm": "1.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.2.1.tgz",
|
||||
"integrity": "sha1-KKarxJOiq+D7TIUHrK7bQ/pVBnE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "1.1.0",
|
||||
"fast-json-stable-stringify": "2.0.0",
|
||||
"json-schema-traverse": "0.3.1"
|
||||
}
|
||||
},
|
||||
"ajv-keywords": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.1.0.tgz",
|
||||
"integrity": "sha1-rCsnk5xUPpXSwG5/f1wnvkqlQ74=",
|
||||
"dev": true
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz",
|
||||
"integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "6.2.1",
|
||||
"ajv-keywords": "3.1.0"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ultron": {
|
||||
|
@ -13850,6 +13914,11 @@
|
|||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"v-click-outside-x": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/v-click-outside-x/-/v-click-outside-x-2.4.0.tgz",
|
||||
"integrity": "sha512-xAouyFRaMDD074px+J3PoxhU5nGQsIj8yxXRYyFd0/PRhY1ob3F55L9mGsd35KzXkQteajEhap6SClaMB0MENg=="
|
||||
},
|
||||
"v8flags": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz",
|
||||
|
@ -14807,6 +14876,17 @@
|
|||
"integrity": "sha1-mTcqXJmb8t8WCvwNdL7U9HlIzSI=",
|
||||
"dev": true
|
||||
},
|
||||
"uglifyjs-webpack-plugin": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz",
|
||||
"integrity": "sha1-uVH0q7a9YX5m9j64kUmOORdj4wk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"source-map": "0.5.7",
|
||||
"uglify-js": "2.8.29",
|
||||
"webpack-sources": "1.1.0"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
|
||||
|
@ -15446,6 +15526,15 @@
|
|||
"integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
|
||||
"dev": true
|
||||
},
|
||||
"worker-farm": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz",
|
||||
"integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"errno": "0.1.7"
|
||||
}
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
|
||||
|
|
|
@ -47,7 +47,8 @@
|
|||
"js-calendar": "^1.2.3",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"popper.js": "^1.14.1",
|
||||
"tinycolor2": "^1.4.1"
|
||||
"tinycolor2": "^1.4.1",
|
||||
"v-click-outside-x": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^2.5.2"
|
||||
|
@ -99,6 +100,7 @@
|
|||
"sinon": "^4.4.2",
|
||||
"sinon-chai": "^3.0.0",
|
||||
"style-loader": "^0.20.2",
|
||||
"uglifyjs-webpack-plugin": "^1.2.3",
|
||||
"url-loader": "^1.0.1",
|
||||
"vue": "^2.5.16",
|
||||
"vue-hot-reload-api": "^2.3.0",
|
||||
|
|
29
src/components/select/functional-options.vue
Normal file
29
src/components/select/functional-options.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
|
||||
<script>
|
||||
const returnArrayFn = () => [];
|
||||
|
||||
export default {
|
||||
props: {
|
||||
options: {
|
||||
type: Array,
|
||||
default: returnArrayFn
|
||||
},
|
||||
slotOptions: {
|
||||
type: Array,
|
||||
default: returnArrayFn
|
||||
},
|
||||
slotUpdateHook: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
},
|
||||
functional: true,
|
||||
render(h, {props, parent}){
|
||||
// to detect changes in the $slot children/options we do this hack
|
||||
// so we can trigger the parents computed properties and have everything reactive
|
||||
// although $slot.default is not
|
||||
if (props.slotOptions !== parent.$slots.default) props.slotUpdateHook();
|
||||
return props.options;
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -1,5 +1,10 @@
|
|||
<template>
|
||||
<li :class="classes" @click.stop="select" @mouseout.stop="blur" v-show="!hidden"><slot>{{ showLabel }}</slot></li>
|
||||
<li
|
||||
:class="classes"
|
||||
@click.stop="select"
|
||||
@mousedown.prevent
|
||||
@touchstart.prevent
|
||||
><slot>{{ showLabel }}</slot></li>
|
||||
</template>
|
||||
<script>
|
||||
import Emitter from '../../mixins/emitter';
|
||||
|
@ -22,15 +27,19 @@
|
|||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isFocused: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selected: false,
|
||||
index: 0, // for up and down to focus
|
||||
isFocus: false,
|
||||
hidden: false, // for search
|
||||
searchLabel: '', // the value is slot,only for search
|
||||
searchLabel: '', // the slot value (textContent)
|
||||
autoComplete: false
|
||||
};
|
||||
},
|
||||
|
@ -41,53 +50,34 @@
|
|||
{
|
||||
[`${prefixCls}-disabled`]: this.disabled,
|
||||
[`${prefixCls}-selected`]: this.selected && !this.autoComplete,
|
||||
[`${prefixCls}-focus`]: this.isFocus
|
||||
[`${prefixCls}-focus`]: this.isFocused
|
||||
}
|
||||
];
|
||||
},
|
||||
showLabel () {
|
||||
return (this.label) ? this.label : this.value;
|
||||
},
|
||||
optionLabel(){
|
||||
return (this.$el && this.$el.textContent) || this.label;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select () {
|
||||
if (this.disabled) {
|
||||
return false;
|
||||
}
|
||||
if (this.disabled) return false;
|
||||
|
||||
this.dispatch('iSelect', 'on-select-selected', this.value);
|
||||
this.dispatch('iSelect', 'on-select-selected', {
|
||||
value: this.value,
|
||||
label: this.optionLabel,
|
||||
});
|
||||
this.$emit('on-select-selected', {
|
||||
value: this.value,
|
||||
label: this.optionLabel,
|
||||
});
|
||||
},
|
||||
blur () {
|
||||
this.isFocus = false;
|
||||
},
|
||||
queryChange (val) {
|
||||
const parsedQuery = val.replace(/(\^|\(|\)|\[|\]|\$|\*|\+|\.|\?|\\|\{|\}|\|)/g, '\\$1');
|
||||
this.hidden = !new RegExp(parsedQuery, 'i').test(this.searchLabel);
|
||||
},
|
||||
// 在使用函数防抖后,设置 key 后,不更新组件了,导致SearchLabel 不更新 #1865
|
||||
updateSearchLabel () {
|
||||
this.searchLabel = this.$el.textContent;
|
||||
},
|
||||
onSelectClose(){
|
||||
this.isFocus = false;
|
||||
},
|
||||
onQueryChange(val){
|
||||
this.queryChange(val);
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateSearchLabel();
|
||||
this.dispatch('iSelect', 'append');
|
||||
this.$on('on-select-close', this.onSelectClose);
|
||||
this.$on('on-query-change',this.onQueryChange);
|
||||
|
||||
const Select = findComponentUpward(this, 'iSelect');
|
||||
if (Select) this.autoComplete = Select.autoComplete;
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.dispatch('iSelect', 'remove');
|
||||
this.$off('on-select-close', this.onSelectClose);
|
||||
this.$off('on-query-change',this.onQueryChange);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
196
src/components/select/select-head.vue
Normal file
196
src/components/select/select-head.vue
Normal file
|
@ -0,0 +1,196 @@
|
|||
<template>
|
||||
<div @click="onHeaderClick">
|
||||
<div class="ivu-tag ivu-tag-checked" v-for="item in selectedMultiple">
|
||||
<span class="ivu-tag-text">{{ item.label }}</span>
|
||||
<Icon type="ios-close-empty" @click.native.stop="removeTag(item)"></Icon>
|
||||
</div>
|
||||
<span
|
||||
:class="singleDisplayClasses"
|
||||
v-show="singleDisplayValue"
|
||||
>{{ singleDisplayValue }}</span>
|
||||
<input
|
||||
:id="inputElementId"
|
||||
type="text"
|
||||
v-if="filterable"
|
||||
v-model="query"
|
||||
:disabled="disabled"
|
||||
:class="[prefixCls + '-input']"
|
||||
:placeholder="showPlaceholder ? localePlaceholder : ''"
|
||||
:style="inputStyle"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
@keydown="resetInputState"
|
||||
@keydown.delete="handleInputDelete"
|
||||
@focus="onInputFocus"
|
||||
@blur="onInputFocus"
|
||||
|
||||
ref="input">
|
||||
<Icon type="ios-close" :class="[prefixCls + '-arrow']" v-if="resetSelect" @click.native.stop="resetSelect"></Icon>
|
||||
<Icon type="arrow-down-b" :class="[prefixCls + '-arrow']" v-if="!resetSelect && !remote && !disabled"></Icon>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Icon from '../icon';
|
||||
import Emitter from '../../mixins/emitter';
|
||||
import Locale from '../../mixins/locale';
|
||||
|
||||
const prefixCls = 'ivu-select';
|
||||
|
||||
export default {
|
||||
name: 'iSelectHead',
|
||||
mixins: [ Emitter, Locale ],
|
||||
components: { Icon },
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
remote: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
initialLabel: {
|
||||
type: String,
|
||||
},
|
||||
values: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
clearable: {
|
||||
type: [Function, Boolean],
|
||||
default: false,
|
||||
},
|
||||
inputElementId: {
|
||||
type: String
|
||||
},
|
||||
placeholder: {
|
||||
type: String
|
||||
},
|
||||
queryProp: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
query: '',
|
||||
inputLength: 20,
|
||||
remoteInitialLabel: this.initialLabel,
|
||||
preventRemoteCall: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
singleDisplayClasses(){
|
||||
const {filterable, multiple, showPlaceholder} = this;
|
||||
return [{
|
||||
[prefixCls + '-placeholder']: showPlaceholder && !filterable,
|
||||
[prefixCls + '-selected-value']: !showPlaceholder && !multiple && !filterable,
|
||||
}];
|
||||
},
|
||||
singleDisplayValue(){
|
||||
if ((this.multiple && this.values.length > 0) || this.filterable) return '';
|
||||
return `${this.selectedSingle}` || this.localePlaceholder;
|
||||
},
|
||||
showPlaceholder () {
|
||||
let status = false;
|
||||
if (!this.multiple) {
|
||||
const value = this.values[0];
|
||||
if (typeof value === 'undefined' || String(value).trim() === ''){
|
||||
status = !this.remoteInitialLabel;
|
||||
}
|
||||
} else {
|
||||
if (!this.values.length > 0) {
|
||||
status = true;
|
||||
}
|
||||
}
|
||||
return status;
|
||||
},
|
||||
resetSelect(){
|
||||
return !this.showPlaceholder && this.clearable;
|
||||
},
|
||||
inputStyle () {
|
||||
let style = {};
|
||||
|
||||
if (this.multiple) {
|
||||
if (this.showPlaceholder) {
|
||||
style.width = '100%';
|
||||
} else {
|
||||
style.width = `${this.inputLength}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
},
|
||||
localePlaceholder () {
|
||||
if (this.placeholder === undefined) {
|
||||
return this.t('i.select.placeholder');
|
||||
} else {
|
||||
return this.placeholder;
|
||||
}
|
||||
},
|
||||
selectedSingle(){
|
||||
const selected = this.values[0];
|
||||
return selected ? selected.label : (this.remoteInitialLabel || '');
|
||||
},
|
||||
selectedMultiple(){
|
||||
return this.multiple ? this.values : [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInputFocus(e){
|
||||
this.$emit(e.type === 'focus' ? 'on-input-focus' : 'on-input-blur');
|
||||
},
|
||||
removeTag (value) {
|
||||
if (this.disabled) return false;
|
||||
this.dispatch('iSelect', 'on-select-selected', value);
|
||||
},
|
||||
resetInputState () {
|
||||
this.inputLength = this.$refs.input.value.length * 12 + 20;
|
||||
},
|
||||
handleInputDelete () {
|
||||
if (this.multiple && this.selectedMultiple.length && this.query === '') {
|
||||
this.removeTag(this.selectedMultiple[this.selectedMultiple.length - 1]);
|
||||
}
|
||||
},
|
||||
onHeaderClick(e){
|
||||
if (this.filterable && e.target === this.$el){
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
values ([value]) {
|
||||
if (!this.filterable) return;
|
||||
this.preventRemoteCall = true;
|
||||
if (this.multiple){
|
||||
this.query = '';
|
||||
this.preventRemoteCall = false; // this should be after the query change setter above
|
||||
return;
|
||||
}
|
||||
// #982
|
||||
if (typeof value === 'undefined' || value === '' || value === null) this.query = '';
|
||||
else this.query = value.label;
|
||||
},
|
||||
query (val) {
|
||||
if (this.preventRemoteCall) {
|
||||
this.preventRemoteCall = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('on-query-change', val);
|
||||
},
|
||||
queryProp(query){
|
||||
if (query !== this.query) this.query = query;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
File diff suppressed because it is too large
Load diff
|
@ -25,23 +25,14 @@
|
|||
border: 1px solid @border-color-base;
|
||||
transition: all @transition-time @ease-in-out;
|
||||
|
||||
.@{select-prefix-cls}-arrow:nth-of-type(1) {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover, &-focused {
|
||||
.hover();
|
||||
.@{select-prefix-cls}-arrow:nth-of-type(1) {
|
||||
.@{select-prefix-cls}-arrow {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-show-clear &-selection:hover .@{select-prefix-cls}-arrow:nth-of-type(2){
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
.inner-arrow();
|
||||
}
|
||||
|
@ -51,14 +42,9 @@
|
|||
.active();
|
||||
}
|
||||
|
||||
.@{select-prefix-cls}-arrow:nth-of-type(2) {
|
||||
.@{select-prefix-cls}-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
&:focus{
|
||||
outline: 0;
|
||||
.@{select-prefix-cls}-selection{
|
||||
.active();
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,7 +52,7 @@
|
|||
.@{select-prefix-cls}-selection {
|
||||
.disabled();
|
||||
|
||||
.@{select-prefix-cls}-arrow:nth-of-type(1) {
|
||||
.@{select-prefix-cls}-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -74,7 +60,7 @@
|
|||
border-color: @border-color-base;
|
||||
box-shadow: none;
|
||||
|
||||
.@{select-prefix-cls}-arrow:nth-of-type(2) {
|
||||
.@{select-prefix-cls}-arrow {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,34 +26,40 @@ describe('Select.vue', () => {
|
|||
const placeholderSpan = vm.$el.querySelector('.ivu-select-placeholder');
|
||||
expect(placeholderSpan.textContent).to.equal(placeholder);
|
||||
expect(placeholderSpan.style.display).to.not.equal('none');
|
||||
|
||||
expect(vm.$children[0].showPlaceholder).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a Select component and take a pre-selected value', done => {
|
||||
vm = createVue({
|
||||
template: `
|
||||
<Select :value="2">
|
||||
<Option v-for="item in options" :value="item.value" :key="item.value">{{ item.label }}</Option>
|
||||
</Select>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
options: [{value: 1, label: 'Foo'}, {value: 2, label: 'Bar'}]
|
||||
};
|
||||
}
|
||||
});
|
||||
vm.$nextTick(() => {
|
||||
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
|
||||
expect(selectedValueSpan.textContent).to.equal('Bar');
|
||||
expect(selectedValueSpan.style.display).to.not.equal('none');
|
||||
expect(vm.$children[0].selectedSingle).to.equal('Bar');
|
||||
expect(vm.$children[0].model).to.equal(2);
|
||||
done();
|
||||
template: `
|
||||
<Select :value="value">
|
||||
<Option v-for="item in options" :value="item.value" :key="item.value">{{ item.label }}</Option>
|
||||
</Select>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
value: 2,
|
||||
options: [{value: 1, label: 'Foo'}, {value: 2, label: 'Bar'}]
|
||||
};
|
||||
}
|
||||
});
|
||||
waitForIt(
|
||||
() => {
|
||||
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
|
||||
return selectedValueSpan.textContent === 'Bar';
|
||||
},
|
||||
() => {
|
||||
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
|
||||
const {label, value} = vm.$children[0].values[0];
|
||||
|
||||
expect(selectedValueSpan.textContent).to.equal('Bar');
|
||||
expect(selectedValueSpan.style.display).to.not.equal('none');
|
||||
expect(label).to.equal('Bar');
|
||||
expect(value).to.equal(2);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept normal characters', done => {
|
||||
|
@ -112,13 +118,20 @@ describe('Select.vue', () => {
|
|||
};
|
||||
}
|
||||
});
|
||||
vm.$nextTick(() => {
|
||||
const placeholderSpan = vm.$el.querySelector('.ivu-select-placeholder');
|
||||
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
|
||||
expect(placeholderSpan.style.display).to.equal('none');
|
||||
expect(selectedValueSpan.style.display).to.not.equal('none');
|
||||
done();
|
||||
});
|
||||
waitForIt(
|
||||
() => {
|
||||
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
|
||||
return selectedValueSpan.textContent === 'Bar';
|
||||
},
|
||||
() => {
|
||||
const placeholderSpan = vm.$el.querySelector('.ivu-select-placeholder');
|
||||
const selectedValueSpan = vm.$el.querySelector('.ivu-select-selected-value');
|
||||
expect(placeholderSpan).to.equal(null);
|
||||
expect(!!selectedValueSpan.style.display).to.not.equal('none');
|
||||
expect(selectedValueSpan.textContent).to.equal('Bar');
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should set different classes for different sizes', done => {
|
||||
|
@ -158,12 +171,10 @@ describe('Select.vue', () => {
|
|||
}
|
||||
});
|
||||
const condition = function() {
|
||||
return vm.$children[0].options.length > 0;
|
||||
const componentOptions = vm.$children[0].flatOptions;
|
||||
return componentOptions && componentOptions.length > 0;
|
||||
};
|
||||
const callback = function() {
|
||||
if (vm.$children[0].options == 0) return setTimeout(waitForIt.bind(null, done), 50);
|
||||
expect(JSON.stringify(vm.$children[0].options)).to.equal(JSON.stringify(laterOptions));
|
||||
|
||||
const renderedOptions = vm.$el.querySelectorAll('.ivu-select-dropdown-list li');
|
||||
expect(renderedOptions.length).to.equal(laterOptions.length);
|
||||
|
||||
|
@ -177,79 +188,234 @@ describe('Select.vue', () => {
|
|||
});
|
||||
|
||||
describe('Behavior tests', () => {
|
||||
it('should create different and independent instances', done => {
|
||||
const options = [
|
||||
{value: 'beijing', label: 'Beijing'},
|
||||
{value: 'stockholm', label: 'Stockholm'},
|
||||
{value: 'lisboa', label: 'Lisboa'}
|
||||
];
|
||||
it('should create different and independent instances', done => {
|
||||
const options = [
|
||||
{value: 'beijing', label: 'Beijing'},
|
||||
{value: 'stockholm', label: 'Stockholm'},
|
||||
{value: 'lisboa', label: 'Lisboa'}
|
||||
];
|
||||
|
||||
vm = createVue({
|
||||
template: `
|
||||
<div>
|
||||
<i-select v-model="modelA" multiple style="width:260px">
|
||||
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
|
||||
</i-select>
|
||||
<i-select v-model="modelB" multiple style="width:260px">
|
||||
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
|
||||
</i-select>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
cityList: [],
|
||||
modelA: [],
|
||||
modelB: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => (this.cityList = options), 200);
|
||||
}
|
||||
vm = createVue({
|
||||
template: `
|
||||
<div>
|
||||
<i-select v-model="modelA" multiple style="width:260px">
|
||||
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
|
||||
</i-select>
|
||||
<i-select v-model="modelB" multiple style="width:260px">
|
||||
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
|
||||
</i-select>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
cityList: [],
|
||||
modelA: [],
|
||||
modelB: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => (this.cityList = options), 200);
|
||||
}
|
||||
});
|
||||
const [SelectA, SelectB] = vm.$children;
|
||||
SelectA.toggleMenu(null, true);
|
||||
SelectB.toggleMenu(null, true);
|
||||
|
||||
new Promise(resolve => {
|
||||
const condition = function() {
|
||||
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
|
||||
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
|
||||
return optionsA.length > 0 && optionsB.length > 0;
|
||||
};
|
||||
waitForIt(condition, resolve);
|
||||
})
|
||||
.then(() => {
|
||||
// click in A options
|
||||
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
|
||||
optionsA[0].click();
|
||||
return promissedTick(SelectA);
|
||||
})
|
||||
.then(() => {
|
||||
expect(SelectA.value[0]).to.equal(options[0].value);
|
||||
expect(SelectA.value.length).to.equal(1);
|
||||
expect(SelectB.value.length).to.equal(0);
|
||||
|
||||
// click in B options
|
||||
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
|
||||
optionsB[1].click();
|
||||
optionsB[2].click();
|
||||
return promissedTick(SelectB);
|
||||
})
|
||||
.then(() => {
|
||||
// lets check the values!
|
||||
const getSelections = component => {
|
||||
const tags = component.$el.querySelectorAll('.ivu-select-selection .ivu-tag');
|
||||
return [...tags].map(el => el.textContent.trim()).join(',');
|
||||
};
|
||||
const selectAValue = getSelections(SelectA);
|
||||
const selectBValue = getSelections(SelectB);
|
||||
|
||||
expect(selectAValue).to.equal(options[0].label);
|
||||
expect(selectBValue).to.equal(options.slice(1, 3).map(obj => obj.label.trim()).join(','));
|
||||
|
||||
done();
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
done(false);
|
||||
});
|
||||
});
|
||||
const [SelectA, SelectB] = vm.$children;
|
||||
SelectA.toggleMenu();
|
||||
SelectB.toggleMenu();
|
||||
|
||||
new Promise(resolve => {
|
||||
const condition = function() {
|
||||
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
|
||||
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
|
||||
return optionsA.length > 0 && optionsB.length > 0;
|
||||
};
|
||||
waitForIt(condition, resolve);
|
||||
})
|
||||
.then(() => {
|
||||
// click in A options
|
||||
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
|
||||
optionsA[0].click();
|
||||
return promissedTick(SelectA);
|
||||
})
|
||||
.then(() => {
|
||||
expect(SelectA.value[0]).to.equal(options[0].value);
|
||||
expect(SelectA.value.length).to.equal(1);
|
||||
expect(SelectB.value.length).to.equal(0);
|
||||
it('should create update model with value, and label when asked', done => {
|
||||
const options = [
|
||||
{value: 'beijing', label: 'Beijing'},
|
||||
{value: 'stockholm', label: 'Stockholm'},
|
||||
{value: 'lisboa', label: 'Lisboa'}
|
||||
];
|
||||
let onChangeValueA, onChangeValueB;
|
||||
|
||||
// click in B options
|
||||
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
|
||||
optionsB[1].click();
|
||||
optionsB[2].click();
|
||||
return promissedTick(SelectB);
|
||||
})
|
||||
.then(() => {
|
||||
// lets check the values!
|
||||
const getSelections = component => {
|
||||
const tags = component.$el.querySelectorAll('.ivu-select-selection .ivu-tag');
|
||||
return [...tags].map(el => el.textContent.trim()).join(',');
|
||||
};
|
||||
const selectAValue = getSelections(SelectA);
|
||||
const selectBValue = getSelections(SelectB);
|
||||
|
||||
expect(selectAValue).to.equal(options[0].label);
|
||||
expect(selectBValue).to.equal(options.slice(1, 3).map(obj => obj.label.trim()).join(','));
|
||||
vm = createVue({
|
||||
template: `
|
||||
<div>
|
||||
<i-select v-model="modelA" style="width:260px" @on-change="onChangeA">
|
||||
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
|
||||
</i-select>
|
||||
<i-select v-model="modelB" label-in-value style="width:260px" @on-change="onChangeB">
|
||||
<i-option v-for="item in cityList" :value="item.value" :key="item.value">{{ item.label }}</i-option>
|
||||
</i-select>
|
||||
</div>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
cityList: options,
|
||||
modelA: [],
|
||||
modelB: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onChangeA(val){
|
||||
onChangeValueA = val;
|
||||
},
|
||||
onChangeB(val){
|
||||
onChangeValueB = val;
|
||||
}
|
||||
}
|
||||
});
|
||||
const [SelectA, SelectB] = vm.$children;
|
||||
SelectA.toggleMenu(null, true);
|
||||
SelectB.toggleMenu(null, true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
new Promise(resolve => {
|
||||
const condition = function() {
|
||||
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
|
||||
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
|
||||
return optionsA.length > 0 && optionsB.length > 0;
|
||||
};
|
||||
waitForIt(condition, resolve);
|
||||
})
|
||||
.then(() => {
|
||||
// click in A options
|
||||
const optionsA = SelectA.$el.querySelectorAll('.ivu-select-item');
|
||||
optionsA[0].click();
|
||||
return promissedTick(SelectA);
|
||||
})
|
||||
.then(() => {
|
||||
expect(vm.modelA).to.equal(options[0].value);
|
||||
expect(onChangeValueA).to.equal(options[0].value);
|
||||
|
||||
// click in B options
|
||||
const optionsB = SelectB.$el.querySelectorAll('.ivu-select-item');
|
||||
optionsB[2].click();
|
||||
return promissedTick(SelectB);
|
||||
})
|
||||
.then(() => {
|
||||
expect(vm.modelB).to.equal(options[2].value);
|
||||
expect(JSON.stringify(onChangeValueB)).to.equal(JSON.stringify(options[2]));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Public API', () => {
|
||||
it('The "setQuery" method should behave as expected', (done) => {
|
||||
|
||||
const options = [
|
||||
{value: 'beijing', label: 'Beijing'},
|
||||
{value: 'stockholm', label: 'Stockholm'},
|
||||
{value: 'lisboa', label: 'Lisboa'}
|
||||
];
|
||||
|
||||
vm = createVue({
|
||||
template: `
|
||||
<Select v-model="value" filterable>
|
||||
<Option v-for="item in options" :value="item.value" :key="item.value">{{ item.label }}</Option>
|
||||
</Select>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
options: options
|
||||
};
|
||||
}
|
||||
});
|
||||
const [Select] = vm.$children;
|
||||
Select.setQuery('i');
|
||||
vm.$nextTick(() => {
|
||||
const query = 'i';
|
||||
const input = vm.$el.querySelector('.ivu-select-input');
|
||||
expect(input.value).to.equal(query);
|
||||
|
||||
const renderedOptions = [...vm.$el.querySelectorAll('.ivu-select-item')].map(el => el.textContent);
|
||||
const filteredOptions = options.filter(option => JSON.stringify(option).includes(query)).map(({label}) => label);
|
||||
expect(JSON.stringify(renderedOptions)).to.equal(JSON.stringify(filteredOptions));
|
||||
|
||||
// reset query
|
||||
// setQuery(null) should clear the select
|
||||
Select.setQuery(null);
|
||||
vm.$nextTick(() => {
|
||||
const input = vm.$el.querySelector('.ivu-select-input');
|
||||
expect(input.value).to.equal('');
|
||||
|
||||
const renderedOptions = [...vm.$el.querySelectorAll('.ivu-select-item')].map(el => el.textContent);
|
||||
expect(JSON.stringify(renderedOptions)).to.equal(JSON.stringify(options.map(({label}) => label)));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('The "clearSingleSelect" method should behave as expected', (done) => {
|
||||
|
||||
// clearSingleSelect
|
||||
const options = [
|
||||
{value: 'beijing', label: 'Beijing'},
|
||||
{value: 'stockholm', label: 'Stockholm'},
|
||||
{value: 'lisboa', label: 'Lisboa'}
|
||||
];
|
||||
const preSelected = 'lisboa';
|
||||
|
||||
vm = createVue({
|
||||
template: `
|
||||
<Select v-model="value" clearable>
|
||||
<Option v-for="item in options" :value="item.value" :key="item.value">{{ item.label }}</Option>
|
||||
</Select>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
value: preSelected,
|
||||
options: options
|
||||
};
|
||||
}
|
||||
});
|
||||
const [Select] = vm.$children;
|
||||
vm.$nextTick(() => {
|
||||
expect(Select.publicValue).to.equal(preSelected);
|
||||
Select.clearSingleSelect();
|
||||
expect(typeof Select.publicValue).to.equal('undefined');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance tests', () => {
|
||||
|
@ -278,7 +444,8 @@ describe('Select.vue', () => {
|
|||
}
|
||||
});
|
||||
const condition = function() {
|
||||
return vm.$children[0].options.length == manyLaterOptions.length;
|
||||
const componentOptions = vm.$children[0].flatOptions;
|
||||
return componentOptions && componentOptions.length === manyLaterOptions.length;
|
||||
};
|
||||
const callback = function() {
|
||||
const end = +new Date();
|
||||
|
|
Loading…
Add table
Reference in a new issue