Cascader: refactor and add multiple mode. (#15611)

pull/15776/head
Simona 2019-05-29 15:04:06 +08:00 committed by luckyCao
parent b245929242
commit bdaae8108e
77 changed files with 7116 additions and 3710 deletions

View File

@ -76,5 +76,6 @@
"calendar": "./packages/calendar/index.js",
"backtop": "./packages/backtop/index.js",
"infiniteScroll": "./packages/infiniteScroll/index.js",
"page-header": "./packages/page-header/index.js"
"page-header": "./packages/page-header/index.js",
"cascader-panel": "./packages/cascader-panel/index.js"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
import CascaderPanel from './src/cascader-panel';
/* istanbul ignore next */
CascaderPanel.install = function(Vue) {
Vue.component(CascaderPanel.name, CascaderPanel);
};
export default CascaderPanel;

View File

@ -0,0 +1,138 @@
<script>
import ElScrollbar from 'element-ui/packages/scrollbar';
import CascaderNode from './cascader-node.vue';
import Locale from 'element-ui/src/mixins/locale';
import { generateId } from 'element-ui/src/utils/util';
export default {
name: 'ElCascaderMenu',
mixins: [Locale],
inject: ['panel'],
components: {
ElScrollbar,
CascaderNode
},
props: {
nodes: {
type: Array,
required: true
},
index: Number
},
data() {
return {
activeNode: null,
hoverTimer: null,
id: generateId()
};
},
computed: {
isEmpty() {
return !this.nodes.length;
},
menuId() {
return `cascader-menu-${this.id}-${this.index}`;
}
},
methods: {
handleExpand(e) {
this.activeNode = e.target;
},
handleMouseMove(e) {
const { activeNode, hoverTimer } = this;
const { hoverZone } = this.$refs;
if (!activeNode || !hoverZone) return;
if (activeNode.contains(e.target)) {
clearTimeout(hoverTimer);
const { left } = this.$el.getBoundingClientRect();
const startX = e.clientX - left;
const { offsetWidth, offsetHeight } = this.$el;
const top = activeNode.offsetTop;
const bottom = top + activeNode.offsetHeight;
hoverZone.innerHTML = `
<path style="pointer-events: auto;" fill="transparent" d="M${startX} ${top} L${offsetWidth} 0 V${top} Z" />
<path style="pointer-events: auto;" fill="transparent" d="M${startX} ${bottom} L${offsetWidth} ${offsetHeight} V${bottom} Z" />
`;
} else if (!hoverTimer) {
this.hoverTimer = setTimeout(this.clearHoverZone, this.panel.config.hoverThreshold);
}
},
clearHoverZone() {
const { hoverZone } = this.$refs;
if (!hoverZone) return;
hoverZone.innerHTML = '';
},
renderEmptyText(h) {
return (
<div class="el-cascader-menu__empty-text">{ this.t('el.cascader.noData') }</div>
);
},
renderNodeList(h) {
const { menuId } = this;
const { isHoverMenu } = this.panel;
const events = { on: {} };
if (isHoverMenu) {
events.on.expand = this.handleExpand;
}
const nodes = this.nodes.map((node, index) => {
const { hasChildren } = node;
return (
<cascader-node
key={ node.uid }
node={ node }
node-id={ `${menuId}-${index}` }
aria-haspopup={ hasChildren }
aria-owns = { hasChildren ? menuId : null }
{ ...events }></cascader-node>
);
});
return [
...nodes,
isHoverMenu ? <svg ref='hoverZone' class='el-cascader-menu__hover-zone'></svg> : null
];
}
},
render(h) {
const { isEmpty, menuId } = this;
const events = { nativeOn: {} };
// optimize hover to expand experience (#8010)
if (this.panel.isHoverMenu) {
events.nativeOn.mousemove = this.handleMouseMove;
// events.nativeOn.mouseleave = this.clearHoverZone;
}
return (
<el-scrollbar
tag="ul"
role="menu"
id={ menuId }
class="el-cascader-menu"
wrap-class="el-cascader-menu__wrap"
view-class={{
'el-cascader-menu__list': true,
'is-empty': isEmpty
}}
{ ...events }>
{ isEmpty ? this.renderEmptyText(h) : this.renderNodeList(h) }
</el-scrollbar>
);
}
};
</script>

View File

@ -0,0 +1,246 @@
<script>
import ElCheckbox from 'element-ui/packages/checkbox';
import ElRadio from 'element-ui/packages/radio';
import { isEqual } from 'element-ui/src/utils/util';
const stopPropagation = e => e.stopPropagation();
export default {
inject: ['panel'],
components: {
ElCheckbox,
ElRadio
},
props: {
node: {
required: true
},
nodeId: String
},
computed: {
config() {
return this.panel.config;
},
isLeaf() {
return this.node.isLeaf;
},
isDisabled() {
return this.node.isDisabled;
},
checkedValue() {
return this.panel.checkedValue;
},
isChecked() {
return this.node.isSameNode(this.checkedValue);
},
inActivePath() {
return this.isInPath(this.panel.activePath);
},
inCheckedPath() {
if (!this.config.checkStrictly) return false;
return this.panel.checkedNodePaths
.some(checkedPath => this.isInPath(checkedPath));
},
value() {
return this.node.getValueByOption();
}
},
methods: {
handleExpand() {
const { panel, node, isDisabled, config } = this;
const { multiple, checkStrictly } = config;
if (!checkStrictly && isDisabled || node.loading) return;
if (config.lazy && !node.loaded) {
panel.lazyLoad(node, () => {
// do not use cached leaf value here, invoke this.isLeaf to get new value.
const { isLeaf } = this;
if (!isLeaf) this.handleExpand();
if (multiple) {
// if leaf sync checked state, else clear checked state
const checked = isLeaf ? node.checked : false;
this.handleMultiCheckChange(checked);
}
});
} else {
panel.handleExpand(node);
}
},
handleCheckChange() {
const { panel, value } = this;
panel.handleCheckChange(value);
},
handleMultiCheckChange(checked) {
this.node.doCheck(checked);
this.panel.calculateMultiCheckedValue();
},
isInPath(pathNodes) {
const { node } = this;
const selectedPathNode = pathNodes[node.level - 1] || {};
return selectedPathNode.uid === node.uid;
},
renderPrefix(h) {
const { isLeaf, isChecked, config } = this;
const { checkStrictly, multiple } = config;
if (multiple) {
return this.renderCheckbox(h);
} else if (checkStrictly) {
return this.renderRadio(h);
} else if (isLeaf && isChecked) {
return this.renderCheckIcon(h);
}
return null;
},
renderPostfix(h) {
const { node, isLeaf } = this;
if (node.loading) {
return this.renderLoadingIcon(h);
} else if (!isLeaf) {
return this.renderExpandIcon(h);
}
return null;
},
renderCheckbox(h) {
const { node, config, isDisabled } = this;
const events = {
on: { change: this.handleMultiCheckChange },
nativeOn: {}
};
if (config.checkStrictly) { // when every node is selectable, click event should not trigger expand event.
events.nativeOn.click = stopPropagation;
}
return (
<el-checkbox
value={ node.checked }
indeterminate={ node.indeterminate }
disabled={ isDisabled }
{ ...events }
></el-checkbox>
);
},
renderRadio(h) {
let { checkedValue, value, isDisabled } = this;
// to keep same reference if value cause radio's checked state is calculated by reference comparision;
if (isEqual(value, checkedValue)) {
value = checkedValue;
}
return (
<el-radio
value={ checkedValue }
label={ value }
disabled={ isDisabled }
onChange={ this.handleCheckChange }
nativeOnClick={ stopPropagation }>
{/* add an empty element to avoid render label */}
<span></span>
</el-radio>
);
},
renderCheckIcon(h) {
return (
<i class="el-icon-check el-cascader-node__prefix"></i>
);
},
renderLoadingIcon(h) {
return (
<i class="el-icon-loading el-cascader-node__postfix"></i>
);
},
renderExpandIcon(h) {
return (
<i class="el-icon-arrow-right el-cascader-node__postfix"></i>
);
},
renderContent(h) {
const { panel, node } = this;
const render = panel.renderLabelFn;
const vnode = render
? render({ node, data: node.data })
: null;
return (
<span class="el-cascader-node__label">{ vnode || node.label }</span>
);
}
},
render(h) {
const {
inActivePath,
inCheckedPath,
isChecked,
isLeaf,
isDisabled,
config,
nodeId
} = this;
const { expandTrigger, checkStrictly, multiple } = config;
const disabled = !checkStrictly && isDisabled;
const events = { on: {} };
if (!isLeaf) {
if (expandTrigger === 'click') {
events.on.click = this.handleExpand;
} else {
events.on.mouseenter = e => {
this.handleExpand();
this.$emit('expand', e);
};
events.on.focus = e => {
this.handleExpand();
this.$emit('expand', e);
};
}
} else if (!isDisabled && !checkStrictly && !multiple) {
events.on.click = this.handleCheckChange;
}
return (
<li
role="menuitem"
id={ nodeId }
aria-expanded={ inActivePath }
tabindex={ disabled ? null : -1 }
class={{
'el-cascader-node': true,
'is-selectable': checkStrictly,
'in-active-path': inActivePath,
'in-checked-path': inCheckedPath,
'is-active': isChecked,
'is-disabled': disabled
}}
{...events}>
{ this.renderPrefix(h) }
{ this.renderContent(h) }
{ this.renderPostfix(h) }
</li>
);
}
};
</script>

View File

@ -0,0 +1,354 @@
<template>
<div
:class="[
'el-cascader-panel',
border && 'is-bordered'
]"
@keydown="handleKeyDown">
<cascader-menu
ref="menu"
v-for="(menu, index) in menus"
:index="index"
:key="index"
:nodes="menu"></cascader-menu>
</div>
</template>
<script>
import CascaderMenu from './cascader-menu';
import Store from './store';
import merge from 'element-ui/src/utils/merge';
import AriaUtils from 'element-ui/src/utils/aria-utils';
import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
import {
noop,
coerceTruthyValueToArray,
isEqual,
isEmpty,
valueEquals
} from 'element-ui/src/utils/util';
const { keys: KeyCode } = AriaUtils;
const DefaultProps = {
expandTrigger: 'click', // or hover
multiple: false,
checkStrictly: false, // whether all nodes can be selected
emitPath: true, // wether to emit an array of all levels value in which node is located
lazy: false,
lazyLoad: noop,
value: 'value',
label: 'label',
children: 'children',
leaf: 'leaf',
disabled: 'disabled',
hoverThreshold: 500
};
const isLeaf = el => !el.getAttribute('aria-owns');
const getSibling = (el, distance) => {
const { parentNode } = el;
if (parentNode) {
const siblings = parentNode.querySelectorAll('.el-cascader-node[tabindex="-1"]');
const index = Array.prototype.indexOf.call(siblings, el);
return siblings[index + distance] || null;
}
return null;
};
const getMenuIndex = (el, distance) => {
if (!el) return;
const pieces = el.id.split('-');
return Number(pieces[pieces.length - 2]);
};
const focusNode = el => {
if (!el) return;
el.focus();
!isLeaf(el) && el.click();
};
const checkNode = el => {
if (!el) return;
const input = el.querySelector('input');
if (input) {
input.click();
} else if (isLeaf(el)) {
el.click();
}
};
export default {
name: 'ElCascaderPanel',
components: {
CascaderMenu
},
props: {
value: {},
options: Array,
props: Object,
border: {
type: Boolean,
default: true
},
renderLabel: Function
},
provide() {
return {
panel: this
};
},
data() {
return {
checkedValue: null,
checkedNodePaths: [],
store: [],
menus: [],
activePath: []
};
},
computed: {
config() {
return merge({ ...DefaultProps }, this.props || {});
},
multiple() {
return this.config.multiple;
},
checkStrictly() {
return this.config.checkStrictly;
},
leafOnly() {
return !this.checkStrictly;
},
isHoverMenu() {
return this.config.expandTrigger === 'hover';
},
renderLabelFn() {
return this.renderLabel || this.$scopedSlots.default;
}
},
watch: {
options: {
handler: function() {
this.initStore();
},
immediate: true,
deep: true
},
value() {
this.syncCheckedValue();
this.checkStrictly && this.calculateCheckedNodePaths();
},
checkedValue(val) {
if (!isEqual(val, this.value)) {
this.checkStrictly && this.calculateCheckedNodePaths();
this.$emit('input', val);
this.$emit('change', val);
}
}
},
mounted() {
if (!isEmpty(this.value)) {
this.syncCheckedValue();
}
},
methods: {
initStore() {
const { config, options } = this;
if (config.lazy && isEmpty(options)) {
this.lazyLoad();
} else {
this.store = new Store(options, config);
this.menus = [this.store.getNodes()];
this.syncMenuState();
}
},
syncCheckedValue() {
const { value, checkedValue } = this;
if (!isEqual(value, checkedValue)) {
this.checkedValue = value;
this.syncMenuState();
}
},
syncMenuState() {
const { multiple, checkStrictly } = this;
this.syncActivePath();
multiple && this.syncMultiCheckState();
checkStrictly && this.calculateCheckedNodePaths();
this.$nextTick(this.scrollIntoView);
},
syncMultiCheckState() {
const nodes = this.getFlattedNodes(this.leafOnly);
nodes.forEach(node => {
node.syncCheckState(this.checkedValue);
});
},
syncActivePath() {
let { checkedValue, store, multiple } = this;
if (isEmpty(checkedValue)) {
this.activePath = [];
this.menus = [store.getNodes()];
} else {
checkedValue = multiple ? checkedValue[0] : checkedValue;
const checkedNode = this.getNodeByValue(checkedValue) || {};
const nodes = [];
let { parent } = checkedNode;
while (parent) {
nodes.unshift(parent);
parent = parent.parent;
}
nodes.forEach(node => this.handleExpand(node, true /* silent */));
}
},
calculateCheckedNodePaths() {
const { checkedValue, multiple } = this;
const checkedValues = multiple
? coerceTruthyValueToArray(checkedValue)
: [ checkedValue ];
this.checkedNodePaths = checkedValues.map(v => {
const checkedNode = this.getNodeByValue(v);
return checkedNode ? checkedNode.pathNodes : [];
});
},
handleKeyDown(e) {
const { target, keyCode } = e;
switch (keyCode) {
case KeyCode.up:
const prev = getSibling(target, -1);
focusNode(prev);
break;
case KeyCode.down:
const next = getSibling(target, 1);
focusNode(next);
break;
case KeyCode.left:
const preMenu = this.$refs.menu[getMenuIndex(target) - 1];
if (preMenu) {
const expandedNode = preMenu.$el.querySelector('.el-cascader-node[aria-expanded="true"]');
focusNode(expandedNode);
}
break;
case KeyCode.right:
const nextMenu = this.$refs.menu[getMenuIndex(target) + 1];
if (nextMenu) {
const firstNode = nextMenu.$el.querySelector('.el-cascader-node[tabindex="-1"]');
focusNode(firstNode);
}
break;
case KeyCode.enter:
checkNode(target);
break;
case KeyCode.esc:
case KeyCode.tab:
this.$emit('close');
break;
default:
return;
}
},
handleExpand(node, silent) {
const { level } = node;
const path = this.activePath.slice(0, level - 1);
const menus = this.menus.slice(0, level);
if (!node.isLeaf) {
path.push(node);
menus.push(node.children);
}
if (valueEquals(path, this.activePath)) return;
this.activePath = path;
this.menus = menus;
if (!silent) {
const pathValues = path.map(node => node.getValue());
this.$emit('active-item-change', pathValues); // Deprecated
this.$emit('expand-change', pathValues);
}
},
handleCheckChange(value) {
this.checkedValue = value;
},
lazyLoad(node, onFullfiled) {
const { config } = this;
if (!node) {
node = node || { root: true, level: 0 };
this.store = new Store([], config);
this.menus = [this.store.getNodes()];
}
node.loading = true;
const resolve = dataList => {
const parent = node.root ? null : node;
dataList && dataList.length && this.store.appendNodes(dataList, parent);
node.loading = false;
node.loaded = true;
onFullfiled && onFullfiled(dataList);
};
config.lazyLoad(node, resolve);
},
/**
* public methods
*/
calculateMultiCheckedValue() {
this.checkedValue = this.getCheckedNodes(this.leafOnly)
.map(node => node.getValueByOption());
},
scrollIntoView() {
if (this.$isServer) return;
const menus = this.$refs.menu || [];
menus.forEach(menu => {
const menuElement = menu.$el;
if (menuElement) {
const container = menuElement.querySelector('.el-scrollbar__wrap');
const activeNode = menuElement.querySelector('.el-cascader-node.is-active') ||
menuElement.querySelector('.el-cascader-node.in-active-path');
scrollIntoView(container, activeNode);
}
});
},
getNodeByValue(val) {
return this.store.getNodeByValue(val);
},
getFlattedNodes(leafOnly) {
const cached = !this.config.lazy;
return this.store.getFlattedNodes(leafOnly, cached);
},
getCheckedNodes(leafOnly) {
const { checkedValue, multiple } = this;
if (multiple) {
const nodes = this.getFlattedNodes(leafOnly);
return nodes.filter(node => node.checked);
} else {
return isEmpty(checkedValue)
? []
: [this.getNodeByValue(checkedValue)];
}
},
clearCheckedNodes() {
const { config, leafOnly } = this;
const { multiple, emitPath } = config;
if (multiple) {
this.getCheckedNodes(leafOnly)
.filter(node => !node.isDisabled)
.forEach(node => node.doCheck(false));
this.calculateMultiCheckedValue();
} else {
this.checkedValue = emitPath ? [] : null;
}
}
}
};
</script>

View File

@ -0,0 +1,166 @@
import { isEqual, capitalize } from 'element-ui/src/utils/util';
import { isDef } from 'element-ui/src/utils/shared';
let uid = 0;
export default class Node {
constructor(data, config, parentNode) {
this.data = data;
this.config = config;
this.parent = parentNode || null;
this.level = !this.parent ? 1 : this.parent.level + 1;
this.uid = uid++;
this.initState();
this.initChildren();
}
initState() {
const { value: valueKey, label: labelKey } = this.config;
this.value = this.data[valueKey];
this.label = this.data[labelKey];
this.pathNodes = this.calculatePathNodes();
this.path = this.pathNodes.map(node => node.value);
this.pathLabels = this.pathNodes.map(node => node.label);
// lazy load
this.loading = false;
this.loaded = false;
}
initChildren() {
const { config } = this;
const childrenKey = config.children;
const childrenData = this.data[childrenKey];
this.hasChildren = Array.isArray(childrenData);
this.children = (childrenData || []).map(child => new Node(child, config, this));
}
get isDisabled() {
const { data, parent, config } = this;
const disabledKey = config.disabled;
const { checkStrictly } = config;
return data[disabledKey] ||
!checkStrictly && parent && parent.isDisabled;
}
get isLeaf() {
const { data, loaded, hasChildren, children } = this;
const { lazy, leaf: leafKey } = this.config;
if (lazy) {
const isLeaf = isDef(data[leafKey])
? data[leafKey]
: (loaded ? !children.length : false);
this.hasChildren = !isLeaf;
return isLeaf;
}
return !hasChildren;
}
calculatePathNodes() {
const nodes = [this];
let parent = this.parent;
while (parent) {
nodes.unshift(parent);
parent = parent.parent;
}
return nodes;
}
getPath() {
return this.path;
}
getValue() {
return this.value;
}
getValueByOption() {
return this.config.emitPath
? this.getPath()
: this.getValue();
}
getText(allLevels, separator) {
return allLevels ? this.pathLabels.join(separator) : this.label;
}
isSameNode(checkedValue) {
const value = this.getValueByOption();
return this.config.multiple && Array.isArray(checkedValue)
? checkedValue.some(val => isEqual(val, value))
: isEqual(checkedValue, value);
}
broadcast(event, ...args) {
const handlerName = `onParent${capitalize(event)}`;
this.children.forEach(child => {
if (child) {
// bottom up
child.broadcast(event, ...args);
child[handlerName] && child[handlerName](...args);
}
});
}
emit(event, ...args) {
const { parent } = this;
const handlerName = `onChild${capitalize(event)}`;
if (parent) {
parent[handlerName] && parent[handlerName](...args);
parent.emit(event, ...args);
}
}
onParentCheck(checked) {
if (!this.isDisabled) {
this.setCheckState(checked);
}
}
onChildCheck() {
const { children } = this;
const validChildren = children.filter(child => !child.isDisabled);
const checked = validChildren.length
? validChildren.every(child => child.checked)
: false;
this.setCheckState(checked);
}
setCheckState(checked) {
const totalNum = this.children.length;
const checkedNum = this.children.reduce((c, p) => {
const num = p.checked ? 1 : (p.indeterminate ? 0.5 : 0);
return c + num;
}, 0);
this.checked = checked;
this.indeterminate = checkedNum !== totalNum && checkedNum > 0;
}
syncCheckState(checkedValue) {
const value = this.getValueByOption();
const checked = this.isSameNode(checkedValue, value);
this.doCheck(checked);
}
doCheck(checked) {
if (this.checked !== checked) {
if (this.config.checkStrictly) {
this.checked = checked;
} else {
// bottom up to unify the calculation of the indeterminate state
this.broadcast('check', checked);
this.setCheckState(checked);
this.emit('check');
}
}
}
}

View File

@ -0,0 +1,63 @@
import Node from './node';
import { coerceTruthyValueToArray } from 'element-ui/src/utils/util';
const flatNodes = (data, leafOnly) => {
return data.reduce((res, node) => {
if (node.isLeaf) {
res.push(node);
} else {
!leafOnly && res.push(node);
res = res.concat(flatNodes(node.children, leafOnly));
}
return res;
}, []);
};
export default class Store {
constructor(data, config) {
this.config = config;
this.initNodes(data);
}
initNodes(data) {
data = coerceTruthyValueToArray(data);
this.nodes = data.map(nodeData => new Node(nodeData, this.config));
this.flattedNodes = this.getFlattedNodes(false, false);
this.leafNodes = this.getFlattedNodes(true, false);
}
appendNode(nodeData, parentNode) {
const node = new Node(nodeData, this.config, parentNode);
const children = parentNode ? parentNode.children : this.nodes;
children.push(node);
}
appendNodes(nodeDataList, parentNode) {
nodeDataList = coerceTruthyValueToArray(nodeDataList);
nodeDataList.forEach(nodeData => this.appendNode(nodeData, parentNode));
}
getNodes() {
return this.nodes;
}
getFlattedNodes(leafOnly, cached = true) {
const cachedNodes = leafOnly ? this.leafNodes : this.flattedNodes;
return cached
? cachedNodes
: flatNodes(this.nodes, leafOnly);
}
getNodeByValue(value) {
if (value) {
value = Array.isArray(value) ? value[value.length - 1] : value;
const nodes = this.getFlattedNodes(false, !this.config.lazy)
.filter(node => node.value === value);
return nodes && nodes.length ? nodes[0] : null;
}
return null;
}
}

View File

@ -1,4 +1,4 @@
import Cascader from './src/main';
import Cascader from './src/cascader';
/* istanbul ignore next */
Cascader.install = function(Vue) {

View File

@ -0,0 +1,639 @@
<template>
<div
ref="reference"
:class="[
'el-cascader',
realSize && `el-cascader--${realSize}`,
{ 'is-disabled': isDisabled }
]"
v-clickoutside="() => toggleDropDownVisible(false)"
@mouseenter="inputHover = true"
@mouseleave="inputHover = false"
@click="() => toggleDropDownVisible(readonly ? undefined : true)"
@keydown="handleKeyDown">
<el-input
ref="input"
v-model="multiple ? presentText : inputValue"
:size="realSize"
:placeholder="placeholder"
:readonly="readonly"
:disabled="isDisabled"
:validate-event="false"
:class="{ 'is-focus': dropDownVisible }"
@focus="handleFocus"
@blur="handleBlur"
@input="handleInput">
<template slot="suffix">
<i
v-if="clearBtnVisible"
key="clear"
class="el-input__icon el-icon-circle-close"
@click.stop="handleClear"></i>
<i
v-else
key="arrow-down"
:class="[
'el-input__icon',
'el-icon-arrow-down',
dropDownVisible && 'is-reverse'
]"
@click.stop="toggleDropDownVisible()"></i>
</template>
</el-input>
<div v-if="multiple" class="el-cascader__tags">
<el-tag
v-for="(tag, index) in presentTags"
:key="tag.key"
type="info"
:size="tagSize"
:hit="tag.hitState"
:closable="tag.closable"
disable-transitions
@close="deleteTag(index)">
<span>{{ tag.text }}</span>
</el-tag>
<input
v-if="filterable && !isDisabled"
v-model.trim="inputValue"
type="text"
class="el-cascader__search-input"
:placeholder="presentTags.length ? '' : placeholder"
@input="e => handleInput(inputValue, e)"
@click.stop="toggleDropDownVisible(true)"
@keydown.delete="handleDelete">
</div>
<transition name="el-zoom-in-top" @after-leave="handleDropdownLeave">
<div
v-show="dropDownVisible"
ref="popper"
:class="['el-popper', 'el-cascader__dropdown', popperClass]">
<el-cascader-panel
ref="panel"
v-show="!filtering"
v-model="checkedValue"
:options="options"
:props="config"
:border="false"
:render-label="$scopedSlots.default"
@expand-change="handleExpandChange"
@close="toggleDropDownVisible(false)"></el-cascader-panel>
<el-scrollbar
ref="suggestionPanel"
v-if="filterable"
v-show="filtering"
tag="ul"
class="el-cascader__suggestion-panel"
view-class="el-cascader__suggestion-list"
@keydown.native="handleSuggestionKeyDown">
<template v-if="suggestions.length">
<li
v-for="(item, index) in suggestions"
:key="item.uid"
:class="[
'el-cascader__suggestion-item',
item.checked && 'is-checked'
]"
:tabindex="-1"
@click="handleSuggestionClick(index)">
<span>{{ item.text }}</span>
<i v-if="item.checked" class="el-icon-check"></i>
</li>
</template>
<slot v-else name="empty">
<li class="el-cascader__empty-text">{{ t('el.cascader.noMatch') }}</li>
</slot>
</el-scrollbar>
</div>
</transition>
</div>
</template>
<script>
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 Migrating from 'element-ui/src/mixins/migrating';
import ElInput from 'element-ui/packages/input';
import ElTag from 'element-ui/packages/tag';
import ElScrollbar from 'element-ui/packages/scrollbar';
import ElCascaderPanel from 'element-ui/packages/cascader-panel';
import AriaUtils from 'element-ui/src/utils/aria-utils';
import { t } from 'element-ui/src/locale';
import { isEqual, isEmpty, kebabCase } from 'element-ui/src/utils/util';
import { isUndefined, isFunction } from 'element-ui/src/utils/types';
import { isDef } from 'element-ui/src/utils/shared';
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
import debounce from 'throttle-debounce/debounce';
const { keys: KeyCode } = AriaUtils;
const MigratingProps = {
expandTrigger: {
newProp: 'expandTrigger',
type: String
},
changeOnSelect: {
newProp: 'checkStrictly',
type: Boolean
},
hoverThreshold: {
newProp: 'hoverThreshold',
type: Number
}
};
const PopperMixin = {
props: {
placement: {
type: String,
default: 'bottom-start'
},
appendToBody: Popper.props.appendToBody,
visibleArrow: {
type: Boolean,
default: true
},
arrowOffset: Popper.props.arrowOffset,
offset: Popper.props.offset,
boundariesPadding: Popper.props.boundariesPadding,
popperOptions: Popper.props.popperOptions
},
methods: Popper.methods,
data: Popper.data,
beforeDestroy: Popper.beforeDestroy
};
const InputSizeMap = {
medium: 36,
small: 32,
mini: 28
};
export default {
name: 'ElCascader',
directives: { Clickoutside },
mixins: [PopperMixin, Emitter, Locale, Migrating],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
components: {
ElInput,
ElTag,
ElScrollbar,
ElCascaderPanel
},
props: {
value: {},
options: Array,
props: Object,
size: String,
placeholder: {
type: String,
default: () => t('el.cascader.placeholder')
},
disabled: Boolean,
clearable: Boolean,
filterable: Boolean,
filterMethod: Function,
separator: {
type: String,
default: ' / '
},
showAllLevels: {
type: Boolean,
default: true
},
collapseTags: Boolean,
debounce: {
type: Number,
default: 300
},
beforeFilter: {
type: Function,
default: () => (() => {})
},
popperClass: String
},
data() {
return {
dropDownVisible: false,
checkedValue: this.value || null,
inputHover: false,
inputValue: null,
presentText: null,
presentTags: [],
checkedNodes: [],
filtering: false,
suggestions: [],
inputInitialHeight: 0,
pressDeleteCount: 0
};
},
computed: {
realSize() {
const _elFormItemSize = (this.elFormItem || {}).elFormItemSize;
return this.size || _elFormItemSize || (this.$ELEMENT || {}).size;
},
tagSize() {
return ['small', 'mini'].indexOf(this.realSize) > -1
? 'mini'
: 'small';
},
isDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
config() {
const config = this.props || {};
const { $attrs } = this;
Object
.keys(MigratingProps)
.forEach(oldProp => {
const { newProp, type } = MigratingProps[oldProp];
let oldValue = $attrs[oldProp] || $attrs[kebabCase(oldProp)];
if (isDef(oldProp) && !isDef(config[newProp])) {
if (type === Boolean && oldValue === '') {
oldValue = true;
}
config[newProp] = oldValue;
}
});
return config;
},
multiple() {
return this.config.multiple;
},
leafOnly() {
return !this.config.checkStrictly;
},
readonly() {
return !this.filterable || this.multiple;
},
clearBtnVisible() {
if (!this.clearable || this.isDisabled || this.filtering || !this.inputHover) {
return false;
}
return this.multiple
? !!this.checkedNodes.filter(node => !node.isDisabled).length
: !!this.presentText;
},
panel() {
return this.$refs.panel;
}
},
watch: {
value(val) {
if (!isEqual(val, this.checkedValue)) {
this.checkedValue = val;
this.computePresentContent();
}
},
checkedValue(val) {
const { value } = this;
if (!isEqual(val, value) || isUndefined(value)) {
this.$emit('input', val);
this.$emit('change', val);
this.dispatch('ElFormItem', 'el.form.change', [val]);
this.computePresentContent();
}
},
options: {
handler: function() {
this.$nextTick(this.computePresentContent);
},
deep: true
},
presentText(val) {
this.inputValue = val;
},
presentTags(val, oldVal) {
if (this.multiple && (val.length || oldVal.length)) {
this.$nextTick(this.updateStyle);
}
},
filtering(val) {
this.$nextTick(this.updatePopper);
}
},
mounted() {
const { input } = this.$refs;
if (input && input.$el) {
this.inputInitialHeight = input.$el.offsetHeight || InputSizeMap[this.realSize] || 40;
}
if (!isEmpty(this.value)) {
this.computePresentContent();
}
this.filterHandler = debounce(this.debounce, () => {
const { inputValue } = this;
if (!inputValue) {
this.filtering = false;
return;
}
const before = this.beforeFilter(inputValue);
if (before && before.then) {
before.then(this.getSuggestions);
} else if (before !== false) {
this.getSuggestions();
} else {
this.filtering = false;
}
});
addResizeListener(this.$el, this.updateStyle);
},
beforeDestroy() {
removeResizeListener(this.$el, this.updateStyle);
},
methods: {
getMigratingConfig() {
return {
props: {
'expand-trigger': 'expand-trigger is removed, use `props.expandTrigger` instead.',
'change-on-select': 'change-on-select is removed, use `props.checkStrictly` instead.',
'hover-threshold': 'hover-threshold is removed, use `props.hoverThreshold` instead'
},
events: {
'active-item-change': 'active-item-change is renamed to expand-change'
}
};
},
toggleDropDownVisible(visible) {
if (this.isDisabled) return;
const { dropDownVisible } = this;
const { input } = this.$refs;
visible = isDef(visible) ? visible : !dropDownVisible;
if (visible !== dropDownVisible) {
this.dropDownVisible = visible;
if (visible) {
this.$nextTick(() => {
this.updatePopper();
this.panel.scrollIntoView();
});
}
input.$refs.input.setAttribute('aria-expanded', visible);
this.$emit('visible-change', visible);
}
},
handleDropdownLeave() {
this.filtering = false;
this.inputValue = this.presentText;
},
handleKeyDown(event) {
switch (event.keyCode) {
case KeyCode.enter:
this.toggleDropDownVisible();
break;
case KeyCode.down:
this.toggleDropDownVisible(true);
this.focusFirstNode();
event.preventDefault();
break;
case KeyCode.esc:
case KeyCode.tab:
this.toggleDropDownVisible(false);
break;
}
},
handleFocus(e) {
this.$emit('focus', e);
},
handleBlur(e) {
this.$emit('blur', e);
},
handleInput(val, event) {
!this.dropDownVisible && this.toggleDropDownVisible(true);
if (event && event.isComposing) return;
if (val) {
this.filterHandler();
} else {
this.filtering = false;
}
},
handleClear() {
this.presentText = '';
this.panel.clearCheckedNodes();
},
handleExpandChange(value) {
this.$nextTick(this.updatePopper.bind(this));
this.$emit('expand-change', value);
this.$emit('active-item-change', value); // Deprecated
},
focusFirstNode() {
this.$nextTick(() => {
const { filtering } = this;
const { popper, suggestionPanel } = this.$refs;
let firstNode = null;
if (filtering && suggestionPanel) {
firstNode = suggestionPanel.$el.querySelector('.el-cascader__suggestion-item');
} else {
const firstMenu = popper.querySelector('.el-cascader-menu');
firstNode = firstMenu.querySelector('.el-cascader-node[tabindex="-1"]');
}
if (firstNode) {
firstNode.focus();
!filtering && firstNode.click();
}
});
},
computePresentContent() {
this.$nextTick(() => {
const { multiple, checkStrictly } = this.config;
if (multiple) {
this.computePresentTags();
this.presentText = this.presentTags.length ? ' ' : null;
} else {
this.computePresentText();
if (!checkStrictly && this.dropDownVisible) {
this.toggleDropDownVisible(false);
}
}
});
},
computePresentText() {
const { checkedValue, config } = this;
if (!isEmpty(checkedValue)) {
const node = this.panel.getNodeByValue(checkedValue);
if (node && (config.checkStrictly || node.isLeaf)) {
this.presentText = node.getText(this.showAllLevels, this.separator);
return;
}
}
this.presentText = null;
},
computePresentTags() {
const { isDisabled, leafOnly, showAllLevels, separator, collapseTags } = this;
const checkedNodes = this.getCheckedNodes(leafOnly);
const tags = [];
const genTag = node => ({
node,
key: node.uid,
text: node.getText(showAllLevels, separator),
hitState: false,
closable: !isDisabled && !node.isDisabled
});
if (checkedNodes.length) {
const [first, ...rest] = checkedNodes;
const restCount = rest.length;
tags.push(genTag(first));
if (restCount) {
if (collapseTags) {
tags.push({
key: -1,
text: `+ ${restCount}`,
closable: false
});
} else {
rest.forEach(node => tags.push(genTag(node)));
}
}
}
this.checkedNodes = checkedNodes;
this.presentTags = tags;
},
getSuggestions() {
let { filterMethod } = this;
if (!isFunction(filterMethod)) {
filterMethod = (node, keyword) => node.text.includes(keyword);
}
const suggestions = this.panel.getFlattedNodes(this.leafOnly)
.filter(node => {
if (node.isDisabled) return false;
node.text = node.getText(this.showAllLevels, this.separator) || '';
return filterMethod(node, this.inputValue);
});
if (this.multiple) {
this.presentTags.forEach(tag => {
tag.hitState = false;
});
} else {
suggestions.forEach(node => {
node.checked = isEqual(this.checkedValue, node.getValueByOption());
});
}
this.filtering = true;
this.suggestions = suggestions;
this.$nextTick(this.updatePopper);
},
handleSuggestionKeyDown(event) {
const { keyCode, target } = event;
switch (keyCode) {
case KeyCode.enter:
target.click();
break;
case KeyCode.up:
const prev = target.previousElementSibling;
prev && prev.focus();
break;
case KeyCode.down:
const next = target.nextElementSibling;
next && next.focus();
break;
case KeyCode.esc:
case KeyCode.tab:
this.toggleDropDownVisible(false);
break;
}
},
handleDelete() {
const { inputValue, pressDeleteCount, presentTags } = this;
const lastIndex = presentTags.length - 1;
const lastTag = presentTags[lastIndex];
this.pressDeleteCount = inputValue ? 0 : pressDeleteCount + 1;
if (!lastTag) return;
if (this.pressDeleteCount) {
if (lastTag.hitState) {
this.deleteTag(lastIndex);
} else {
lastTag.hitState = true;
}
}
},
handleSuggestionClick(index) {
const { multiple } = this;
const targetNode = this.suggestions[index];
if (multiple) {
const { checked } = targetNode;
targetNode.doCheck(!checked);
this.panel.calculateMultiCheckedValue();
} else {
this.checkedValue = targetNode.getValueByOption();
this.toggleDropDownVisible(false);
}
},
deleteTag(index) {
const { checkedValue } = this;
const val = checkedValue[index];
this.checkedValue = checkedValue.filter((n, i) => i !== index);
this.$emit('remove-tag', val);
},
updateStyle() {
const { $el, inputInitialHeight } = this;
if (this.$isServer || !$el) return;
const { suggestionPanel } = this.$refs;
const inputInner = $el.querySelector('.el-input__inner');
if (!inputInner) return;
const tags = $el.querySelector('.el-cascader__tags');
let suggestionPanelEl = null;
if (suggestionPanel && (suggestionPanelEl = suggestionPanel.$el)) {
const suggestionList = suggestionPanelEl.querySelector('.el-cascader__suggestion-list');
suggestionList.style.minWidth = inputInner.offsetWidth + 'px';
}
if (tags) {
const { offsetHeight } = tags;
const height = Math.max(offsetHeight + 6, inputInitialHeight) + 'px';
inputInner.style.height = height;
this.updatePopper();
}
},
getCheckedNodes(leafOnly) {
return this.panel.getCheckedNodes(leafOnly);
}
}
};
</script>

View File

@ -1,452 +0,0 @@
<template>
<span
class="el-cascader"
:class="[
{
'is-opened': menuVisible,
'is-disabled': cascaderDisabled
},
cascaderSize ? 'el-cascader--' + cascaderSize : ''
]"
@click="handleClick"
@mouseenter="inputHover = true"
@focus="inputHover = true"
@mouseleave="inputHover = false"
@blur="inputHover = false"
ref="reference"
v-clickoutside="handleClickoutside"
@keydown="handleKeydown"
>
<el-input
ref="input"
:readonly="readonly"
:placeholder="currentLabels.length ? undefined : placeholder"
v-model="inputValue"
@input="debouncedInputChange"
@focus="handleFocus"
@blur="handleBlur"
@compositionstart.native="handleComposition"
@compositionend.native="handleComposition"
:validate-event="false"
:size="size"
:disabled="cascaderDisabled"
:class="{ 'is-focus': menuVisible }"
>
<template slot="suffix">
<i
key="1"
v-if="clearable && inputHover && currentLabels.length"
class="el-input__icon el-icon-circle-close el-cascader__clearIcon"
@click="clearValue"
></i>
<i
key="2"
v-else
class="el-input__icon el-icon-arrow-down"
:class="{ 'is-reverse': menuVisible }"
></i>
</template>
</el-input>
<span class="el-cascader__label" v-show="inputValue === '' && !isOnComposition">
<template v-if="showAllLevels">
<template v-for="(label, index) in currentLabels">
{{ label }}
<span v-if="index < currentLabels.length - 1" :key="index"> {{ separator }} </span>
</template>
</template>
<template v-else>
{{ currentLabels[currentLabels.length - 1] }}
</template>
</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';
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';
import { generateId, escapeRegexpString, isIE, isEdge } from 'element-ui/src/utils/util';
const popperMixin = {
props: {
placement: {
type: String,
default: 'bottom-start'
},
appendToBody: Popper.props.appendToBody,
arrowOffset: Popper.props.arrowOffset,
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, emitter, Locale],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
components: {
ElInput
},
props: {
options: {
type: Array,
required: true
},
props: {
type: Object,
default() {
return {
children: 'children',
label: 'label',
value: 'value',
disabled: 'disabled'
};
}
},
value: {
type: Array,
default() {
return [];
}
},
separator: {
type: String,
default: '/'
},
placeholder: {
type: String,
default() {
return t('el.cascader.placeholder');
}
},
disabled: Boolean,
clearable: {
type: Boolean,
default: false
},
changeOnSelect: Boolean,
popperClass: String,
expandTrigger: {
type: String,
default: 'click'
},
filterable: Boolean,
size: String,
showAllLevels: {
type: Boolean,
default: true
},
debounce: {
type: Number,
default: 300
},
beforeFilter: {
type: Function,
default: () => (() => {})
},
hoverThreshold: {
type: Number,
default: 500
}
},
data() {
return {
currentValue: this.value || [],
menu: null,
debouncedInputChange() {},
menuVisible: false,
inputHover: false,
inputValue: '',
flatOptions: null,
id: generateId(),
needFocus: true,
isOnComposition: false
};
},
computed: {
labelKey() {
return this.props.label || 'label';
},
valueKey() {
return this.props.value || 'value';
},
childrenKey() {
return this.props.children || 'children';
},
disabledKey() {
return this.props.disabled || 'disabled';
},
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;
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
cascaderSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
cascaderDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
readonly() {
return !this.filterable || (!isIE() && !isEdge() && !this.menuVisible);
}
},
watch: {
menuVisible(value) {
this.$refs.input.$refs.input.setAttribute('aria-expanded', value);
value ? this.showMenu() : this.hideMenu();
this.$emit('visible-change', value);
},
value(value) {
this.currentValue = value;
},
currentValue(value) {
this.dispatch('ElFormItem', 'el.form.change', [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.menu.hoverThreshold = this.hoverThreshold;
this.popperElm = this.menu.$el;
this.menu.$refs.menus[0].setAttribute('id', `cascader-menu-${this.id}`);
this.menu.$on('pick', this.handlePick);
this.menu.$on('activeItemChange', this.handleActiveItemChange);
this.menu.$on('menuLeave', this.doDestroy);
this.menu.$on('closeInside', this.handleClickoutside);
},
showMenu() {
if (!this.menu) {
this.initMenu();
}
this.menu.value = this.currentValue.slice(0);
this.menu.visible = true;
this.menu.options = this.options;
this.$nextTick(_ => {
this.updatePopper();
this.menu.inputWidth = this.$refs.input.$el.offsetWidth - 2;
});
},
hideMenu() {
this.inputValue = '';
this.menu.visible = false;
if (this.needFocus) {
this.$refs.input.focus();
} else {
this.needFocus = true;
}
},
handleActiveItemChange(value) {
this.$nextTick(_ => {
this.updatePopper();
});
this.$emit('active-item-change', value);
},
handleKeydown(e) {
const keyCode = e.keyCode;
if (keyCode === 13) {
this.handleClick();
} else if (keyCode === 40) { // down
this.menuVisible = true; //
setTimeout(() => {
const firstMenu = this.popperElm.querySelectorAll('.el-cascader-menu')[0];
firstMenu.querySelectorAll("[tabindex='-1']")[0].focus();
});
e.stopPropagation();
e.preventDefault();
} else if (keyCode === 27 || keyCode === 9) { // esc tab
this.inputValue = '';
if (this.menu) this.menu.visible = false;
}
},
handlePick(value, close = true) {
this.currentValue = value;
this.$emit('input', value);
this.$emit('change', value);
if (close) {
this.menuVisible = false;
} else {
this.$nextTick(this.updatePopper);
}
},
handleInputChange(value) {
if (!this.menuVisible) return;
const flatOptions = this.flatOptions;
if (!value) {
this.menu.options = this.options;
this.$nextTick(this.updatePopper);
return;
}
let filteredFlatOptions = flatOptions.filter(optionsStack => {
return optionsStack.some(option => new RegExp(escapeRegexpString(value), 'i')
.test(option[this.labelKey]));
});
if (filteredFlatOptions.length > 0) {
filteredFlatOptions = filteredFlatOptions.map(optionStack => {
return {
__IS__FLAT__OPTIONS: true,
value: optionStack.map(item => item[this.valueKey]),
label: this.renderFilteredOptionLabel(value, optionStack),
disabled: optionStack.some(item => item[this.disabledKey])
};
});
} else {
filteredFlatOptions = [{
__IS__FLAT__OPTIONS: true,
label: this.t('el.cascader.noMatch'),
value: '',
disabled: true
}];
}
this.menu.options = filteredFlatOptions;
this.$nextTick(this.updatePopper);
},
renderFilteredOptionLabel(inputValue, optionsStack) {
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 : [` ${this.separator} `, 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[this.childrenKey]) {
flatOptions.push(optionsStack);
} else {
if (this.changeOnSelect) {
flatOptions.push(optionsStack);
}
flatOptions = flatOptions.concat(this.flattenOptions(option[this.childrenKey], optionsStack));
}
});
return flatOptions;
},
clearValue(ev) {
ev.stopPropagation();
this.handlePick([], true);
},
handleClickoutside(pickFinished = false) {
if (this.menuVisible && !pickFinished) {
this.needFocus = false;
}
this.menuVisible = false;
},
handleClick() {
if (this.cascaderDisabled) return;
this.$refs.input.focus();
if (this.filterable) {
this.menuVisible = true;
return;
}
this.menuVisible = !this.menuVisible;
},
handleFocus(event) {
this.$emit('focus', event);
},
handleBlur(event) {
this.$emit('blur', event);
},
handleComposition(event) {
this.isOnComposition = event.type !== 'compositionend';
}
},
created() {
this.debouncedInputChange = debounce(this.debounce, value => {
const before = this.beforeFilter(value);
if (before && before.then) {
this.menu.options = [{
__IS__FLAT__OPTIONS: true,
label: this.t('el.cascader.loading'),
value: '',
disabled: true
}];
before
.then(() => {
this.$nextTick(() => {
this.handleInputChange(value);
});
});
} else if (before !== false) {
this.$nextTick(() => {
this.handleInputChange(value);
});
}
});
},
mounted() {
this.flatOptions = this.flattenOptions(this.options);
}
};
</script>

View File

