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_js: 3,
panel_css: 1 panel_css: 1
}; };
const form = document.createElement('form'); const form = document.getElementById('fiddle-form') || document.createElement('form');
form.innerHTML = '';
const node = document.createElement('textarea'); const node = document.createElement('textarea');
form.method = 'post'; form.method = 'post';
@ -197,6 +198,9 @@
node.value = data[name].toString(); node.value = data[name].toString();
form.appendChild(node.cloneNode()); form.appendChild(node.cloneNode());
} }
form.setAttribute('id', 'fiddle-form');
form.style.display = 'none';
document.body.appendChild(form);
form.submit(); 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. 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 ```html
<template> <template>
<el-select v-model="value8" filterable placeholder="Select"> <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 Vue from 'vue';
import entry from './app'; import entry from './app';

View File

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

View File

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

View File

@ -6,6 +6,7 @@
return { return {
inputWidth: 0, inputWidth: 0,
options: [], options: [],
props: {},
visible: false, visible: false,
activeValue: [], activeValue: [],
value: [], value: [],
@ -34,6 +35,20 @@
cache: false, cache: false,
get() { get() {
const activeValue = this.activeValue; 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 loadActiveOptions = (options, activeOptions = []) => {
const level = activeOptions.length; const level = activeOptions.length;
@ -48,6 +63,7 @@
return activeOptions; return activeOptions;
}; };
formatOptions(this.options);
return loadActiveOptions(this.options); return loadActiveOptions(this.options);
} }
} }
@ -66,7 +82,11 @@
const len = this.activeOptions.length; const len = this.activeOptions.length;
this.activeValue.splice(menuIndex, len, item.value); this.activeValue.splice(menuIndex, len, item.value);
this.activeOptions.splice(menuIndex + 1, len, item.children); 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 = {}; let menuStyle = {};
if (isFlat) { if (isFlat) {
menuStyle.width = this.inputWidth + 'px'; menuStyle.minWidth = this.inputWidth + 'px';
} }
return ( return (

View File

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

View File

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

View File

@ -13,6 +13,7 @@ describe('Cascader', () => {
ref="cascader" ref="cascader"
placeholder="请选择" placeholder="请选择"
:options="options" :options="options"
clearable
v-model="selectedOptions" v-model="selectedOptions"
></el-cascader> ></el-cascader>
`, `,
@ -456,6 +457,7 @@ describe('Cascader', () => {
placeholder="请选择" placeholder="请选择"
:options="options" :options="options"
filterable filterable
:debounce="0"
v-model="selectedOptions" v-model="selectedOptions"
></el-cascader> ></el-cascader>
`, `,
@ -507,7 +509,7 @@ describe('Cascader', () => {
const item1 = menuElm.querySelector('.el-cascader-menu__item'); const item1 = menuElm.querySelector('.el-cascader-menu__item');
expect(menuElm.children.length).to.be.equal(1); 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(); done();
item1.click(); item1.click();
@ -521,4 +523,106 @@ describe('Cascader', () => {
}, 500); }, 500);
}, 300); }, 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);
});
}); });