mirror of https://github.com/ElemeFE/element
add cascader
parent
0a408949a4
commit
1a16fbb735
|
@ -58,5 +58,6 @@
|
|||
"scrollbar": "./packages/scrollbar/index.js",
|
||||
"carousel-item": "./packages/carousel-item/index.js",
|
||||
"collapse": "./packages/collapse/index.js",
|
||||
"collapse-item": "./packages/collapse-item/index.js"
|
||||
"collapse-item": "./packages/collapse-item/index.js",
|
||||
"cascader": "./packages/cascader/index.js"
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
## cascader
|
|
@ -0,0 +1,109 @@
|
|||
<script>
|
||||
module.exports = {
|
||||
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: [],
|
||||
selectedOptions2: ['jiangsu', 'nanjing', 'zhonghuamen'],
|
||||
selectedOptions3: [],
|
||||
selectedOptions4: [],
|
||||
selectedOptions5: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
## 级联选择
|
||||
|
||||
需要从一组相关联的数据集合进行选择,例如省市区,公司层级,事物分类等。
|
||||
|
||||
从一个较大的数据集合中进行选择时,用多级分类进行分隔,方便选择。
|
||||
|
||||
### 基本使用
|
||||
|
||||
:::demo
|
||||
```html
|
||||
<el-cascader
|
||||
:options="options"
|
||||
v-model="selectedOptions"
|
||||
></el-cascader>
|
||||
```
|
||||
:::
|
||||
|
||||
### 默认值
|
||||
|
||||
:::demo
|
||||
```html
|
||||
<el-cascader
|
||||
:options="options"
|
||||
v-model="selectedOptions2"
|
||||
></el-cascader>
|
||||
```
|
||||
:::
|
||||
|
||||
### 移入展开
|
||||
|
||||
:::demo
|
||||
```html
|
||||
<el-cascader
|
||||
:options="options"
|
||||
v-model="selectedOptions3"
|
||||
expand-trigger="hover"
|
||||
></el-cascader>
|
||||
```
|
||||
:::
|
||||
|
||||
### 选择即改变
|
||||
|
||||
:::demo
|
||||
```html
|
||||
<el-cascader
|
||||
:options="options"
|
||||
v-model="selectedOptions4"
|
||||
change-on-select
|
||||
></el-cascader>
|
||||
```
|
||||
:::
|
||||
|
||||
### 可搜索
|
||||
|
||||
:::demo
|
||||
```html
|
||||
<el-cascader
|
||||
:options="options"
|
||||
v-model="selectedOptions5"
|
||||
show-search
|
||||
></el-cascader>
|
||||
```
|
||||
:::
|
|
@ -215,6 +215,10 @@
|
|||
{
|
||||
"path": "/collapse",
|
||||
"title": "Collapse 折叠面板"
|
||||
},
|
||||
{
|
||||
"path": "/cascader",
|
||||
"title": "Cascader 级联选择"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -437,6 +441,10 @@
|
|||
{
|
||||
"path": "/collapse",
|
||||
"title": "Collapse"
|
||||
},
|
||||
{
|
||||
"path": "/cascader",
|
||||
"title": "Cascader"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
var cooking = require('cooking');
|
||||
var path = require('path');
|
||||
var config = require('../../build/config');
|
||||
|
||||
cooking.set({
|
||||
entry: {
|
||||
index: path.join(__dirname, 'index.js')
|
||||
},
|
||||
dist: path.join(__dirname, 'lib'),
|
||||
template: false,
|
||||
format: 'umd',
|
||||
moduleName: 'ElCascader',
|
||||
extends: ['vue2'],
|
||||
alias: config.alias,
|
||||
externals: { vue: config.vue }
|
||||
});
|
||||
|
||||
module.exports = cooking.resolve();
|
|
@ -0,0 +1,8 @@
|
|||
import Cascader from './src/main';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Cascader.install = function(Vue) {
|
||||
Vue.component(Cascader.name, Cascader);
|
||||
};
|
||||
|
||||
export default Cascader;
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "element-cascader",
|
||||
"version": "0.0.0",
|
||||
"description": "A cascader component for Vue.js.",
|
||||
"keywords": [
|
||||
"element",
|
||||
"vue",
|
||||
"component"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"repository": "https://github.com/ElemeFE/element/tree/master/packages/cascader",
|
||||
"author": "elemefe",
|
||||
"license": "MIT",
|
||||
"dependencies": {}
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
<template>
|
||||
<span
|
||||
class="el-cascader"
|
||||
:class="{
|
||||
'is-opened': menuVisible
|
||||
}"
|
||||
@click="menuVisible = !menuVisible"
|
||||
@mouseenter="inputHover = true"
|
||||
@mouseleave="inputHover = false"
|
||||
ref="reference"
|
||||
v-clickoutside="handleClickoutside"
|
||||
>
|
||||
<el-input
|
||||
:readonly="!showSearch"
|
||||
:placeholder="placeholder"
|
||||
v-model="inputValue"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
<template slot="icon">
|
||||
<i
|
||||
key="1"
|
||||
v-if="inputHover && displayValue !== ''"
|
||||
class="el-input__icon el-icon-circle-close"
|
||||
@click="clearValue"
|
||||
></i>
|
||||
<i
|
||||
key="2"
|
||||
v-else
|
||||
class="el-input__icon el-icon-caret-bottom"
|
||||
:class="{ 'is-reverse': menuVisible }"
|
||||
></i>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="el-cascader__label" v-show="inputValue === ''">{{displayValue}}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import ElCascaderMenu from './menu';
|
||||
import ElInput from 'element-ui/packages/input';
|
||||
import Popper from 'element-ui/src/utils/vue-popper';
|
||||
import Clickoutside from 'element-ui/src/utils/clickoutside';
|
||||
|
||||
const popperMixin = {
|
||||
props: {
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom-start'
|
||||
},
|
||||
appendToBody: Popper.props.appendToBody,
|
||||
offset: Popper.props.offset,
|
||||
boundariesPadding: Popper.props.boundariesPadding,
|
||||
popperOptions: Popper.props.popperOptions
|
||||
},
|
||||
methods: Popper.methods,
|
||||
data: Popper.data,
|
||||
beforeDestroy: Popper.beforeDestroy
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'ElCascader',
|
||||
|
||||
directives: { Clickoutside },
|
||||
|
||||
mixins: [popperMixin],
|
||||
|
||||
components: {
|
||||
ElInput
|
||||
},
|
||||
|
||||
props: {
|
||||
options: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
placeholder: String,
|
||||
disabled: Boolean,
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
changeOnSelect: Boolean,
|
||||
popperClass: String,
|
||||
expandTrigger: {
|
||||
type: String,
|
||||
default: 'click'
|
||||
},
|
||||
showSearch: Boolean
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentValue: this.value,
|
||||
displayValue: this.value.join('/'),
|
||||
menuVisible: false,
|
||||
inputHover: false,
|
||||
inputValue: '',
|
||||
flatOptions: this.showSearch && this.flattenOptions(this.options)
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
menuVisible(value) {
|
||||
value ? this.showMenu() : this.hideMenu();
|
||||
},
|
||||
value(value) {
|
||||
this.currentValue = value;
|
||||
},
|
||||
currentValue(value) {
|
||||
this.displayValue = value.join('/');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
showMenu() {
|
||||
if (!this.menu) {
|
||||
this.menu = new Vue(ElCascaderMenu).$mount(document.createElement('div'));
|
||||
this.menu.options = this.options;
|
||||
this.menu.expandTrigger = this.expandTrigger;
|
||||
this.menu.changeOnSelect = this.changeOnSelect;
|
||||
this.popperElm = this.menu.$el;
|
||||
}
|
||||
|
||||
this.menu.value = this.currentValue.slice(0);
|
||||
this.menu.visible = true;
|
||||
this.menu.$on('change', this.handlePick);
|
||||
this.updatePopper();
|
||||
},
|
||||
hideMenu() {
|
||||
this.menu.visible = false;
|
||||
this.inputValue = '';
|
||||
},
|
||||
handlePick(value, close = true) {
|
||||
this.currentValue = value;
|
||||
this.$emit('input', value);
|
||||
if (close) {
|
||||
this.menuVisible = false;
|
||||
}
|
||||
},
|
||||
handleInputChange(value) {
|
||||
const flatOptions = this.flatOptions;
|
||||
|
||||
if (!value) {
|
||||
this.menu.options = this.options;
|
||||
return;
|
||||
}
|
||||
|
||||
let filteredFlatOptions = flatOptions.filter(optionsStack => {
|
||||
return optionsStack.some(option => option.label.indexOf(value) > -1);
|
||||
});
|
||||
|
||||
if (filteredFlatOptions.length > 0) {
|
||||
this.menu.options = filteredFlatOptions.map(optionStack => {
|
||||
return {
|
||||
__IS__FLAT__OPTIONS: true,
|
||||
value: optionStack.map(item => item.value),
|
||||
label: this.renderRenderFilteredOption(value, optionStack)
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return [{ label: 'notFoundContent', value: 'ANT_CASCADER_NOT_FOUND', disabled: true }];
|
||||
}
|
||||
},
|
||||
renderRenderFilteredOption(inputValue, optionsStack) {
|
||||
return optionsStack.map(({ label }, index) => {
|
||||
const node = label.indexOf(inputValue) > -1 ? this.highlightKeyword(label, inputValue) : label;
|
||||
return index === 0 ? node : [' / ', node];
|
||||
});
|
||||
},
|
||||
highlightKeyword(label, keyword) {
|
||||
const h = this._c;
|
||||
return label.split(keyword)
|
||||
.map((node, index) => index === 0 ? node : [
|
||||
h('span', { class: { 'el-cascader-menu__item__keyword': true }}, [this._v(keyword)]),
|
||||
node
|
||||
]);
|
||||
},
|
||||
flattenOptions(options, ancestor = []) {
|
||||
let flatOptions = [];
|
||||
options.forEach((option) => {
|
||||
const optionsStack = ancestor.concat(option);
|
||||
if (!option.children) {
|
||||
flatOptions.push(optionsStack);
|
||||
}
|
||||
if (option.children) {
|
||||
flatOptions = flatOptions.concat(this.flattenOptions(option.children, optionsStack));
|
||||
}
|
||||
});
|
||||
return flatOptions;
|
||||
},
|
||||
clearValue(ev) {
|
||||
ev.stopPropagation();
|
||||
this.handlePick([], true);
|
||||
},
|
||||
handleClickoutside() {
|
||||
this.menuVisible = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,133 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'ElCascaderMenu',
|
||||
|
||||
data() {
|
||||
return {
|
||||
options: [],
|
||||
visible: false,
|
||||
activeValue: [],
|
||||
value: [],
|
||||
expandTrigger: 'click',
|
||||
changeOnSelect: false
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
visible(value) {
|
||||
if (value) {
|
||||
this.activeValue = this.value;
|
||||
}
|
||||
},
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
this.activeValue = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
activeOptions: {
|
||||
cache: false,
|
||||
get() {
|
||||
const activeValue = this.activeValue;
|
||||
let options = this.options;
|
||||
|
||||
const loadActiveOptions = (options, activeOptions = []) => {
|
||||
const level = activeOptions.length;
|
||||
activeOptions[level] = options;
|
||||
let active = activeValue[level];
|
||||
if (active) {
|
||||
options = options.filter(option => option.value === active)[0];
|
||||
if (options && options.children) {
|
||||
loadActiveOptions(options.children, activeOptions);
|
||||
}
|
||||
}
|
||||
return activeOptions;
|
||||
};
|
||||
|
||||
const result = loadActiveOptions(options);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectItem(item, menuIndex) {
|
||||
const len = this.activeOptions.length;
|
||||
const closeMenu = !item.children;
|
||||
|
||||
if (item.__IS__FLAT__OPTIONS) {
|
||||
this.activeValue.splice(menuIndex, len, ...item.value);
|
||||
} else {
|
||||
this.activeValue.splice(menuIndex, len, item.value);
|
||||
}
|
||||
|
||||
if (this.changeOnSelect) {
|
||||
this.$emit('change', this.activeValue, closeMenu);
|
||||
}
|
||||
},
|
||||
expandItem(item, menuIndex) {
|
||||
const len = this.activeOptions.length;
|
||||
if (item.children) {
|
||||
this.activeValue.splice(menuIndex, len, item.value);
|
||||
this.activeOptions.splice(menuIndex + 1, len, item.children);
|
||||
}
|
||||
},
|
||||
handleItemClick(item, menuIndex) {
|
||||
this.expandItem(item, menuIndex);
|
||||
this.selectItem(item, menuIndex);
|
||||
|
||||
if (!item.children && !this.changeOnSelect) {
|
||||
this.$emit('change', this.activeValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render(h) {
|
||||
const {
|
||||
activeValue,
|
||||
activeOptions,
|
||||
visible,
|
||||
expandTrigger
|
||||
} = this;
|
||||
|
||||
const menus = this._l(activeOptions, (menu, index) => {
|
||||
const items = this._l(menu, item => {
|
||||
const events = {
|
||||
on: {}
|
||||
};
|
||||
|
||||
if (expandTrigger === 'click' || !item.children) {
|
||||
events.on['click'] = () => { this.handleItemClick(item, index); };
|
||||
} else {
|
||||
events.on['mouseenter'] = () => { this.expandItem(item, index); };
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
class={{
|
||||
'el-cascader-menu__item': true,
|
||||
'el-cascader-menu__item--extensible': item.children,
|
||||
'is-active': item.value === activeValue[index]
|
||||
}}
|
||||
{...events}
|
||||
>
|
||||
{item.label}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
return <ul class="el-cascader-menu">{items}</ul>;
|
||||
});
|
||||
return (
|
||||
<transition name="el-zoom-in-top">
|
||||
<div class="el-cascader-menus" v-show={visible}>
|
||||
{menus}
|
||||
</div>
|
||||
</transition>
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -3,42 +3,136 @@
|
|||
@import "./common/var.css";
|
||||
/*@import "./core/dropdown.css";*/
|
||||
|
||||
@component-namespace element {
|
||||
@component-namespace el {
|
||||
|
||||
@b cascader {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
|
||||
@e dropdown {
|
||||
background-color: var(--cascader-menu-fill);
|
||||
border: var(--cascader-menu-border);
|
||||
border-radius: var(--cascader-menu-radius);
|
||||
box-shadow: var(--cascader-menu-submenu-shadow);
|
||||
margin-top: 5px;
|
||||
max-height: var(--cascader-height);
|
||||
.el-input,
|
||||
.el-input__inner {
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.el-input__icon {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.el-icon-caret-bottom {
|
||||
transition: transform .3s;
|
||||
|
||||
@when reverse {
|
||||
transform: rotateZ(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
@e label {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: var(--input-height);
|
||||
line-height: @height;
|
||||
padding: 0 15px 0 10px;
|
||||
color: var(--input-color);
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@e wrap {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@b cascader-menus {
|
||||
white-space: nowrap;
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
margin: 5px 0;
|
||||
z-index: 1001;
|
||||
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;
|
||||
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;
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
@e menu {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
@e item {
|
||||
font-size: var(--select-font-size);
|
||||
padding: 8px 30px 8px 10px;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--select-option-color);
|
||||
height: var(--select-option-height);
|
||||
line-height: 1.5;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
border-left: var(--cascader-menu-border);
|
||||
content: " ";
|
||||
height: var(--cascader-height);
|
||||
left: 0;
|
||||
position: absolute;
|
||||
@e keyword {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
@m extensible {
|
||||
&:after {
|
||||
font-family: 'element-icons';
|
||||
content: "\e602";
|
||||
font-size: 12px;
|
||||
transform: scale(0.8);
|
||||
color: rgb(191, 203, 217);
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@when disabled {
|
||||
color: var(--select-option-disabled-color);
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
}
|
||||
|
||||
@when active {
|
||||
color: var(--color-white);
|
||||
background-color: var(--select-option-selected);
|
||||
|
||||
&.hover {
|
||||
background-color: var(--select-option-selected-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--select-option-hover-background);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: var(--color-white);
|
||||
background-color: var(--select-option-selected);
|
||||
|
||||
&.hover {
|
||||
background-color: var(--select-option-selected-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,3 +44,4 @@
|
|||
@import "./carousel.css";
|
||||
@import "./carousel-item.css";
|
||||
@import "./collapse.css";
|
||||
@import "./cascader.css";
|
||||
|
|
|
@ -60,6 +60,7 @@ import Scrollbar from '../packages/scrollbar';
|
|||
import CarouselItem from '../packages/carousel-item';
|
||||
import Collapse from '../packages/collapse';
|
||||
import CollapseItem from '../packages/collapse-item';
|
||||
import Cascader from '../packages/cascader';
|
||||
import locale from 'element-ui/src/locale';
|
||||
|
||||
const components = [
|
||||
|
@ -118,7 +119,8 @@ const components = [
|
|||
Scrollbar,
|
||||
CarouselItem,
|
||||
Collapse,
|
||||
CollapseItem
|
||||
CollapseItem,
|
||||
Cascader
|
||||
];
|
||||
|
||||
const install = function(Vue, opts = {}) {
|
||||
|
@ -211,5 +213,6 @@ module.exports = {
|
|||
Scrollbar,
|
||||
CarouselItem,
|
||||
Collapse,
|
||||
CollapseItem
|
||||
CollapseItem,
|
||||
Cascader
|
||||
};
|
||||
|
|
|
@ -83,6 +83,7 @@ export default {
|
|||
this.$slots.reference[0]) {
|
||||
reference = this.referenceElm = this.$slots.reference[0].elm;
|
||||
}
|
||||
|
||||
if (!popper || !reference) return;
|
||||
if (this.visibleArrow) this.appendArrow(popper);
|
||||
if (this.appendToBody) document.body.appendChild(this.popperElm);
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { createTest, destroyVM } from '../util';
|
||||
import Cascader from 'packages/cascader';
|
||||
|
||||
describe('Cascader', () => {
|
||||
let vm;
|
||||
afterEach(() => {
|
||||
destroyVM(vm);
|
||||
});
|
||||
|
||||
it('create', () => {
|
||||
vm = createTest(Cascader, true);
|
||||
expect(vm.$el).to.exist;
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue