<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: [], loadCount: 0 }; }, 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: { value() { this.syncCheckedValue(); this.checkStrictly && this.calculateCheckedNodePaths(); }, options: { handler: function() { this.initStore(); }, immediate: true, deep: true }, checkedValue(val) { if (!isEqual(val, this.value)) { this.checkStrictly && this.calculateCheckedNodePaths(); this.$emit('input', val); this.$emit('change', val); } } }, mounted() { if (!this.isEmptyValue(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.activePath = []; 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); }); }, isEmptyValue(val) { const { multiple, config } = this; const { emitPath } = config; if (multiple || emitPath) { return isEmpty(val); } return false; }, syncActivePath() { const { store, multiple, activePath, checkedValue } = this; if (!isEmpty(activePath)) { const nodes = activePath.map(node => this.getNodeByValue(node.getValue())); this.expandNodes(nodes); } else if (!this.isEmptyValue(checkedValue)) { const value = multiple ? checkedValue[0] : checkedValue; const checkedNode = this.getNodeByValue(value) || {}; const nodes = (checkedNode.pathNodes || []).slice(0, -1); this.expandNodes(nodes); } else { this.activePath = []; this.menus = [store.getNodes()]; } }, expandNodes(nodes) { 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 { activePath } = this; const { level } = node; const path = activePath.slice(0, level - 1); const menus = this.menus.slice(0, level); if (!node.isLeaf) { path.push(node); menus.push(node.children); } this.activePath = path; this.menus = menus; if (!silent) { const pathValues = path.map(node => node.getValue()); const activePathValues = activePath.map(node => node.getValue()); if (!valueEquals(pathValues, activePathValues)) { 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; // dispose default value on lazy load mode if (Array.isArray(this.checkedValue)) { const nodeValue = this.checkedValue[this.loadCount++]; const valueKey = this.config.value; const leafKey = this.config.leaf; if (Array.isArray(dataList) && dataList.filter(item => item[valueKey] === nodeValue).length > 0) { const checkedNode = this.store.getNodeByValue(nodeValue); if (!checkedNode.data[leafKey]) { this.lazyLoad(checkedNode, () => { this.handleExpand(checkedNode); }); } if (this.loadCount === this.checkedValue.length) { this.$parent.computePresentText(); } } } 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 this.isEmptyValue(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>