diff --git a/components/cascader-panel/cascaderPanel.vue b/components/cascader-panel/cascaderPanel.vue index dfd23f8d..b070c0e2 100644 --- a/components/cascader-panel/cascaderPanel.vue +++ b/components/cascader-panel/cascaderPanel.vue @@ -19,6 +19,7 @@ import { } from './const'; import usePanel from './usePanel'; import CascaderMenu from './menu'; +import PROPS from './props'; const prefixCls = getPrefixCls('cascader-panel'); @@ -28,21 +29,7 @@ export default defineComponent({ CascaderMenu, }, props: { - currentValue: [Number, String, Array, Object], - options: { - type: Array, - default: () => [], - }, - multiple: Boolean, - nodeConfig: { - type: Object, - default: () => {}, - }, - renderLabel: Function, - handleUpdateSelectedNodes: { - type: Function, - required: true, - }, + ...PROPS, }, emits: ['expandChange', 'checkChange', 'close'], setup(props, { emit, slots }) { diff --git a/components/cascader-panel/const.js b/components/cascader-panel/const.js index 1f563334..20f029b3 100644 --- a/components/cascader-panel/const.js +++ b/components/cascader-panel/const.js @@ -23,3 +23,9 @@ export const EVENT_CODE = { DOWN: 'ArrowDown', // 40 ESC: 'Escape', }; + +export const CHECK_STRATEGY = { + ALL: 'all', + PARENT: 'parent', + CHILD: 'child', +}; diff --git a/components/cascader-panel/nodeContent.jsx b/components/cascader-panel/nodeContent.jsx index 3238917d..a05fa5f1 100644 --- a/components/cascader-panel/nodeContent.jsx +++ b/components/cascader-panel/nodeContent.jsx @@ -1,4 +1,4 @@ -import { defineComponent, h, inject } from 'vue'; +import { computed, defineComponent, inject } from 'vue'; import getPrefixCls from '../_util/getPrefixCls'; import { CASCADER_PANEL_INJECTION_KEY } from './const'; @@ -18,16 +18,15 @@ export default defineComponent({ const { data, label } = node; const { renderLabelFn } = panel; - return () => - h( - 'span', - { - class: { - [prefixCls]: true, - 'is-multiple': panel.multiple, - }, - }, - renderLabelFn ? renderLabelFn({ node, data }) : label, - ); + const classes = computed(() => ({ + [prefixCls]: true, + 'is-multiple': panel.multiple, + })); + + return () => ( + + {renderLabelFn ? renderLabelFn({ node, data }) : label} + + ); }, }); diff --git a/components/cascader-panel/props.js b/components/cascader-panel/props.js new file mode 100644 index 00000000..6c0c6c6f --- /dev/null +++ b/components/cascader-panel/props.js @@ -0,0 +1,33 @@ +import { CHECK_STRATEGY } from './const'; + +const PROPS = { + currentValue: [Number, String, Array, Object], + options: { + type: Array, + default: () => [], + }, + multiple: Boolean, + nodeConfig: { + type: Object, + default: () => {}, + }, + renderLabel: Function, + handleUpdateSelectedNodes: Function, + showAllLevels: { + type: Boolean, + default: true, + }, + separator: { + type: String, + default: ' / ', + }, + checkStrictly: { + type: String, + default: CHECK_STRATEGY.CHILD, + validator(value) { + return Object.values(CHECK_STRATEGY).includes(value); + }, + }, +}; + +export default PROPS; diff --git a/components/cascader-panel/useNode.js b/components/cascader-panel/useNode.js index 60001b39..76ee3ceb 100644 --- a/components/cascader-panel/useNode.js +++ b/components/cascader-panel/useNode.js @@ -51,7 +51,7 @@ function setNodeElem(node, elem) { node.elem = elem; } -function useSelectedNodes(config, props, nodes) { +function useSelectedNodes(config, props, allNodes) { const selectedNodes = computed(() => { const { emitPath } = config.value; const { currentValue, multiple } = props; @@ -66,7 +66,7 @@ function useSelectedNodes(config, props, nodes) { currentValue, ); - const node = getNodeByValue(nodes.value, nodeValue); + const node = getNodeByValue(allNodes.value, nodeValue); if (node) { currentSelectedNodes.push(node); @@ -79,10 +79,9 @@ function useSelectedNodes(config, props, nodes) { emitPath, currentValue, ); - nodeValues.forEach((nodeValue) => { - const node = getNodeByValue(nodes.value, nodeValue); - if (node) { - currentSelectedNodes.push(node); + allNodes.value.forEach((item) => { + if (nodeValues.includes(item.value)) { + currentSelectedNodes.push(item); } }); } @@ -101,7 +100,7 @@ export default (config, props) => { props, ); - const { selectedNodes } = useSelectedNodes(config, props, nodes); + const { selectedNodes } = useSelectedNodes(config, props, allNodes); return { nodes, diff --git a/components/cascader-panel/usePanel.js b/components/cascader-panel/usePanel.js index 3ffbbbd4..2155e089 100644 --- a/components/cascader-panel/usePanel.js +++ b/components/cascader-panel/usePanel.js @@ -1,9 +1,10 @@ import { ref, watch } from 'vue'; import { flatNodes } from '../_util/utils'; -import { EVENT_CODE, EXPAND_TRIGGER } from './const'; +import { CHECK_STRATEGY, EVENT_CODE, EXPAND_TRIGGER } from './const'; import useNode from './useNode'; import { updateParentNodesCheckState, + updateChildNodesCheckState, getValueByOption, getNodeSibling, focusNodeElem, @@ -11,6 +12,7 @@ import { getMenuIndexByElem, checkNodeElem, generateId, + getCheckNodesByLeafCheckNodes, } from './utils'; function useUpdateNodes(props, selectedNodes, allNodes) { @@ -25,15 +27,22 @@ function useUpdateNodes(props, selectedNodes, allNodes) { const doUpdateNodes = () => { clearCheckedNodes(); - const { multiple, handleUpdateSelectedNodes } = props; + const { multiple, handleUpdateSelectedNodes, checkStrictly } = props; selectedNodes.value.forEach((node) => { node.checked = true; }); + /** + * 多选情况,更新节点选中状态 + * 1. 根据选中节点,更新父节点的中间状态和选中状态 + * 1. 若为 checkStrictly = parent 情况,则需要更新子节点的选中状态 + */ if (multiple) { - // 更新中间状态 updateParentNodesCheckState(selectedNodes.value); + if (checkStrictly === CHECK_STRATEGY.PARENT) { + updateChildNodesCheckState(selectedNodes.value); + } } handleUpdateSelectedNodes(selectedNodes.value); @@ -92,7 +101,14 @@ function useExpandNode(menus, emit, updateMenus) { }; } -function useCheckChange(config, props, emit, selectedNodes, leafNodes) { +function useCheckChange( + config, + props, + emit, + selectedNodes, + leafNodes, + allNodes, +) { const handleCheckChange = (node, checked) => { if (node.checked === checked) return; const { multiple } = props; @@ -104,33 +120,58 @@ function useCheckChange(config, props, emit, selectedNodes, leafNodes) { } else { /** * 多选 - * 1. 解析得到节点值的列表(目前为最后一级节点的值的列表) - * 2. 解析得到当前节点下所有子孙节点的值的列表 - * 3. 若为选中情况,则遍历子孙节点值,判断当前值是否存在,若不存在,则插入 - * 4. 若为取消选中情况,则遍历子孙节点值,判断当前值是否存在,若存在,则删除 + * 解析得到叶子节点列表 + * 1. 解析得到当前节点下所有叶子节点的值的列表 + * 2. 若为选中情况,则遍历子孙节点值,判断当前值是否存在,若不存在,则插入 + * 3. 若为取消选中情况,则遍历子孙节点值,判断当前值是否存在,若存在,则删除 */ - const nodeValues = selectedNodes.value.map((item) => item.value); - + // 因为 selectedNodes 可能父子节点都有的情况,所以需要做下去重处理 + const leafNodeValues = flatNodes(selectedNodes.value, true).reduce( + (prev, cur) => + prev.includes(cur.value) ? prev : [...prev, cur.value], + [], + ); // 获取当前节点的子孙节点信息 const checkNodes = flatNodes([node], true); checkNodes.forEach((item) => { if (checked) { - if (!nodeValues.includes(item.value)) { - nodeValues.push(item.value); + if (!leafNodeValues.includes(item.value)) { + leafNodeValues.push(item.value); } - } else if (nodeValues.includes(item.value)) { - nodeValues.splice(nodeValues.indexOf(item.value), 1); + } else if (leafNodeValues.includes(item.value)) { + leafNodeValues.splice( + leafNodeValues.indexOf(item.value), + 1, + ); } }); // 目前值为最后一级节点值,所以仅需过滤子节点即可 - const filterNodes = leafNodes.value.filter((item) => - nodeValues.includes(item.value), + const sortLeafNodes = leafNodes.value.filter((item) => + leafNodeValues.includes(item.value), ); - const sortValues = filterNodes.map((item) => - getValueByOption(config.value, item), - ); - emit('checkChange', sortValues); + + let checkValues = []; + + if (props.checkStrictly === CHECK_STRATEGY.CHILD) { + const sortLeafValues = sortLeafNodes.map((item) => + getValueByOption(config.value, item), + ); + checkValues = sortLeafValues; + } else { + const checkParentNodes = getCheckNodesByLeafCheckNodes( + sortLeafNodes, + allNodes.value, + props.checkStrictly, + ); + const checkParentValus = checkParentNodes.map((item) => + getValueByOption(config.value, item), + ); + + checkValues = checkParentValus; + } + + emit('checkChange', checkValues); } }; @@ -239,6 +280,7 @@ export default (config, props, emit) => { emit, selectedNodes, leafNodes, + allNodes, ); const { handleKeyDown } = useKeyDown(config, emit, menus); diff --git a/components/cascader-panel/utils.js b/components/cascader-panel/utils.js index b19c7f70..7c2b0ae7 100644 --- a/components/cascader-panel/utils.js +++ b/components/cascader-panel/utils.js @@ -1,4 +1,5 @@ import getPrefixCls from '../_util/getPrefixCls'; +import { CHECK_STRATEGY } from './const'; import { flatNodes } from '../_util/utils'; /** @@ -22,7 +23,7 @@ export const calculatePathNodes = (node) => { /** * 多选的时候,更新选中节点的父级节点的选中和半选中状态 - * 1. 遍历选中节点,level从下到上更新父节点的选中状态 + * 1. 遍历选中节点,level 从下到上更新父节点的选中状态 */ export const updateParentNodesCheckState = (selectedNodes = []) => { selectedNodes.forEach((node) => { @@ -44,6 +45,61 @@ export const updateParentNodesCheckState = (selectedNodes = []) => { }); }; +/** + * 多选情况,更新选中节点子级节点的选中状态 + * 1. 遍历选中节点,获取所有的子孙节点,全部置为选中状态 + */ +export const updateChildNodesCheckState = (selectedNodes = []) => { + selectedNodes.forEach((node) => { + const childAndLeafNodes = flatNodes(node.children); + childAndLeafNodes.forEach((item) => { + item.checked = true; + }); + }); +}; + +/** + * 根据叶子节点列表获取包含关联选中父节点的值列表 + * 1. 遍历叶子节点,level 从下到上判断父节点是否选中 + * 2. 若父节点为选中状态(所有子节点值都在值列表中),则值列表中插入父节点 + * 3. 遍历全部节点列表,顺序返回 + * 4. 若为 checkStrictly = parent 情况,则若当前节点的父节点值不在值列表中,则插入当前节点 + */ +export const getCheckNodesByLeafCheckNodes = ( + checkLeafNodes = [], + allNodes = [], + checkStrictly, +) => { + const checkNodeValues = []; + + checkLeafNodes.forEach((node) => { + checkNodeValues.push(node.value); + + const parentNodes = node.pathNodes.slice(0, node.pathNodes.length - 1); + for (let i = parentNodes.length - 1; i >= 0; i--) { + const parentNode = parentNodes[i]; + const parentChecked = parentNode.children.every((child) => + checkNodeValues.includes(child.value), + ); + if (parentChecked) { + checkNodeValues.push(parentNode.value); + } + } + }); + + return allNodes.filter((item) => { + if (!checkNodeValues.includes(item.value)) { + return false; + } + if (checkStrictly === CHECK_STRATEGY.PARENT) { + if (item.parent) { + return !checkNodeValues.includes(item.parent.value); + } + } + return true; + }); +}; + export const getNode = (data = [], config = {}, parent = null) => { const node = {}; @@ -79,11 +135,8 @@ export const getNode = (data = [], config = {}, parent = null) => { return node; }; -export const getNodeByValue = (nodes, value) => { - const filterNodes = flatNodes(nodes).filter((node) => node.value === value); - - return filterNodes[0] || null; -}; +export const getNodeByValue = (allNodes, value) => + allNodes.find((node) => node.value === value) || null; export const getValueByOption = (config, node) => config.emitPath ? node.pathValues : node.value; @@ -93,7 +146,7 @@ export const getNodeValueByCurrentValue = (emitPath, value) => { let nodeValue = ''; if (emitPath) { if (Array.isArray(value)) { - nodeValue = value[value.length - 1]; + nodeValue = (value.length && value[value.length - 1]) || ''; } else { console.warn( 'value类型不符预期,emitPath为true的情况下,value应该为数组格式', @@ -124,10 +177,10 @@ export const getMultiNodeValuesByCurrentValue = (emitPath, currentValue) => { }; export const getMenuIndexByElem = (el, menus) => - menus.findIndex((menu) => menu.find((node) => node.elem === el)); + menus.findIndex((menu) => menu.nodes.find((node) => node.elem === el)); export const getMenuNodeByElem = (el, menus) => { const currentMenu = menus[getMenuIndexByElem(el, menus)] || null; - return currentMenu?.find((node) => node.elem === el) || null; + return currentMenu?.nodes.find((node) => node.elem === el) || null; }; // 获取元素兄弟节点 @@ -137,21 +190,22 @@ export const getNodeSibling = (el, distance = 0, menus) => { let currentNodeIndex = -1; const currentMenu = menus[getMenuIndexByElem(el, menus)] || null; - currentNodeIndex = currentMenu?.findIndex((node) => node.elem === el); + currentNodeIndex = currentMenu?.nodes.findIndex((node) => node.elem === el); siblingNode = currentNodeIndex > -1 - ? currentMenu?.[currentNodeIndex + distance] || null + ? currentMenu?.nodes[currentNodeIndex + distance] || null : null; while (siblingNode && siblingNode.isDisabled) { const currentElem = siblingNode.elem; - currentNodeIndex = currentMenu.findIndex( + currentNodeIndex = currentMenu.nodes.findIndex( (node) => node.elem === currentElem, ); siblingNode = currentNodeIndex > -1 - ? currentMenu[currentNodeIndex + (distance > 0 ? 1 : -1)] || - null + ? currentMenu.nodes[ + currentNodeIndex + (distance > 0 ? 1 : -1) + ] || null : null; } diff --git a/components/cascader/cascader.vue b/components/cascader/cascader.vue index 14beb461..6a92cc56 100644 --- a/components/cascader/cascader.vue +++ b/components/cascader/cascader.vue @@ -28,6 +28,7 @@