@ -1,375 +0,0 @@
<script>
import { isDef } from 'element-ui/src/utils/shared';
import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
import { generateId } from 'element-ui/src/utils/util';
const copyArray = (arr, props) => {
if (!arr || !Array.isArray(arr) || !props) return arr;
const result = [];
const configurableProps = ['__IS__FLAT__OPTIONS', 'label', 'value', 'disabled'];
const childrenProp = props.children || 'children';
arr.forEach(item => {
const itemCopy = {};
configurableProps.forEach(prop => {
let name = props[prop];
let value = item[name];
if (value === undefined) {
name = prop;
value = item[name];
}
if (value !== undefined) itemCopy[name] = value;
});
if (Array.isArray(item[childrenProp])) {
itemCopy[childrenProp] = copyArray(item[childrenProp], props);
}
result.push(itemCopy);
});
return result;
};
export default {
name: 'ElCascaderMenu',
data() {
return {
inputWidth: 0,
options: [],
props: {},
visible: false,
activeValue: [],
value: [],
expandTrigger: 'click',
changeOnSelect: false,
popperClass: '',
hoverTimer: 0,
clicking: false,
id: generateId()
};
},
watch: {
visible(value) {
if (value) {
this.activeValue = this.value;
}
},
value: {
immediate: true,
handler(value) {
this.activeValue = value;
}
}
},
computed: {
activeOptions: {
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 !== undefined) option[prop] = value;
});
if (Array.isArray(option.children)) {
formatOptions(option.children);
}
});
};
const loadActiveOptions = (options, activeOptions = []) => {
const level = activeOptions.length;
activeOptions[level] = options;
let active = activeValue[level];
if (isDef(active)) {
options = options.filter(option => option.value === active)[0];
if (options && options.children) {
loadActiveOptions(options.children, activeOptions);
}
}
return activeOptions;
};
const optionsCopy = copyArray(this.options, this.props);
formatOptions(optionsCopy);
return loadActiveOptions(optionsCopy);
}
}
},
methods: {
select(item, menuIndex) {
if (item.__IS__FLAT__OPTIONS) {
this.activeValue = item.value;
} else if (menuIndex) {
this.activeValue.splice(menuIndex, this.activeValue.length - 1, item.value);
} else {
this.activeValue = [item.value];
}
this.$emit('pick', this.activeValue.slice());
},
handleMenuLeave() {
this.$emit('menuLeave');
},
activeItem(item, menuIndex) {
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.slice(), false);
} else {
this.$emit('activeItemChange', this.activeValue);
}
},
scrollMenu(menu) {
scrollIntoView(menu, menu.getElementsByClassName('is-active')[0]);
},
handleMenuEnter() {
this.$nextTick(() => this.$refs.menus.forEach(menu => this.scrollMenu(menu)));
}
},
render(h) {
const {
activeValue,
activeOptions,
visible,
expandTrigger,
popperClass,
hoverThreshold
} = this;
let itemId = null;
let itemIndex = 0;
let hoverMenuRefs = {};
const hoverMenuHandler = e => {
const activeMenu = hoverMenuRefs.activeMenu;
if (!activeMenu) return;
const offsetX = e.offsetX;
const width = activeMenu.offsetWidth;
const height = activeMenu.offsetHeight;
if (e.target === hoverMenuRefs.activeItem) {
clearTimeout(this.hoverTimer);
const {activeItem} = hoverMenuRefs;
const offsetY_top = activeItem.offsetTop;
const offsetY_Bottom = offsetY_top + activeItem.offsetHeight;
hoverMenuRefs.hoverZone.innerHTML = `
<path style="pointer-events: auto;" fill="transparent" d="M${offsetX} ${offsetY_top} L${width} 0 V${offsetY_top} Z" />
<path style="pointer-events: auto;" fill="transparent" d="M${offsetX} ${offsetY_Bottom} L${width} ${height} V${offsetY_Bottom} Z" />
`;
} else {
if (!this.hoverTimer) {
this.hoverTimer = setTimeout(() => {
hoverMenuRefs.hoverZone.innerHTML = '';
}, hoverThreshold);
}
}
};
const menus = this._l(activeOptions, (menu, menuIndex) => {
let isFlat = false;
const menuId = `menu-${this.id}-${ menuIndex}`;
const ownsId = `menu-${this.id}-${ menuIndex + 1 }`;
const items = this._l(menu, item => {
const events = {
on: {}
};
if (item.__IS__FLAT__OPTIONS) isFlat = true;
if (!item.disabled) {
// keydown up/down/left/right/enter
events.on.keydown = (ev) => {
const keyCode = ev.keyCode;
if ([37, 38, 39, 40, 13, 9, 27].indexOf(keyCode) < 0) {
return;
}
const currentEle = ev.target;
const parentEle = this.$refs.menus[menuIndex];
const menuItemList = parentEle.querySelectorAll("[tabindex='-1']");
const currentIndex = Array.prototype.indexOf.call(menuItemList, currentEle); //
let nextIndex, nextMenu;
if ([38, 40].indexOf(keyCode) > -1) {
if (keyCode === 38) { // up
nextIndex = currentIndex !== 0 ? (currentIndex - 1) : currentIndex;
} else if (keyCode === 40) { // down
nextIndex = currentIndex !== (menuItemList.length - 1) ? currentIndex + 1 : currentIndex;
}
menuItemList[nextIndex].focus();
} else if (keyCode === 37) { // left
if (menuIndex !== 0) {
const previousMenu = this.$refs.menus[menuIndex - 1];
previousMenu.querySelector('[aria-expanded=true]').focus();
}
} else if (keyCode === 39) { // right
if (item.children) {
// menu menumenuitem
nextMenu = this.$refs.menus[menuIndex + 1];
nextMenu.querySelectorAll("[tabindex='-1']")[0].focus();
}
} else if (keyCode === 13) {
if (!item.children) {
const id = currentEle.getAttribute('id');
parentEle.setAttribute('aria-activedescendant', id);
this.select(item, menuIndex);
this.$nextTick(() => this.scrollMenu(this.$refs.menus[menuIndex]));
}
} else if (keyCode === 9 || keyCode === 27) { // esc tab
this.$emit('closeInside');
}
};
if (item.children) {
let triggerEvent = {
click: 'click',
hover: 'mouseenter'
}[expandTrigger];
const triggerHandler = () => {
if (this.visible) {
this.activeItem(item, menuIndex);
this.$nextTick(() => {
// adjust self and next level
this.scrollMenu(this.$refs.menus[menuIndex]);
this.scrollMenu(this.$refs.menus[menuIndex + 1]);
});
}
};
events.on[triggerEvent] = triggerHandler;
if (triggerEvent === 'mouseenter' && this.changeOnSelect) {
events.on.click = () => {
if (this.activeValue.indexOf(item.value) !== -1) {
this.$emit('closeInside', true);
}
};
}
events.on['mousedown'] = () => {
this.clicking = true;
};
events.on['focus'] = () => { // focus
if (this.clicking) {
this.clicking = false;
return;
}
triggerHandler();
};
} else {
events.on.click = () => {
this.select(item, menuIndex);
this.$nextTick(() => this.scrollMenu(this.$refs.menus[menuIndex]));
};
}
}
if (!item.disabled && !item.children) { // no children set id
itemId = `${menuId}-${itemIndex}`;
itemIndex++;
}
return (
<li
class={{
'el-cascader-menu__item': true,
'el-cascader-menu__item--extensible': item.children,
'is-active': item.value === activeValue[menuIndex],
'is-disabled': item.disabled
}}
ref={item.value === activeValue[menuIndex] ? 'activeItem' : null}
{...events}
tabindex= { item.disabled ? null : -1 }
role="menuitem"
aria-haspopup={ !!item.children }
aria-expanded={ item.value === activeValue[menuIndex] }
id = { itemId }
aria-owns = { !item.children ? null : ownsId }
>
<span>{item.label}</span>
</li>
);
});
let menuStyle = {};
if (isFlat) {
menuStyle.minWidth = this.inputWidth + 'px';
}
const isHoveredMenu = expandTrigger === 'hover' && activeValue.length - 1 === menuIndex;
const hoverMenuEvent = {
on: {
}
};
if (isHoveredMenu) {
hoverMenuEvent.on.mousemove = hoverMenuHandler;
menuStyle.position = 'relative';
}
return (
<ul
class={{
'el-cascader-menu': true,
'el-cascader-menu--flexible': isFlat
}}
{...hoverMenuEvent}
style={menuStyle}
refInFor
ref="menus"
role="menu"
id = { menuId }
>
{items}
{
isHoveredMenu
? (<svg
ref="hoverZone"
style={{
position: 'absolute',
top: 0,
height: '100%',
width: '100%',
left: 0,
pointerEvents: 'none'
}}
></svg>) : null
}
</ul>
);
});
if (expandTrigger === 'hover') {
this.$nextTick(() => {
const activeItem = this.$refs.activeItem;
if (activeItem) {
const activeMenu = activeItem.parentElement;
const hoverZone = this.$refs.hoverZone;
hoverMenuRefs = {
activeMenu,
activeItem,
hoverZone
};
} else {
hoverMenuRefs = {};
}
});
}
return (
<transition name="el-zoom-in-top" on-before-enter={this.handleMenuEnter} on-after-leave={this.handleMenuLeave}>
<div
v-show={visible}
class={[
'el-cascader-menus el-popper',
popperClass
]}
ref="wrapper"
>
<div x-arrow class="popper__arrow"></div>
{menus}
</div>
</transition>
);
}
};
</script>

View File

