mirror of https://github.com/ElemeFE/element
Cascader: refactor and add multiple mode. (#15611)
parent
b245929242
commit
bdaae8108e
|
@ -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
|
@ -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;
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import Cascader from './src/main';
|
||||
import Cascader from './src/cascader';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Cascader.install = function(Vue) {
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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 选择子menu的第一个menuitem
|
||||
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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
-------------------------- */
|
||||
|
|
|
@ -72,4 +72,6 @@
|
|||
@import "./image.scss";
|
||||
@import "./calendar.scss";
|
||||
@import "./backtop.scss";
|
||||
@import "./infiniteScroll.scss";
|
||||
@import "./page-header.scss";
|
||||
@import "./cascader-panel.scss";
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Geen toepaslike data',
|
||||
loading: 'Laai',
|
||||
placeholder: 'Kies'
|
||||
placeholder: 'Kies',
|
||||
noData: 'Geen data'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Gaan na',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'لايوجد بيانات مطابقة',
|
||||
loading: 'جار التحميل',
|
||||
placeholder: 'أختر'
|
||||
placeholder: 'أختر',
|
||||
noData: 'لايوجد بيانات'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'أذهب إلى',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Няма намерени',
|
||||
loading: 'Зареждане',
|
||||
placeholder: 'Избери'
|
||||
placeholder: 'Избери',
|
||||
noData: 'Няма данни'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Иди на',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Sobivad andmed puuduvad',
|
||||
loading: 'Laadimine',
|
||||
placeholder: 'Vali'
|
||||
placeholder: 'Vali',
|
||||
noData: 'Andmed puuduvad'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Mine lehele',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Δεν βρέθηκαν αποτελέσματα',
|
||||
loading: 'Φόρτωση',
|
||||
placeholder: 'Επιλογή'
|
||||
placeholder: 'Επιλογή',
|
||||
noData: 'Χωρίς δεδομένα'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Μετάβαση σε',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'No matching data',
|
||||
loading: 'Loading',
|
||||
placeholder: 'Select'
|
||||
placeholder: 'Select',
|
||||
noData: 'No data'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Go to',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Bat datorren daturik ez',
|
||||
loading: 'Kargatzen',
|
||||
placeholder: 'Hautatu'
|
||||
placeholder: 'Hautatu',
|
||||
noData: 'Daturik ez'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Joan',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'هیچ دادهای پیدا نشد',
|
||||
loading: 'بارگیری',
|
||||
placeholder: 'انتخاب کنید'
|
||||
placeholder: 'انتخاب کنید',
|
||||
noData: 'اطلاعاتی وجود ندارد'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'برو به',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Ei vastaavia tietoja',
|
||||
loading: 'Lataa',
|
||||
placeholder: 'Valitse'
|
||||
placeholder: 'Valitse',
|
||||
noData: 'Ei tietoja'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Mene',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Aucune correspondance',
|
||||
loading: 'Chargement',
|
||||
placeholder: 'Choisir'
|
||||
placeholder: 'Choisir',
|
||||
noData: 'Aucune donnée'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Aller à',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'ללא נתונים מתאימים',
|
||||
loading: 'טוען',
|
||||
placeholder: 'בחר'
|
||||
placeholder: 'בחר',
|
||||
noData: 'ללא נתונים'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'עבור ל',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Համապատասխան տուեալներ չկան',
|
||||
loading: 'Բեռնում',
|
||||
placeholder: 'Ընտրել'
|
||||
placeholder: 'Ընտրել',
|
||||
noData: 'Տվյալներ չկան'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Անցնել',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Nessuna corrispondenza',
|
||||
loading: 'Caricamento',
|
||||
placeholder: 'Seleziona'
|
||||
placeholder: 'Seleziona',
|
||||
noData: 'Nessun dato'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Vai a',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'データなし',
|
||||
loading: 'ロード中',
|
||||
placeholder: '選択してください'
|
||||
placeholder: '選択してください',
|
||||
noData: 'データなし'
|
||||
},
|
||||
pagination: {
|
||||
goto: '',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Дал келген маалыматтар',
|
||||
loading: 'Жүктөлүүдө',
|
||||
placeholder: 'тандоо'
|
||||
placeholder: 'тандоо',
|
||||
noData: 'маалымат жок'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Мурунку',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'គ្មានទិន្ន័យដូច',
|
||||
loading: 'កំពុងផ្ទុក',
|
||||
placeholder: 'ជ្រើសរើស'
|
||||
placeholder: 'ជ្រើសរើស',
|
||||
noData: 'គ្មានទិន្ន័យ'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'ទៅកាន់',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: '맞는 데이터가 없습니다',
|
||||
loading: '불러오는 중',
|
||||
placeholder: '선택'
|
||||
placeholder: '선택',
|
||||
noData: '데이터 없음'
|
||||
},
|
||||
pagination: {
|
||||
goto: '이동',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Сәйкес деректер жоқ',
|
||||
loading: 'Жүктелуде',
|
||||
placeholder: 'Таңдаңыз'
|
||||
placeholder: 'Таңдаңыз',
|
||||
noData: 'Деректер жоқ'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Бару',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Duomenų nerasta',
|
||||
loading: 'Kraunasi',
|
||||
placeholder: 'Pasirink'
|
||||
placeholder: 'Pasirink',
|
||||
noData: 'Nėra duomenų'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Eiti į',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Тохирох өгөгдөл байхгүй',
|
||||
loading: 'Ачаалж байна',
|
||||
placeholder: 'Сонгох'
|
||||
placeholder: 'Сонгох',
|
||||
noData: 'Өгөгдөл байхгүй'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Очих',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Ingen samsvarende data',
|
||||
loading: 'Laster',
|
||||
placeholder: 'Velg'
|
||||
placeholder: 'Velg',
|
||||
noData: 'Ingen data'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Gå til',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Geen overeenkomende resultaten',
|
||||
loading: 'Laden',
|
||||
placeholder: 'Selecteer'
|
||||
placeholder: 'Selecteer',
|
||||
noData: 'Geen data'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Ga naar',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Brak dopasowań',
|
||||
loading: 'Ładowanie',
|
||||
placeholder: 'Wybierz'
|
||||
placeholder: 'Wybierz',
|
||||
noData: 'Brak danych'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Idź do',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Sem resultados',
|
||||
loading: 'Carregando',
|
||||
placeholder: 'Selecione'
|
||||
placeholder: 'Selecione',
|
||||
noData: 'Sem dados'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Ir para',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Совпадений не найдено',
|
||||
loading: 'Загрузка',
|
||||
placeholder: 'Выбрать'
|
||||
placeholder: 'Выбрать',
|
||||
noData: 'Нет данных'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Перейти',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Ni ustreznih podatkov',
|
||||
loading: 'Nalaganje',
|
||||
placeholder: 'Izberi'
|
||||
placeholder: 'Izberi',
|
||||
noData: 'Ni podatkov'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Pojdi na',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Нема резултата',
|
||||
loading: 'Учитавање',
|
||||
placeholder: 'Изабери'
|
||||
placeholder: 'Изабери',
|
||||
noData: 'Нема података'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Иди на',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -66,7 +66,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'பொருத்தமான தரவு கிடைக்கவில்லை',
|
||||
loading: 'தயாராகிக்கொண்டிருக்கிறது',
|
||||
placeholder: 'தேர்வு செய்'
|
||||
placeholder: 'தேர்வு செய்',
|
||||
noData: 'தரவு இல்லை'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'தேவையான் பகுதிக்கு செல்',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'ไม่พบข้อมูลที่ตรงกัน',
|
||||
loading: 'กำลังโหลด',
|
||||
placeholder: 'เลือก'
|
||||
placeholder: 'เลือก',
|
||||
noData: 'ไม่พบข้อมูล'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'ไปที่',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'Співпадінь не знайдено',
|
||||
loading: 'Завантаження',
|
||||
placeholder: 'Обрати'
|
||||
placeholder: 'Обрати',
|
||||
noData: 'Немає даних'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'Перейти',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: 'ئۇچۇر تېپىلمىدى',
|
||||
loading: 'يۈكلىنىۋاتىدۇ',
|
||||
placeholder: 'تاللاڭ'
|
||||
placeholder: 'تاللاڭ',
|
||||
noData: 'ئۇچۇر يوق'
|
||||
},
|
||||
pagination: {
|
||||
goto: 'كىيىنكى بەت',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: '无匹配数据',
|
||||
loading: '加载中',
|
||||
placeholder: '请选择'
|
||||
placeholder: '请选择',
|
||||
noData: '暂无数据'
|
||||
},
|
||||
pagination: {
|
||||
goto: '前往',
|
||||
|
|
|
@ -67,7 +67,8 @@ export default {
|
|||
cascader: {
|
||||
noMatch: '無匹配資料',
|
||||
loading: '加載中',
|
||||
placeholder: '請選擇'
|
||||
placeholder: '請選擇',
|
||||
noData: '無資料'
|
||||
},
|
||||
pagination: {
|
||||
goto: '前往',
|
||||
|
|
|
@ -115,7 +115,8 @@ aria.Utils.keys = {
|
|||
left: 37,
|
||||
up: 38,
|
||||
right: 39,
|
||||
down: 40
|
||||
down: 40,
|
||||
esc: 27
|
||||
};
|
||||
|
||||
export default aria.Utils;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue