mirror of https://github.com/ElemeFE/element
Cascader: update (#2845)
* Cascader: update * Cascader: add tests * Cascader: move flatOptions and add debouncepull/2909/head
parent
3f64571fcc
commit
0643460b28
|
@ -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
|
@ -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
|
@ -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';
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
},
|
||||
{
|
||||
"path": "/cascader",
|
||||
"title": "Cascader 级联选择"
|
||||
"title": "Cascader 级联选择器"
|
||||
},
|
||||
{
|
||||
"path": "/switch",
|
||||
|
|
|
@ -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) {
|
||||
options: {
|
||||
deep: true,
|
||||
handler(value) {
|
||||
if (!this.menu) {
|
||||
this.initMenu();
|
||||
}
|
||||
this.flatOptions = this.flattenOptions(this.options);
|
||||
this.menu.options = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
showMenu() {
|
||||
if (!this.menu) {
|
||||
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.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>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue