Cascader: update (#2845)

* Cascader: update

* Cascader: add tests

* Cascader: move flatOptions and add debounce
pull/2909/head
杨奕 2017-02-19 00:50:55 +08:00 committed by baiyaaaaa
parent 3f64571fcc
commit 0643460b28
11 changed files with 3271 additions and 666 deletions

View File

@ -185,7 +185,8 @@
panel_js: 3,
panel_css: 1
};
const form = document.createElement('form');
const form = document.getElementById('fiddle-form') || document.createElement('form');
form.innerHTML = '';
const node = document.createElement('textarea');
form.method = 'post';
@ -197,6 +198,9 @@
node.value = data[name].toString();
form.appendChild(node.cloneNode());
}
form.setAttribute('id', 'fiddle-form');
form.style.display = 'none';
document.body.appendChild(form);
form.submit();
}

File diff suppressed because it is too large Load Diff

View File

@ -462,7 +462,7 @@ Display options in groups.
You can filter options for your desired ones.
:::demo Adding `filterable` to `el-select` enables filtering. By default, Select will find all the options whose `label` attribute contains the input value. If you prefer other filtering strategies, you can pass the `filter-method`. `filter-method` is a `Function` that gets called when the input value changed, and its parameter is the current input value.
:::demo Adding `filterable` to `el-select` enables filtering. By default, Select will find all the options whose `label` attribute contains the input value. If you prefer other filtering strategies, you can pass the `filter-method`. `filter-method` is a `Function` that gets called when the input value changes, and its parameter is the current input value.
```html
<template>
<el-select v-model="value8" filterable placeholder="Select">

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
require('offline-plugin/runtime').install();
process.env.NODE_ENV === 'production' && require('offline-plugin/runtime').install();
import Vue from 'vue';
import entry from './app';

View File

@ -78,7 +78,7 @@
},
{
"path": "/cascader",
"title": "Cascader 级联选择"
"title": "Cascader 级联选择"
},
{
"path": "/switch",

View File

@ -17,9 +17,9 @@
<el-input
ref="input"
:readonly="!filterable"
:placeholder="displayValue ? undefined : placeholder"
:placeholder="currentLabels.length ? undefined : placeholder"
v-model="inputValue"
@change="handleInputChange"
@change="debouncedInputChange"
:validate-event="false"
:size="size"
:disabled="disabled"
@ -27,7 +27,7 @@
<template slot="icon">
<i
key="1"
v-if="inputHover && displayValue !== ''"
v-if="clearable && inputHover && currentLabels.length"
class="el-input__icon el-icon-circle-close el-cascader__clearIcon"
@click="clearValue"
></i>
@ -39,7 +39,17 @@
></i>
</template>
</el-input>
<span class="el-cascader__label" v-show="inputValue === ''">{{displayValue}}</span>
<span class="el-cascader__label" v-show="inputValue === ''">
<template v-if="showAllLevels">
<template v-for="(label, index) in currentLabels">
{{ label }}
<span v-if="index < currentLabels.length - 1"> / </span>
</template>
</template>
<template v-else>
{{ currentLabels[currentLabels.length - 1] }}
</template>
</span>
</span>
</template>
@ -51,6 +61,8 @@ import Popper from 'element-ui/src/utils/vue-popper';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import emitter from 'element-ui/src/mixins/emitter';
import Locale from 'element-ui/src/mixins/locale';
import { t } from 'element-ui/src/locale';
import debounce from 'throttle-debounce/debounce';
const popperMixin = {
props: {
@ -84,17 +96,33 @@ export default {
type: Array,
required: true
},
props: {
type: Object,
default() {
return {
children: 'children',
label: 'label',
value: 'value',
disabled: 'disabled'
};
}
},
value: {
type: Array,
default() {
return [];
}
},
placeholder: String,
placeholder: {
type: String,
default() {
return t('el.cascader.placeholder');
}
},
disabled: Boolean,
clearable: {
type: Boolean,
default: true
default: false
},
changeOnSelect: Boolean,
popperClass: String,
@ -103,20 +131,53 @@ export default {
default: 'click'
},
filterable: Boolean,
size: String
size: String,
showAllLevels: {
type: Boolean,
default: true
},
debounce: {
type: Number,
default: 300
}
},
data() {
return {
currentValue: this.value,
displayValue: this.value.join('/'),
menu: null,
debouncedInputChange() {},
menuVisible: false,
inputHover: false,
inputValue: '',
flatOptions: this.filterable && this.flattenOptions(this.options)
flatOptions: null
};
},
computed: {
labelKey() {
return this.props.label || 'label';
},
valueKey() {
return this.props.value || 'value';
},
childrenKey() {
return this.props.children || 'children';
},
currentLabels() {
let options = this.options;
let labels = [];
this.currentValue.forEach(value => {
const targetOption = options && options.filter(option => option[this.valueKey] === value)[0];
if (targetOption) {
labels.push(targetOption[this.labelKey]);
options = targetOption[this.childrenKey];
}
});
return labels;
}
},
watch: {
menuVisible(value) {
value ? this.showMenu() : this.hideMenu();
@ -125,29 +186,40 @@ export default {
this.currentValue = value;
},
currentValue(value) {
this.displayValue = value.join('/');
this.dispatch('ElFormItem', 'el.form.change', [value]);
},
options(value) {
this.menu.options = value;
options: {
deep: true,
handler(value) {
if (!this.menu) {
this.initMenu();
}
this.flatOptions = this.flattenOptions(this.options);
this.menu.options = value;
}
}
},
methods: {
initMenu() {
this.menu = new Vue(ElCascaderMenu).$mount();
this.menu.options = this.options;
this.menu.props = this.props;
this.menu.expandTrigger = this.expandTrigger;
this.menu.changeOnSelect = this.changeOnSelect;
this.menu.popperClass = this.popperClass;
this.popperElm = this.menu.$el;
this.menu.$on('pick', this.handlePick);
this.menu.$on('activeItemChange', this.handleActiveItemChange);
},
showMenu() {
if (!this.menu) {
this.menu = new Vue(ElCascaderMenu).$mount();
this.menu.options = this.options;
this.menu.expandTrigger = this.expandTrigger;
this.menu.changeOnSelect = this.changeOnSelect;
this.menu.popperClass = this.popperClass;
this.popperElm = this.menu.$el;
this.initMenu();
}
this.menu.value = this.currentValue.slice(0);
this.menu.visible = true;
this.menu.options = this.options;
this.menu.$on('pick', this.handlePick);
this.updatePopper();
this.$nextTick(_ => {
this.menu.inputWidth = this.$refs.input.$el.offsetWidth - 2;
@ -157,6 +229,12 @@ export default {
this.inputValue = '';
this.menu.visible = false;
},
handleActiveItemChange(value) {
this.$nextTick(_ => {
this.updatePopper();
});
this.$emit('active-item-change', value);
},
handlePick(value, close = true) {
this.currentValue = value;
this.$emit('input', value);
@ -176,14 +254,14 @@ export default {
}
let filteredFlatOptions = flatOptions.filter(optionsStack => {
return optionsStack.some(option => option.label.indexOf(value) > -1);
return optionsStack.some(option => new RegExp(value, 'i').test(option[this.labelKey]));
});
if (filteredFlatOptions.length > 0) {
filteredFlatOptions = filteredFlatOptions.map(optionStack => {
return {
__IS__FLAT__OPTIONS: true,
value: optionStack.map(item => item.value),
value: optionStack.map(item => item[this.valueKey]),
label: this.renderFilteredOptionLabel(value, optionStack)
};
});
@ -198,8 +276,11 @@ export default {
this.menu.options = filteredFlatOptions;
},
renderFilteredOptionLabel(inputValue, optionsStack) {
return optionsStack.map(({ label }, index) => {
const node = label.indexOf(inputValue) > -1 ? this.highlightKeyword(label, inputValue) : label;
return optionsStack.map((option, index) => {
const label = option[this.labelKey];
const keywordIndex = label.toLowerCase().indexOf(inputValue.toLowerCase());
const labelPart = label.slice(keywordIndex, inputValue.length + keywordIndex);
const node = keywordIndex > -1 ? this.highlightKeyword(label, labelPart) : label;
return index === 0 ? node : [' / ', node];
});
},
@ -215,10 +296,13 @@ export default {
let flatOptions = [];
options.forEach((option) => {
const optionsStack = ancestor.concat(option);
if (!option.children) {
if (!option[this.childrenKey]) {
flatOptions.push(optionsStack);
} else {
flatOptions = flatOptions.concat(this.flattenOptions(option.children, optionsStack));
if (this.changeOnSelect) {
flatOptions.push(optionsStack);
}
flatOptions = flatOptions.concat(this.flattenOptions(option[this.childrenKey], optionsStack));
}
});
return flatOptions;
@ -238,6 +322,16 @@ export default {
}
this.menuVisible = !this.menuVisible;
}
},
created() {
this.debouncedInputChange = debounce(this.debounce, value => {
this.handleInputChange(value);
});
},
mounted() {
this.flatOptions = this.flattenOptions(this.options);
}
};
</script>

View File

@ -6,6 +6,7 @@
return {
inputWidth: 0,
options: [],
props: {},
visible: false,
activeValue: [],
value: [],
@ -34,6 +35,20 @@
cache: false,
get() {
const activeValue = this.activeValue;
const configurableProps = ['label', 'value', 'children', 'disabled'];
const formatOptions = options => {
options.forEach(option => {
if (option.__IS__FLAT__OPTIONS) return;
configurableProps.forEach(prop => {
const value = option[this.props[prop] || prop];
if (value) option[prop] = value;
});
if (Array.isArray(option.children)) {
formatOptions(option.children);
}
});
};
const loadActiveOptions = (options, activeOptions = []) => {
const level = activeOptions.length;
@ -48,6 +63,7 @@
return activeOptions;
};
formatOptions(this.options);
return loadActiveOptions(this.options);
}
}
@ -66,7 +82,11 @@
const len = this.activeOptions.length;
this.activeValue.splice(menuIndex, len, item.value);
this.activeOptions.splice(menuIndex + 1, len, item.children);
if (this.changeOnSelect) this.$emit('pick', this.activeValue, false);
if (this.changeOnSelect) {
this.$emit('pick', this.activeValue, false);
} else {
this.$emit('activeItemChange', this.activeValue);
}
}
},
@ -116,7 +136,7 @@
});
let menuStyle = {};
if (isFlat) {
menuStyle.width = this.inputWidth + 'px';
menuStyle.minWidth = this.inputWidth + 'px';
}
return (

View File

@ -13,7 +13,7 @@
.el-input__inner {
cursor: pointer;
background-color: transparent;
z-index: 1;
z-index: var(--index-normal);
}
.el-input__icon {
@ -34,7 +34,7 @@
top: 0;
height: 100%;
line-height: 34px;
padding: 0 15px 0 10px;
padding: 0 25px 0 10px;
color: var(--input-color);
width: 100%;
white-space: nowrap;
@ -42,6 +42,11 @@
overflow: hidden;
box-sizing: border-box;
cursor: pointer;
font-size: 14px;
text-align: left;
span {
color: var(--color-light-silver);
}
}
@m large {
@ -65,24 +70,23 @@
background: #fff;
position: absolute;
margin: 5px 0;
z-index: 1001;
z-index: calc(var(--index-normal) + 1);
border: var(--select-dropdown-border);
border-radius: var(--border-radius-small);
overflow: hidden;
box-shadow: var(--select-dropdown-shadow);
}
@b cascader-menu {
display: inline-block;
vertical-align: top;
height: 180px;
height: 204px;
overflow: auto;
border-right: var(--select-dropdown-border);
background-color: var(--select-dropdown-background);
box-sizing: border-box;
margin: 0;
padding: 0;
min-width: 110px;
padding: 6px 0;
min-width: 160px;
&:last-child {
border-right: 0;
@ -102,13 +106,13 @@
cursor: pointer;
@e keyword {
color: var(--color-danger);
font-weight: bold;
}
@m extensible {
&:after {
font-family: 'element-icons';
content: "\e602";
content: "\e606";
font-size: 12px;
transform: scale(0.8);
color: rgb(191, 203, 217);
@ -132,7 +136,7 @@
color: var(--color-white);
background-color: var(--select-option-selected);
&.hover {
&:hover {
background-color: var(--select-option-selected-hover);
}
}

View File

@ -12,7 +12,7 @@ export default {
startTime: 'Hora de inicio',
endDate: 'Data de fim',
endTime: 'Hora de fim',
year: 'Ano',
year: '',
month1: 'Janeiro',
month2: 'Fevereiro',
month3: 'Março',

View File

@ -13,6 +13,7 @@ describe('Cascader', () => {
ref="cascader"
placeholder="请选择"
:options="options"
clearable
v-model="selectedOptions"
></el-cascader>
`,
@ -456,6 +457,7 @@ describe('Cascader', () => {
placeholder="请选择"
:options="options"
filterable
:debounce="0"
v-model="selectedOptions"
></el-cascader>
`,
@ -507,7 +509,7 @@ describe('Cascader', () => {
const item1 = menuElm.querySelector('.el-cascader-menu__item');
expect(menuElm.children.length).to.be.equal(1);
expect(menuElm.children[0].children.length).to.be.equal(1);
expect(menuElm.children[0].children.length).to.be.equal(3);
done();
item1.click();
@ -521,4 +523,106 @@ describe('Cascader', () => {
}, 500);
}, 300);
});
it('props', done => {
vm = createVue({
template: `
<el-cascader
ref="cascader"
:options="options"
:props="props"
v-model="selectedOptions"
></el-cascader>
`,
data() {
return {
options: [{
label: 'Zhejiang',
cities: [{
label: 'Hangzhou'
}, {
label: 'NingBo'
}]
}, {
label: 'Jiangsu',
cities: [{
label: 'Nanjing'
}]
}],
props: {
value: 'label',
children: 'cities'
},
selectedOptions: []
};
}
}, true);
vm.$el.click();
setTimeout(_ => {
expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
const menu = vm.$refs.cascader.menu;
const menuElm = menu.$el;
let items = menuElm.querySelectorAll('.el-cascader-menu__item');
expect(items.length).to.equal(2);
items[0].click();
setTimeout(_ => {
items = menuElm.querySelectorAll('.el-cascader-menu__item');
expect(items.length).to.equal(4);
expect(items[items.length - 1].innerText).to.equal('NingBo');
done();
}, 100);
}, 100);
});
it('show last level', done => {
vm = createVue({
template: `
<el-cascader
ref="cascader"
:options="options"
:show-all-levels="false"
v-model="selectedOptions"
></el-cascader>
`,
data() {
return {
options: [{
value: 'zhejiang',
label: 'Zhejiang',
children: [{
value: 'hangzhou',
label: 'Hangzhou',
children: [{
value: 'xihu',
label: 'West Lake'
}]
}, {
value: 'ningbo',
label: 'NingBo',
children: [{
value: 'jiangbei',
label: 'Jiang Bei'
}]
}]
}, {
value: 'jiangsu',
label: 'Jiangsu',
children: [{
value: 'nanjing',
label: 'Nanjing',
children: [{
value: 'zhonghuamen',
label: 'Zhong Hua Men'
}]
}]
}],
selectedOptions: ['zhejiang', 'ningbo', 'jiangbei']
};
}
}, true);
setTimeout(_ => {
const span = vm.$el.querySelector('.el-cascader__label');
expect(span.innerText).to.equal('Jiang Bei');
done();
}, 100);
});
});