@ -0,0 +1,124 @@
@import "mixins/mixins";
@import "common/var";
@import "./checkbox";
@import "./radio";
@import "./scrollbar";
@include b(cascader-panel) {
display: flex;
border-radius: $--cascader-menu-radius;
font-size: $--cascader-menu-font-size;
@include when(bordered) {
border: $--cascader-menu-border;
border-radius: $--cascader-menu-radius;
}
}
@include b(cascader-menu) {
min-width: 180px;
box-sizing: border-box;
color: $--cascader-menu-font-color;
border-right: $--cascader-menu-border;
&:last-child {
border-right: none;
.el-cascader-node {
padding-right: 20px;
}
}
@include e(wrap) {
height: 204px;
}
@include e(list) {
position: relative;
min-height: 100%;
margin: 0;
padding: 6px 0;
list-style: none;
box-sizing: border-box;
}
@include e(hover-zone) {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
@include e(empty-text) {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: $--cascader-color-empty;
}
}
@include b(cascader-node) {
position: relative;
display: flex;
align-items: center;
padding: 0 30px 0 20px;
height: 34px;
line-height: 34px;
outline: none;
&.is-selectable.in-active-path {
color: $--cascader-menu-font-color;
}
&.in-active-path,
&.is-selectable.in-checked-path,
&.is-active {
color: $--cascader-menu-selected-font-color;
font-weight: bold;
}
&:not(.is-disabled) {
cursor: pointer;
&:hover, &:focus {
background: $--cascader-node-background-hover;
}
}
@include when(disabled) {
color: $--cascader-node-color-disabled;
cursor: not-allowed;
}
@include e(prefix) {
position: absolute;
left: 10px;
}
@include e(postfix) {
position: absolute;
right: 10px;
}
@include e(label) {
flex: 1;
padding: 0 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> .el-checkbox {
margin-right: 0;
}
> .el-radio {
margin-right: 0;
.el-radio__label {
padding-left: 0;
}
}
}

View File

@ -1,7 +1,9 @@
@import "mixins/mixins";
@import "common/var";
@import "./input.scss";
@import "./input";
@import "./popper";
@import "./tag";
@import "./cascader-panel";
@include b(cascader) {
display: inline-block;
@ -9,76 +11,57 @@
font-size: $--font-size-base;
line-height: $--input-height;
.el-input,
.el-input__inner {
cursor: pointer;
}
.el-input.is-focus .el-input__inner {
border-color: $--input-focus-border;
}
.el-input__icon {
transition: none;
}
.el-icon-arrow-down {
transition: transform .3s;
font-size: 14px;
@include when(reverse) {
transform: rotateZ(180deg);
&:not(.is-disabled):hover {
.el-input__inner {
cursor: pointer;
border-color: $--input-hover-border;
}
}
.el-icon-circle-close {
z-index: #{$--index-normal + 1};
transition: $--color-transition-base;
&:hover {
color: $--color-text-secondary;
}
}
@include e(clearIcon) {
z-index: 2;
position: relative;
}
@include e(label) {
position: absolute;
left: 0;
top: 0;
height: 100%;
padding: 0 25px 0 15px;
color: $--cascader-menu-font-color;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
box-sizing: border-box;
.el-input {
cursor: pointer;
text-align: left;
font-size: inherit;
span {
color: $--color-black;
.el-input__inner {
text-overflow: ellipsis;
&:focus {
border-color: $--input-focus-border;
}
}
.el-icon-arrow-down {
transition: transform .3s;
font-size: 14px;
@include when(reverse) {
transform: rotateZ(180deg);
}
}
.el-icon-circle-close:hover {
color: $--input-clear-hover-color;
}
@include when(focus) {
.el-input__inner {
border-color: $--input-focus-border;
}
}
}
@include m(medium) {
font-size: $--input-medium-font-size;
line-height: #{$--input-medium-height};
line-height: $--input-medium-height;
}
@include m(small) {
font-size: $--input-small-font-size;
line-height: #{$--input-small-height};
line-height: $--input-small-height;
}
@include m(mini) {
font-size: $--input-mini-font-size;
line-height: #{$--input-mini-height};
line-height: $--input-mini-height;
}
@include when(disabled) {
@ -87,99 +70,113 @@
color: $--disabled-color-base;
}
}
}
@include b(cascader-menus) {
white-space: nowrap;
background: #fff;
position: absolute;
margin: 5px 0;
z-index: #{$--index-normal + 1};
border: $--select-dropdown-border;
border-radius: $--border-radius-small;
box-shadow: $--select-dropdown-shadow;
}
@include b(cascader-menu) {
display: inline-block;
vertical-align: top;
height: 204px;
overflow: auto;
border-right: $--select-dropdown-border;
background-color: $--select-dropdown-background;
box-sizing: border-box;
margin: 0;
padding: 6px 0;
min-width: 160px;
&:last-child {
border-right: 0;
@include e(dropdown) {
margin: 5px 0;
font-size: $--cascader-menu-font-size;
background: $--cascader-menu-fill;
border: $--cascader-menu-border;
border-radius: $--cascader-menu-radius;
box-shadow: $--cascader-menu-shadow;
}
@include e(item) {
font-size: $--select-font-size;
padding: 8px 20px;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: $--select-option-color;
height: $--select-option-height;
line-height: 1.5;
@include e(tags) {
position: absolute;
left: 0;
right: 30px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-wrap: wrap;
line-height: normal;
text-align: left;
box-sizing: border-box;
cursor: pointer;
.el-tag {
display: inline-flex;
align-items: center;
max-width: 100%;
margin: 2px 0 2px 6px;
text-overflow: ellipsis;
background: $--cascader-tag-background;
&:not(.is-hit) {
border-color: transparent;
}
> span {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.el-icon-close {
flex: none;
background-color: $--color-text-placeholder;
color: $--color-white;
&:hover {
background-color: $--color-text-secondary;
}
}
}
}
@include e(suggestion-panel) {
border-radius: $--cascader-menu-radius;
}
@include e(suggestion-list) {
max-height: 204px;
margin: 0;
padding: 6px 0;
font-size: $--font-size-base;
color: $--cascader-menu-font-color;
text-align: center;
}
@include e(suggestion-item) {
display: flex;
justify-content: space-between;
align-items: center;
height: 34px;
padding: 0 15px;
text-align: left;
outline: none;
cursor: pointer;
span {
padding-right: 10px;
&:hover, &:focus {
background: $--cascader-node-background-hover;
}
@include m(extensible) {
&:after {
font-family: 'element-icons';
content: "\e6e0";
font-size: 14px;
color: rgb(191, 203, 217);
position: absolute;
right: 15px;
}
}
@include when(disabled) {
color: $--select-option-disabled-color;
background-color: $--select-option-disabled-background;
cursor: not-allowed;
&:hover {
background-color: $--color-white;
}
}
@include when(active) {
&.is-checked {
color: $--cascader-menu-selected-font-color;
font-weight: bold;
}
&:hover, &:focus:not(:active) {
background-color: $--select-option-hover-background;
}
&.selected {
color: $--color-white;
background-color: $--select-option-selected-hover;
> span {
margin-right: 10px;
}
}
@include e(item__keyword) {
font-weight: bold;
@include e(empty-text) {
margin: 10px 0;
color: $--cascader-color-empty;
}
@include m(flexible) {
height: auto;
max-height: 180px;
overflow: auto;
@include e(search-input) {
flex: 1;
height: 24px;
min-width: 60px;
margin: 2px 0 2px 15px;
padding: 0;
color: $--cascader-menu-font-color;
border: none;
outline: none;
box-sizing: border-box;
.el-cascader-menu__item {
overflow: visible;
&::placeholder {
color: $--color-text-placeholder;
}
}
}

View File

@ -474,19 +474,14 @@ $--cascader-menu-font-color: $--color-text-regular !default;
/// color||Color|0
$--cascader-menu-selected-font-color: $--color-primary !default;
$--cascader-menu-fill: $--fill-base !default;
$--cascader-menu-border: $--border-base !default;
$--cascader-menu-border-width: $--border-width-base !default;
$--cascader-menu-color: $--color-text-regular !default;
$--cascader-menu-option-color-active: $--color-text-secondary !default;
$--cascader-menu-option-fill-active: rgba($--color-text-secondary, 0.12) !default;
$--cascader-menu-option-color-hover: $--color-text-regular !default;
$--cascader-menu-option-fill-hover: rgba($--color-text-primary, 0.06) !default;
$--cascader-menu-option-color-disabled: #999 !default;
$--cascader-menu-option-fill-disabled: rgba($--color-black, 0.06) !default;
$--cascader-menu-option-empty-color: #666 !default;
$--cascader-menu-shadow: 0 1px 2px rgba($--color-black, 0.14), 0 0 3px rgba($--color-black, 0.14) !default;
$--cascader-menu-option-pinyin-color: #999 !default;
$--cascader-menu-submenu-shadow: 1px 1px 2px rgba($--color-black, 0.14), 1px 0 2px rgba($--color-black, 0.14) !default;
$--cascader-menu-font-size: $--font-size-base !default;
$--cascader-menu-radius: $--border-radius-base !default;
$--cascader-menu-border: solid 1px $--border-color-light !default;
$--cascader-menu-shadow: $--box-shadow-light !default;
$--cascader-node-background-hover: $--background-color-base !default;
$--cascader-node-color-disabled:$--color-text-placeholder !default;
$--cascader-color-empty:$--color-text-placeholder !default;
$--cascader-tag-background: #f0f2f5;
/* Group
-------------------------- */

View File

@ -72,4 +72,6 @@
@import "./image.scss";
@import "./calendar.scss";
@import "./backtop.scss";
@import "./infiniteScroll.scss";
@import "./page-header.scss";
@import "./cascader-panel.scss";

View File

@ -78,6 +78,7 @@ import Calendar from '../packages/calendar/index.js';
import Backtop from '../packages/backtop/index.js';
import InfiniteScroll from '../packages/infiniteScroll/index.js';
import PageHeader from '../packages/page-header/index.js';
import CascaderPanel from '../packages/cascader-panel/index.js';
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
@ -155,6 +156,7 @@ const components = [
Calendar,
Backtop,
PageHeader,
CascaderPanel,
CollapseTransition
];
@ -272,5 +274,6 @@ export default {
Calendar,
Backtop,
InfiniteScroll,
PageHeader
PageHeader,
CascaderPanel
};

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Geen toepaslike data',
loading: 'Laai',
placeholder: 'Kies'
placeholder: 'Kies',
noData: 'Geen data'
},
pagination: {
goto: 'Gaan na',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'لايوجد بيانات مطابقة',
loading: 'جار التحميل',
placeholder: 'أختر'
placeholder: 'أختر',
noData: 'لايوجد بيانات'
},
pagination: {
goto: 'أذهب إلى',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Няма намерени',
loading: 'Зареждане',
placeholder: 'Избери'
placeholder: 'Избери',
noData: 'Няма данни'
},
pagination: {
goto: 'Иди на',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'No hi ha dades que coincideixin',
loading: 'Carregant',
placeholder: 'Seleccionar'
placeholder: 'Seleccionar',
noData: 'Sense Dades'
},
pagination: {
goto: 'Anar a',

View File

@ -69,7 +69,8 @@ export default {
cascader: {
noMatch: 'Žádná shoda',
loading: 'Načítání',
placeholder: 'Vybrat'
placeholder: 'Vybrat',
noData: 'Žádná data'
},
pagination: {
goto: 'Jít na',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Ingen matchende data',
loading: 'Henter',
placeholder: 'Vælg'
placeholder: 'Vælg',
noData: 'Ingen data'
},
pagination: {
goto: 'Gå til',

View File

@ -69,7 +69,8 @@ export default {
cascader: {
noMatch: 'Nichts gefunden.',
loading: 'Lädt.',
placeholder: 'Daten wählen'
placeholder: 'Daten wählen',
noData: 'Keine Daten'
},
pagination: {
goto: 'Gehe zu',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Sobivad andmed puuduvad',
loading: 'Laadimine',
placeholder: 'Vali'
placeholder: 'Vali',
noData: 'Andmed puuduvad'
},
pagination: {
goto: 'Mine lehele',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Δεν βρέθηκαν αποτελέσματα',
loading: 'Φόρτωση',
placeholder: 'Επιλογή'
placeholder: 'Επιλογή',
noData: 'Χωρίς δεδομένα'
},
pagination: {
goto: 'Μετάβαση σε',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'No matching data',
loading: 'Loading',
placeholder: 'Select'
placeholder: 'Select',
noData: 'No data'
},
pagination: {
goto: 'Go to',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'No hay datos que coincidan',
loading: 'Cargando',
placeholder: 'Seleccionar'
placeholder: 'Seleccionar',
noData: 'Sin datos'
},
pagination: {
goto: 'Ir a',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Bat datorren daturik ez',
loading: 'Kargatzen',
placeholder: 'Hautatu'
placeholder: 'Hautatu',
noData: 'Daturik ez'
},
pagination: {
goto: 'Joan',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'هیچ داده‌ای پیدا نشد',
loading: 'بارگیری',
placeholder: 'انتخاب کنید'
placeholder: 'انتخاب کنید',
noData: 'اطلاعاتی وجود ندارد'
},
pagination: {
goto: 'برو به',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Ei vastaavia tietoja',
loading: 'Lataa',
placeholder: 'Valitse'
placeholder: 'Valitse',
noData: 'Ei tietoja'
},
pagination: {
goto: 'Mene',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Aucune correspondance',
loading: 'Chargement',
placeholder: 'Choisir'
placeholder: 'Choisir',
noData: 'Aucune donnée'
},
pagination: {
goto: 'Aller à',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'ללא נתונים מתאימים',
loading: 'טוען',
placeholder: 'בחר'
placeholder: 'בחר',
noData: 'ללא נתונים'
},
pagination: {
goto: 'עבור ל',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Nema pronađenih podataka',
loading: 'Učitavanje',
placeholder: 'Izaberi'
placeholder: 'Izaberi',
noData: 'Nema podataka'
},
pagination: {
goto: 'Idi na',

View File

@ -66,7 +66,8 @@ export default {
cascader: {
noMatch: 'Nincs találat',
loading: 'Betöltés',
placeholder: 'Válassz'
placeholder: 'Válassz',
noData: 'Nincs adat'
},
pagination: {
goto: 'Ugrás',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Համապատասխան տուեալներ չկան',
loading: 'Բեռնում',
placeholder: 'Ընտրել'
placeholder: 'Ընտրել',
noData: 'Տվյալներ չկան'
},
pagination: {
goto: 'Անցնել',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Tidak ada data yg cocok',
loading: 'Memuat',
placeholder: 'Pilih'
placeholder: 'Pilih',
noData: 'Tidak ada data'
},
pagination: {
goto: 'Pergi ke',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Nessuna corrispondenza',
loading: 'Caricamento',
placeholder: 'Seleziona'
placeholder: 'Seleziona',
noData: 'Nessun dato'
},
pagination: {
goto: 'Vai a',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'データなし',
loading: 'ロード中',
placeholder: '選択してください'
placeholder: '選択してください',
noData: 'データなし'
},
pagination: {
goto: '',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Дал келген маалыматтар',
loading: 'Жүктөлүүдө',
placeholder: 'тандоо'
placeholder: 'тандоо',
noData: 'маалымат жок'
},
pagination: {
goto: 'Мурунку',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'គ្មានទិន្ន័យដូច',
loading: 'កំពុងផ្ទុក',
placeholder: 'ជ្រើសរើស'
placeholder: 'ជ្រើសរើស',
noData: 'គ្មានទិន្ន័យ'
},
pagination: {
goto: 'ទៅកាន់',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: '맞는 데이터가 없습니다',
loading: '불러오는 중',
placeholder: '선택'
placeholder: '선택',
noData: '데이터 없음'
},
pagination: {
goto: '이동',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Li hembere ve agahî tune',
loading: 'Bardibe',
placeholder: 'Bibijêre'
placeholder: 'Bibijêre',
noData: 'Agahî tune'
},
pagination: {
goto: 'Biçe',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Сәйкес деректер жоқ',
loading: 'Жүктелуде',
placeholder: 'Таңдаңыз'
placeholder: 'Таңдаңыз',
noData: 'Деректер жоқ'
},
pagination: {
goto: 'Бару',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Duomenų nerasta',
loading: 'Kraunasi',
placeholder: 'Pasirink'
placeholder: 'Pasirink',
noData: 'Nėra duomenų'
},
pagination: {
goto: 'Eiti į',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Nav atbilstošu datu',
loading: 'Ielādē',
placeholder: 'Izvēlēties'
placeholder: 'Izvēlēties',
noData: 'Nav datu'
},
pagination: {
goto: 'Iet uz',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Тохирох өгөгдөл байхгүй',
loading: 'Ачаалж байна',
placeholder: 'Сонгох'
placeholder: 'Сонгох',
noData: 'Өгөгдөл байхгүй'
},
pagination: {
goto: 'Очих',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Ingen samsvarende data',
loading: 'Laster',
placeholder: 'Velg'
placeholder: 'Velg',
noData: 'Ingen data'
},
pagination: {
goto: 'Gå til',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Geen overeenkomende resultaten',
loading: 'Laden',
placeholder: 'Selecteer'
placeholder: 'Selecteer',
noData: 'Geen data'
},
pagination: {
goto: 'Ga naar',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Brak dopasowań',
loading: 'Ładowanie',
placeholder: 'Wybierz'
placeholder: 'Wybierz',
noData: 'Brak danych'
},
pagination: {
goto: 'Idź do',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Sem resultados',
loading: 'Carregando',
placeholder: 'Selecione'
placeholder: 'Selecione',
noData: 'Sem dados'
},
pagination: {
goto: 'Ir para',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Sem correspondência',
loading: 'A carregar',
placeholder: 'Selecione'
placeholder: 'Selecione',
noData: 'Sem dados'
},
pagination: {
goto: 'Ir para',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Nu există date potrivite',
loading: 'Se încarcă',
placeholder: 'Selectează'
placeholder: 'Selectează',
noData: 'Nu există date'
},
pagination: {
goto: 'Go to',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Совпадений не найдено',
loading: 'Загрузка',
placeholder: 'Выбрать'
placeholder: 'Выбрать',
noData: 'Нет данных'
},
pagination: {
goto: 'Перейти',

View File

@ -69,7 +69,8 @@ export default {
cascader: {
noMatch: 'Žiadna zhoda',
loading: 'Načítavanie',
placeholder: 'Vybrať'
placeholder: 'Vybrať',
noData: 'Žiadne dáta'
},
pagination: {
goto: 'Choď na',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Ni ustreznih podatkov',
loading: 'Nalaganje',
placeholder: 'Izberi'
placeholder: 'Izberi',
noData: 'Ni podatkov'
},
pagination: {
goto: 'Pojdi na',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Нема резултата',
loading: 'Учитавање',
placeholder: 'Изабери'
placeholder: 'Изабери',
noData: 'Нема података'
},
pagination: {
goto: 'Иди на',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Hittade inget',
loading: 'Laddar',
placeholder: 'Välj'
placeholder: 'Välj',
noData: 'Ingen data'
},
pagination: {
goto: 'Gå till',

View File

@ -66,7 +66,8 @@ export default {
cascader: {
noMatch: 'பொருத்தமான தரவு கிடைக்கவில்லை',
loading: 'தயாராகிக்கொண்டிருக்கிறது',
placeholder: 'தேர்வு செய்'
placeholder: 'தேர்வு செய்',
noData: 'தரவு இல்லை'
},
pagination: {
goto: 'தேவையான் பகுதிக்கு செல்',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'ไม่พบข้อมูลที่ตรงกัน',
loading: 'กำลังโหลด',
placeholder: 'เลือก'
placeholder: 'เลือก',
noData: 'ไม่พบข้อมูล'
},
pagination: {
goto: 'ไปที่',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Hiçzat tapylmady',
loading: 'Indirilýär',
placeholder: 'Saýlaň'
placeholder: 'Saýlaň',
noData: 'Hiçzat ýok'
},
pagination: {
goto: 'Git',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Eşleşen veri bulunamadı',
loading: 'Yükleniyor',
placeholder: 'Seç'
placeholder: 'Seç',
noData: 'Veri yok'
},
pagination: {
goto: 'Git',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Співпадінь не знайдено',
loading: 'Завантаження',
placeholder: 'Обрати'
placeholder: 'Обрати',
noData: 'Немає даних'
},
pagination: {
goto: 'Перейти',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'ئۇچۇر تېپىلمىدى',
loading: 'يۈكلىنىۋاتىدۇ',
placeholder: 'تاللاڭ'
placeholder: 'تاللاڭ',
noData: 'ئۇچۇر يوق'
},
pagination: {
goto: 'كىيىنكى بەت',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Dữ liệu không phù hợp',
loading: 'Đang tải',
placeholder: 'Chọn'
placeholder: 'Chọn',
noData: 'Không tìm thấy dữ liệu'
},
pagination: {
goto: 'Nhảy tới',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: '无匹配数据',
loading: '加载中',
placeholder: '请选择'
placeholder: '请选择',
noData: '暂无数据'
},
pagination: {
goto: '前往',

View File

@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: '無匹配資料',
loading: '加載中',
placeholder: '請選擇'
placeholder: '請選擇',
noData: '無資料'
},
pagination: {
goto: '前往',

View File

@ -115,7 +115,8 @@ aria.Utils.keys = {
left: 37,
up: 38,
right: 39,
down: 40
down: 40,
esc: 27
};
export default aria.Utils;

View File

@ -1,4 +1,5 @@
import Vue from 'vue';
import { isString, isObject } from 'element-ui/src/utils/types';
const hasOwnProperty = Object.prototype.hasOwnProperty;
@ -143,3 +144,75 @@ export const kebabCase = function(str) {
.replace(hyphenateRE, '$1-$2')
.toLowerCase();
};
export const capitalize = function(str) {
if (!isString(str)) return str;
return str.charAt(0).toUpperCase() + str.slice(1);
};
export const looseEqual = function(a, b) {
const isObjectA = isObject(a);
const isObjectB = isObject(b);
if (isObjectA && isObjectB) {
return JSON.stringify(a) === JSON.stringify(b);
} else if (!isObjectA && !isObjectB) {
return String(a) === String(b);
} else {
return false;
}
};
export const arrayEquals = function(arrayA, arrayB) {
arrayA = arrayA || [];
arrayB = arrayB || [];
if (arrayA.length !== arrayB.length) {
return false;
}
for (let i = 0; i < arrayA.length; i++) {
if (!looseEqual(arrayA[i], arrayB[i])) {
return false;
}
}
return true;
};
export const isEqual = function(value1, value2) {
if (Array.isArray(value1) && Array.isArray(value2)) {
return arrayEquals(value1, value2);
}
return looseEqual(value1, value2);
};
export const isEmpty = function(val) {
// null or undefined
if (val == null) return true;
if (typeof val === 'boolean') return false;
if (typeof val === 'number') return !val;
if (val instanceof Error) return val.message === '';
switch (Object.prototype.toString.call(val)) {
// String or Array
case '[object String]':
case '[object Array]':
return !val.length;
// Map or Set or File
case '[object File]':
case '[object Map]':
case '[object Set]': {
return !val.size;
}
// Plain Object
case '[object Object]': {
return !Object.keys(val).length;
}
}
return false;
};

View File

@ -0,0 +1,536 @@
import {
createTest,
createVue,
destroyVM,
waitImmediate,
wait,
triggerEvent
} from '../util';
import CascaderPanel from 'packages/cascader-panel';
const selectedValue = ['zhejiang', 'hangzhou', 'xihu'];
const options = [{
value: 'zhejiang',
label: 'Zhejiang',
children: [{
value: 'hangzhou',
label: 'Hangzhou',
children: [{
value: 'xihu',
label: 'West Lake'
}, {
value: 'binjiang',
label: 'Bin Jiang'
}]
}, {
value: 'ningbo',
label: 'NingBo',
children: [{
value: 'jiangbei',
label: 'Jiang Bei'
}, {
value: 'jiangdong',
label: 'Jiang Dong',
disabled: true
}]
}]
}, {
value: 'jiangsu',
label: 'Jiangsu',
disabled: true,
children: [{
value: 'nanjing',
label: 'Nanjing',
children: [{
value: 'zhonghuamen',
label: 'Zhong Hua Men'
}]
}]
}];
const options2 = [{
id: 'zhejiang',
name: 'Zhejiang',
areas: [{
id: 'hangzhou',
name: 'Hangzhou',
areas: [{
id: 'xihu',
name: 'West Lake'
}, {
id: 'binjiang',
name: 'Bin Jiang'
}]
}, {
id: 'ningbo',
name: 'NingBo',
areas: [{
id: 'jiangbei',
label: 'Jiang Bei'
}, {
id: 'jiangdong',
name: 'Jiang Dong',
invalid: true
}]
}]
}, {
id: 'jiangsu',
name: 'Jiangsu',
invalid: true,
areas: [{
id: 'nanjing',
name: 'Nanjing',
areas: [{
id: 'zhonghuamen',
name: 'Zhong Hua Men'
}]
}]
}];
const getMenus = el => el.querySelectorAll('.el-cascader-menu');
const getOptions = (el, menuIndex) => getMenus(el)[menuIndex].querySelectorAll('.el-cascader-node');
const getValidOptions = (el, menuIndex) => getMenus(el)[menuIndex].querySelectorAll('.el-cascader-node[tabindex="-1"]');
const getLabel = el => el.querySelector('.el-cascader-node__label').textContent;
describe('CascaderPanel', () => {
let vm;
afterEach(() => {
destroyVM(vm);
});
it('create', () => {
vm = createTest(CascaderPanel, true);
expect(vm.$el).to.exist;
});
it('expand and check', async() => {
vm = createVue({
template: `
<el-cascader-panel
ref="panel"
v-model="value"
:options="options"></el-cascader-panel>
`,
data() {
return {
value: [],
options
};
}
}, true);
const el = vm.$el;
const expandHandler = sinon.spy();
const changeHandler = sinon.spy();
vm.$refs.panel.$on('expand-change', expandHandler);
vm.$refs.panel.$on('change', changeHandler);
expect(getMenus(el).length).to.equal(1);
expect(getOptions(el, 0).length).to.equal(2);
const firstOption = getOptions(el, 0)[0];
expect(getLabel(firstOption)).to.equal('Zhejiang');
firstOption.click();
await waitImmediate();
expect(expandHandler.calledOnceWith(['zhejiang'])).to.be.true;
expect(getMenus(el).length).to.equal(2);
getOptions(el, 1)[0].click();
await waitImmediate();
expect(getMenus(el).length).to.equal(3);
getOptions(el, 2)[0].click();
await waitImmediate();
expect(changeHandler.calledOnceWith(selectedValue)).to.be.true;
expect(vm.value).to.deep.equal(selectedValue);
});
it('with default value', async() => {
vm = createVue({
template: `
<el-cascader-panel
ref="panel"
v-model="value"
:options="options"></el-cascader-panel>
`,
data() {
return {
value: selectedValue,
options
};
}
}, true);
const el = vm.$el;
await waitImmediate();
expect(getMenus(el).length).to.equal(3);
expect(getOptions(el, 0)[0].className).to.includes('in-active-path');
expect(getOptions(el, 2)[0].className).to.includes('is-active');
expect(getOptions(el, 2)[0].querySelector('.el-icon-check')).to.exist;
});
it('disabled options', async() => {
vm = createVue({
template: `
<el-cascader-panel
ref="panel"
:value="value"
:options="options"></el-cascader-panel>
`,
data() {
return {
value: [],
options
};
}
}, true);
const el = vm.$el;
const expandHandler = sinon.spy();
vm.$refs.panel.$on('expand-change', expandHandler);
expect(getOptions(el, 0).length).to.equal(2);
expect(getValidOptions(el, 0).length).to.equal(1);
const secondOption = getOptions(el, 0)[1];
expect(secondOption.className).to.includes('is-disabled');
secondOption.click();
await waitImmediate();
expect(expandHandler.called).to.be.false;
expect(getMenus(el).length).to.equal(1);
});
it('expand by hover', async() => {
vm = createVue({
template: `
<el-cascader-panel
:options="options"
:props="props"></el-cascader-panel>
`,
data() {
return {
options,
props: {
expandTrigger: 'hover'
}
};
}
}, true);
const el = vm.$el;
triggerEvent(getOptions(el, 0)[1], 'mouseenter');
await waitImmediate();
expect(getMenus(el).length).to.equal(1);
triggerEvent(getOptions(el, 0)[0], 'mouseenter');
await waitImmediate();
expect(getMenus(el).length).to.equal(2);
triggerEvent(getOptions(el, 1)[0], 'mouseenter');
await waitImmediate();
expect(getMenus(el).length).to.equal(3);
});
it('emit value only', async() => {
vm = createVue({
template: `
<el-cascader-panel
ref="panel"
v-model="value"
:options="options"
:props="props"></el-cascader-panel>
`,
data() {
return {
value: 'xihu',
options,
props: {
emitPath: false
}
};
}
}, true);
const el = vm.$el;
await waitImmediate();
expect(getMenus(el).length).to.equal(3);
expect(getOptions(el, 2)[0].querySelector('.el-icon-check')).to.exist;
getOptions(el, 1)[1].click();
await waitImmediate();
getOptions(el, 2)[0].click();
await waitImmediate();
expect(vm.value).to.equal('jiangbei');
});
it('multiple mode', async() => {
vm = createVue({
template: `
<el-cascader-panel
v-model="value"
:options="options"
:props="props"></el-cascader-panel>
`,
data() {
return {
value: [],
options: options,
props: {
multiple: true
}
};
}
}, true);
const el = vm.$el;
const checkbox = getOptions(el, 0)[0].querySelector('.el-checkbox');
expect(checkbox).to.exist;
expect(checkbox.querySelector('.el-checkbox__input').className).to.not.includes('is-checked');
checkbox.querySelector('input').click();
await waitImmediate();
expect(checkbox.querySelector('.el-checkbox__input').className).to.includes('is-checked');
expect(vm.value.length).to.equal(3);
});
it('multiple mode with disabled default value', async() => {
vm = createVue({
template: `
<el-cascader-panel
v-model="value"
:options="options"
:props="props"></el-cascader-panel>
`,
data() {
return {
value: [['zhejiang', 'ningbo', 'jiangdong']],
options: options,
props: {
multiple: true
}
};
}
}, true);
const el = vm.$el;
const checkbox = getOptions(el, 0)[0].querySelector('.el-checkbox');
await waitImmediate();
expect(checkbox).to.exist;
expect(checkbox.querySelector('.el-checkbox__input').className).to.includes('is-indeterminate');
checkbox.querySelector('input').click();
await waitImmediate();
expect(checkbox.querySelector('.el-checkbox__input').className).to.includes('is-checked');
expect(vm.value.length).to.equal(4);
getOptions(el, 1)[1].click();
await waitImmediate();
getOptions(el, 2)[1].querySelector('input').click();
await waitImmediate();
expect(vm.value.length).to.equal(4);
});
it('check strictly in single mode', async() => {
vm = createVue({
template: `
<el-cascader-panel
v-model="value"
:options="options"
:props="props"></el-cascader-panel>
`,
data() {
return {
value: ['zhejiang'],
options: options,
props: {
checkStrictly: true
}
};
}
}, true);
const el = vm.$el;
const radio = getOptions(el, 0)[0].querySelector('.el-radio');
await waitImmediate();
expect(radio).to.exist;
expect(radio.className).to.includes('is-checked');
getOptions(el, 0)[0].click();
await waitImmediate();
getOptions(el, 1)[0].querySelector('input').click();
await waitImmediate();
expect(vm.value).to.deep.equal(['zhejiang', 'hangzhou']);
expect(getOptions(el, 0)[1].querySelector('.el-radio').className).to.includes('is-disabled');
});
it('check strictly in multiple mode', async() => {
vm = createVue({
template: `
<el-cascader-panel
v-model="value"
:options="options"
:props="props"></el-cascader-panel>
`,
data() {
return {
value: [['zhejiang']],
options: options,
props: {
multiple: true,
checkStrictly: true,
emitPath: false
}
};
}
}, true);
const el = vm.$el;
const checkbox = getOptions(el, 0)[0].querySelector('.el-checkbox');
await waitImmediate();
expect(checkbox).to.exist;
expect(checkbox.className).to.includes('is-checked');
getOptions(el, 0)[0].click();
await waitImmediate();
expect(getOptions(el, 1)[0].querySelector('.el-checkbox').className).to.not.includes('is-checked');
getOptions(el, 1)[0].querySelector('input').click();
await waitImmediate();
expect(vm.value).to.deep.equal(['zhejiang', 'hangzhou']);
expect(getOptions(el, 0)[1].querySelector('.el-checkbox').className).to.includes('is-disabled');
});
it('custom props', async() => {
vm = createVue({
template: `
<el-cascader-panel
v-model="value"
:options="options"
:props="props"></el-cascader-panel>
`,
data() {
return {
value: [],
options: options2,
props: {
value: 'id',
label: 'name',
children: 'areas',
disabled: 'invalid'
}
};
}
}, true);
const el = vm.$el;
expect(getMenus(el).length).to.equal(1);
expect(getOptions(el, 0).length).to.equal(2);
expect(getValidOptions(el, 0).length).to.equal(1);
const firstOption = getOptions(el, 0)[0];
expect(getLabel(firstOption)).to.equal('Zhejiang');
firstOption.click();
await waitImmediate();
expect(getMenus(el).length).to.equal(2);
getOptions(el, 1)[0].click();
await waitImmediate();
expect(getMenus(el).length).to.equal(3);
getOptions(el, 2)[0].click();
await waitImmediate();
expect(vm.value).to.deep.equal(selectedValue);
});
it('value key is same as label key', async() => {
vm = createVue({
template: `
<el-cascader-panel
v-model="value"
:options="options"
:props="props"></el-cascader-panel>
`,
data() {
return {
value: [],
options,
props: {
label: 'value'
}
};
}
}, true);
const el = vm.$el;
expect(getMenus(el).length).to.equal(1);
expect(getOptions(el, 0).length).to.equal(2);
expect(getValidOptions(el, 0).length).to.equal(1);
const firstOption = getOptions(el, 0)[0];
expect(getLabel(firstOption)).to.equal('zhejiang');
firstOption.click();
await waitImmediate();
expect(getMenus(el).length).to.equal(2);
getOptions(el, 1)[0].click();
await waitImmediate();
expect(getMenus(el).length).to.equal(3);
getOptions(el, 2)[0].click();
await waitImmediate();
expect(vm.value).to.deep.equal(selectedValue);
});
it('dynamic loading', async() => {
vm = createVue({
template: `
<el-cascader-panel
v-model="value"
:props="props"></el-cascader-panel>
`,
data() {
let id = 0;
return {
value: [],
props: {
lazy: true,
lazyLoad(node, resolve) {
const { level } = node;
setTimeout(() => {
const nodes = Array.from({ length: level + 1 })
.map(() => ({
value: ++id,
label: `选项${id}`,
leaf: level >= 2
}));
resolve(nodes);
}, 1000);
}
}
};
}
}, true);
const el = vm.$el;
await wait(1000);
const firstOption = getOptions(el, 0)[0];
firstOption.click();
await waitImmediate();
expect(firstOption.querySelector('i').className).to.includes('el-icon-loading');
await wait(1000);
expect(firstOption.querySelector('i').className).to.includes('el-icon-arrow-right');
expect(getMenus(el).length).to.equal(2);
getOptions(el, 1)[0].click();
await wait(1000);
getOptions(el, 2)[0].click();
await waitImmediate();
expect(vm.value.length).to.equal(3);
});
});

File diff suppressed because it is too large Load Diff

72
types/cascader-panel.d.ts vendored Normal file
View File

@ -0,0 +1,72 @@
import { VNode, CreateElement } from 'vue';
import { ElementUIComponent } from './component'
/** Trigger mode of expanding current item */
export type ExpandTrigger = 'click' | 'hover'
/** Cascader Option */
export interface CascaderOption {
label: string,
value: any,
children?: CascaderOption[],
disabled?: boolean,
leaf?: boolean
}
/** Cascader Props */
export interface CascaderProps<V, D> {
expandTrigger?: ExpandTrigger,
multiple?: boolean,
checkStrictly?: boolean,
emitPath?: boolean,
lazy?: boolean,
lazyLoad?: (node: CascaderNode<V, D>, resolve: Resolve<D>) => void,
value?: string,
label?: string,
children?: string,
disabled?: string
leaf?: string
}
/** Cascader Node */
export interface CascaderNode<V, D> {
uid: number,
data: D,
value: V,
label: string,
level: number,
isDisabled: boolean,
isLeaf: boolean,
parent: CascaderNode<V, D> | null,
children: CascaderNode<V, D>[]
config: CascaderProps<V, D>
}
type Resolve<D> = (dataList?: D[]) => void
export interface CascaderPanelSlots {
/** Custom label content */
default: VNode[]
[key: string]: VNode[]
}
/** CascaderPanel Component */
export declare class ElCascaderPanel<V = any, D = CascaderOption> extends ElementUIComponent {
/** Selected value */
value: V | V[]
/** Data of the options */
options: D[]
/** Configuration options */
props: CascaderProps<V, D>
/** Whether to add border */
border: boolean
/** Render function of custom label content */
renderLabel: (h: CreateElement, context: { node: CascaderNode<V, D>; data: D }) => VNode
$slots: CascaderPanelSlots
}

54
types/cascader.d.ts vendored
View File

@ -1,29 +1,36 @@
import { VNode } from 'vue';
import { ElementUIComponent, ElementUIComponentSize } from './component'
import { CascaderOption, CascaderProps, CascaderNode } from './cascader-panel';
/** Trigger mode of expanding current item */
export type ExpandTrigger = 'click' | 'hover'
export type CascaderOption = CascaderOption
/** Cascader Option */
export interface CascaderOption {
label: string,
value: any,
children?: CascaderOption[],
disabled?: boolean
export type CascaderProps<V, D> = CascaderProps<V, D>
export type CascaderNode<V, D> = CascaderNode<V, D>
export interface CascaderSlots {
/** Custom label content */
default: VNode[],
/** Empty content when no option matches */
empty: VNode[]
[key: string]: VNode[]
}
/** Cascader Component */
export declare class ElCascader extends ElementUIComponent {
export declare class ElCascader<V = any, D = CascaderOption> extends ElementUIComponent {
/** Data of the options */
options: CascaderOption[]
/** Configuration options */
props: object
props: CascaderProps<V, D>
/** Selected value */
value: any[]
value: V | V[]
/** Custom class name for Cascader's dropdown */
popperClass: string
/** Size of Input */
size: ElementUIComponentSize
/** Input placeholder */
placeholder: string
@ -34,24 +41,29 @@ export declare class ElCascader extends ElementUIComponent {
/** Whether selected value can be cleared */
clearable: boolean
/** Trigger mode of expanding current item */
expandTrigger: ExpandTrigger
/** Whether to display all levels of the selected value in the input */
showAllLevels: boolean
/** Whether to collapse selected tags in multiple selection mode */
collapseTags: boolean
/** Separator of option labels */
separator: string
/** Whether the options can be searched */
filterable: boolean
/** filter method to match options according to input keyword */
filterMethod: (node: CascaderNode<V, D>, keyword: string) => boolean
/** Debounce delay when typing filter keyword, in millisecond */
debounce: number
/** Whether selecting an option of any level is permitted */
changeOnSelect: boolean
/** Size of Input */
size: ElementUIComponentSize
/** Custom class name for Cascader's dropdown */
popperClass: string
/** Hook function before filtering with the value to be filtered as its parameter */
beforeFilter: (value: string) => boolean | Promise<any>
$slots: CascaderSlots
}