treeUtil.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. /**
  2. * Part of the utils function implementation process reference
  3. * https://github.com/react-component/tree/blob/master/src/util.tsx
  4. */
  5. import { difference, uniq, max, isObject, isNull, isUndefined, isEmpty, pick, get } from 'lodash';
  6. export interface KeyEntities {
  7. [x: string]: any
  8. }
  9. export interface TreeDataSimpleJson {
  10. [x: string]: string | TreeDataSimpleJson
  11. }
  12. export interface NodeData {
  13. key: any;
  14. label: any;
  15. value: any;
  16. children?: any
  17. }
  18. const DRAG_OFFSET = 0.45;
  19. function getPosition(level: any, index: any) {
  20. return `${level}-${index}`;
  21. }
  22. function isValid(val: any) {
  23. return !isNull(val) && !isUndefined(val);
  24. }
  25. /**
  26. * Flat nest tree data into flatten list. This is used for virtual list render.
  27. * @param treeNodeList Origin data node list
  28. * @param expandedKeys
  29. * @param filteredShownKeys
  30. * need expanded keys, provides `true` means all expanded
  31. */
  32. // eslint-disable-next-line max-len
  33. export function flattenTreeData(treeNodeList: any[], expandedKeys: Set<string>, filteredShownKeys: boolean | Set<any> = false) {
  34. const flattenList: any[] = [];
  35. const filterSearch = Boolean(filteredShownKeys);
  36. function flatten(list: any[], parent: any = null) {
  37. return list.map((treeNode, index) => {
  38. const pos = getPosition(parent ? parent.pos : '0', index);
  39. const mergedKey = treeNode.key;
  40. // Add FlattenDataNode into list
  41. const flattenNode: any = {
  42. ...pick(treeNode, ['key', 'label', 'value', 'icon', 'disabled', 'isLeaf']),
  43. parent,
  44. pos,
  45. children: null,
  46. data: treeNode,
  47. _innerDataTag: true,
  48. };
  49. const isBooleanFilteredShownKeys = typeof filteredShownKeys === 'boolean';
  50. if (!filterSearch || (!isBooleanFilteredShownKeys && filteredShownKeys.has(mergedKey))) {
  51. flattenList.push(flattenNode);
  52. }
  53. // Loop treeNode children
  54. // eslint-disable-next-line max-len
  55. if (expandedKeys.has(mergedKey) && (!filterSearch || (!isBooleanFilteredShownKeys && filteredShownKeys.has(mergedKey)))) {
  56. flattenNode.children = flatten(treeNode.children || [], flattenNode);
  57. } else {
  58. flattenNode.children = [];
  59. }
  60. return flattenNode;
  61. });
  62. }
  63. flatten(treeNodeList);
  64. return flattenList;
  65. }
  66. export function convertJsonToData(treeJson: TreeDataSimpleJson) {
  67. const treeData: any[] = [];
  68. const traverseNode = (key: string, children: any, path: any, res: any[]) => {
  69. const currPath = [...path, key];
  70. const itemKey = currPath.join('-');
  71. const newNode: NodeData = {
  72. key: itemKey,
  73. label: key,
  74. value: children,
  75. };
  76. if (isObject(children)) {
  77. const newChildren: any[] = [];
  78. Object.entries(children).forEach(c => {
  79. traverseNode(c[0], c[1], currPath, newChildren);
  80. });
  81. newNode.children = newChildren;
  82. }
  83. res.push(newNode);
  84. };
  85. Object.entries(treeJson).forEach(item => traverseNode(item[0], item[1], [], treeData));
  86. return treeData;
  87. }
  88. /**
  89. * Traverse all the data by `treeData`.
  90. */
  91. export function traverseDataNodes(treeNodes: any[], callback: (data: any) => void) {
  92. const processNode = (node: any, ind?: number, parent?: any) => {
  93. const children = node ? node.children : treeNodes;
  94. const pos = node ? getPosition(parent.pos, ind) : '0';
  95. // Process node if is not root
  96. if (node) {
  97. const data = {
  98. data: { ...node },
  99. ind,
  100. pos,
  101. key: node.key !== null ? node.key : pos,
  102. parentPos: parent.node ? parent.pos : null,
  103. level: Number(parent.level) + 1,
  104. };
  105. callback(data);
  106. }
  107. // Process children node
  108. if (children) {
  109. children.forEach((subNode: any, subIndex: number) => {
  110. processNode(subNode, subIndex, {
  111. node,
  112. pos,
  113. level: parent ? Number(parent.level) + 1 : -1,
  114. });
  115. });
  116. }
  117. };
  118. processNode(null);
  119. }
  120. /* Convert data to entities map */
  121. export function convertDataToEntities(dataNodes: any[]) {
  122. const posEntities = {};
  123. const keyEntities = {};
  124. const valueEntities = {};
  125. const wrapper = {
  126. posEntities,
  127. keyEntities,
  128. valueEntities,
  129. };
  130. traverseDataNodes(dataNodes, (data: any) => {
  131. const { pos, key, parentPos } = data;
  132. const entity = { ...data };
  133. const value = get(entity, 'data.value', null);
  134. if (value !== null) {
  135. valueEntities[value] = key;
  136. }
  137. posEntities[pos] = entity;
  138. keyEntities[key] = entity;
  139. // Fill children
  140. entity.parent = posEntities[parentPos];
  141. if (entity.parent) {
  142. entity.parent.children = entity.parent.children || [];
  143. entity.parent.children.push(entity);
  144. }
  145. });
  146. return wrapper;
  147. }
  148. /* Get key by value */
  149. export function findKeysForValues(valueList: any, valueEntities: any, isMultiple = false) {
  150. if (!isValid(valueList)) {
  151. return [];
  152. }
  153. if (!isMultiple && Array.isArray(valueList)) {
  154. valueList = valueList.length ? [valueList[0]] : [];
  155. } else if (!Array.isArray(valueList)) {
  156. valueList = [valueList];
  157. }
  158. if (isEmpty(valueEntities)) {
  159. return valueList;
  160. }
  161. const res: any[] = [];
  162. valueList.forEach((val: string) => {
  163. if (val in valueEntities) {
  164. res.push(valueEntities[val]);
  165. } else {
  166. // if val not in valueEntities, then value push to keys array
  167. val && res.push(val);
  168. }
  169. });
  170. return res;
  171. }
  172. export function findDescendantKeys(selectedKeys: string[], options: KeyEntities, self = true) {
  173. const res: string[] = [];
  174. const findChild = (item: any) => {
  175. if (!item) {
  176. return;
  177. }
  178. const { children } = item;
  179. const hasChildren = isValid(children);
  180. if (hasChildren) {
  181. children.forEach((child: any) => {
  182. res.push(child.key);
  183. findChild(options[child.key]);
  184. });
  185. }
  186. };
  187. selectedKeys.forEach(item => {
  188. if (self) {
  189. res.push(item);
  190. }
  191. findChild(options[item]);
  192. });
  193. return res;
  194. }
  195. export function findChildKeys(keys: string[], options: any, omitKeys: any[] = []) {
  196. const res: any[] = [];
  197. keys &&
  198. keys.forEach(key => {
  199. const opts = options[key];
  200. opts &&
  201. opts.children &&
  202. opts.children.forEach((child: any) => {
  203. if (!omitKeys.length || !omitKeys.includes(child.key)) {
  204. res.push(child.key);
  205. }
  206. });
  207. });
  208. return res;
  209. }
  210. /* istanbul ignore next */
  211. export function findLeafKeys(keys: string[], options: any) {
  212. const res: any[] = [];
  213. const findChild = (item: any) => {
  214. if (!item) {
  215. return;
  216. }
  217. const { children } = item;
  218. const isLeaf = !isValid(children);
  219. if (isLeaf) {
  220. res.push(item.key);
  221. } else {
  222. children.forEach((child: any) => {
  223. findChild(options[child.key]);
  224. });
  225. }
  226. };
  227. keys.forEach(item => {
  228. findChild(options[item]);
  229. });
  230. return res;
  231. }
  232. export function findSiblingKeys(selectedKeys: string[], options: any, self = true) {
  233. const par: any[] = [];
  234. selectedKeys.forEach(item => {
  235. if (options[item] && options[item].parent) {
  236. par.push(options[item].parent.key);
  237. }
  238. });
  239. const res = findChildKeys(uniq(par), options, self ? [] : selectedKeys);
  240. return res;
  241. }
  242. export function findAncestorKeys(selectedKeys: string[], options: any, self = true) {
  243. const res: any[] = [];
  244. // Recursively find the parent element
  245. const findPar = (item: any) => {
  246. if (item.parent) {
  247. res.push(item.parent.key);
  248. findPar(item.parent);
  249. }
  250. };
  251. selectedKeys.forEach(item => {
  252. options[item] && findPar(options[item]);
  253. if (self) {
  254. res.push(item);
  255. }
  256. });
  257. return res;
  258. }
  259. function getSortedKeyList(keyList: any[], keyEntities: KeyEntities) {
  260. const levelMap = {};
  261. keyList.forEach(key => {
  262. if (!keyEntities[key]) {
  263. return;
  264. }
  265. const { level } = keyEntities[key];
  266. if (levelMap[level]) {
  267. levelMap[level].push(key);
  268. } else {
  269. levelMap[level] = [key];
  270. }
  271. });
  272. return levelMap;
  273. }
  274. export function calcCheckedKeys(values: any, keyEntities: KeyEntities) {
  275. const keyList = Array.isArray(values) ? values : [values];
  276. const descendantKeys = findDescendantKeys(keyList, keyEntities, true);
  277. /**
  278. * Recursively find the parent element. Because the incoming nodes are all checked,
  279. * their descendants must be checked. That is to say, if the descendant nodes have
  280. * disabled+unchecked nodes, their ancestor nodes will definitely not be checked
  281. */
  282. const checkedKeys = new Set([...descendantKeys]);
  283. let halfCheckedKeys = new Set([]);
  284. let visited: any[] = [];
  285. const levelMap:{[key: number]: string[]} = getSortedKeyList(keyList, keyEntities);
  286. const calcCurrLevel = (node: any) => {
  287. const { key, parent, level } = node;
  288. // If the node does not have a parent node, or the node has been processed just now, no processing is done
  289. if (!parent || visited.includes(key)) {
  290. return;
  291. }
  292. const siblingKeys = findSiblingKeys([key], keyEntities);
  293. // visited for caching to avoid double counting
  294. visited = [...visited, ...siblingKeys];
  295. const allChecked = siblingKeys.every((siblingKey: string) => checkedKeys.has(siblingKey));
  296. if (!allChecked) {
  297. const ancestorKeys = findAncestorKeys([key], keyEntities, false);
  298. halfCheckedKeys = new Set([...halfCheckedKeys, ...ancestorKeys]);
  299. } else {
  300. checkedKeys.add(parent.key);
  301. // IMPORTANT! parent level may not exist in original level map; if add to the end directly may destroy the hierarchical order
  302. if (level - 1 in levelMap && level) {
  303. levelMap[level - 1].push(parent.key);
  304. } else {
  305. levelMap[level - 1] = [parent.key];
  306. }
  307. }
  308. };
  309. // Loop keyList from deepest Level to topLevel, bottom up
  310. while (!isEmpty(levelMap)) {
  311. const maxLevel = max(Object.keys(levelMap).map(key => Number(key)));
  312. levelMap[maxLevel].forEach((key: string) => calcCurrLevel(keyEntities[key]));
  313. delete levelMap[maxLevel];
  314. }
  315. return {
  316. checkedKeys,
  317. halfCheckedKeys,
  318. };
  319. }
  320. /* Calculate the expanded node by key */
  321. export function calcExpandedKeys(keyList: any[] = [], keyEntities: KeyEntities, autoExpandParent = true) {
  322. if (!Array.isArray(keyList)) {
  323. keyList = [keyList];
  324. }
  325. if (autoExpandParent) {
  326. const ancestorKeys = findAncestorKeys(keyList, keyEntities, true);
  327. return new Set(ancestorKeys);
  328. }
  329. return new Set(keyList);
  330. }
  331. /* Calculate the expanded node by value */
  332. // eslint-disable-next-line max-len
  333. export function calcExpandedKeysForValues(value: any, keyEntities: KeyEntities, isMultiple: boolean, valueEntities: any) {
  334. const keys = findKeysForValues(value, valueEntities, isMultiple);
  335. return new Set(findAncestorKeys(keys, keyEntities, false));
  336. }
  337. export function calcMotionKeys(oldKeySet: Set<string>, newKeySet: Set<string>, keyEntities: KeyEntities) {
  338. let motionType = 'show';
  339. const oldKeys = [...oldKeySet];
  340. const newKeys = [...newKeySet];
  341. if (Math.abs(oldKeys.length - newKeys.length) !== 1) {
  342. return { motionType, motionKeys: [] };
  343. }
  344. let diffKeys = [];
  345. if (oldKeys.length > newKeys.length) {
  346. motionType = 'hide';
  347. diffKeys = difference(oldKeys, newKeys);
  348. } else {
  349. diffKeys = difference(newKeys, oldKeys);
  350. }
  351. return {
  352. motionType: diffKeys.length === 1 ? motionType : 'show',
  353. motionKeys: diffKeys.length === 1 ? findDescendantKeys(diffKeys, keyEntities, false) : [],
  354. };
  355. }
  356. /**
  357. * @returns whether option includes sugInput.
  358. * When filterTreeNode is a function,returns the result of filterTreeNode which called with (sugInput, target, option).
  359. * The filteredPath parameter will only be passed in when the Cascader calls the filter function
  360. */
  361. export function filter(sugInput: string, option: any, filterTreeNode: any, filterProps: any, filteredPath?: string) {
  362. if (!filterTreeNode) {
  363. return true;
  364. }
  365. let filterFn = filterTreeNode;
  366. let target = filteredPath ?? option;
  367. if (typeof filterTreeNode === 'boolean') {
  368. filterFn = (targetVal: string, val: any) => {
  369. const input = targetVal.toLowerCase();
  370. return val
  371. .toString()
  372. .toLowerCase()
  373. .includes(input);
  374. };
  375. }
  376. if (filterProps) {
  377. target = option[filterProps];
  378. }
  379. return filterFn(sugInput, target, option);
  380. }
  381. export function normalizedArr(val: any) {
  382. if (!Array.isArray(val)) {
  383. return [val];
  384. } else {
  385. return val;
  386. }
  387. }
  388. // flag is used to determine whether to return when the key does not belong to the keys in keyEntities
  389. // export function normalizeKeyList(keyList: any, keyEntities: KeyEntities, leafOnly = false) {
  390. export function normalizeKeyList(keyList: any, keyEntities: KeyEntities, leafOnly = false, flag?: boolean) {
  391. const res: string[] = [];
  392. const keyListSet = new Set(keyList);
  393. if (!leafOnly) {
  394. keyList.forEach((key: string) => {
  395. if (!keyEntities[key]) {
  396. if (flag) {
  397. res.push(key);
  398. }
  399. return;
  400. }
  401. const { parent } = keyEntities[key];
  402. if (parent && keyListSet.has(parent.key)) {
  403. return;
  404. }
  405. res.push(key);
  406. });
  407. } else {
  408. keyList.forEach(key => {
  409. if (keyEntities[key] && !isValid(keyEntities[key].children)) {
  410. res.push(key);
  411. }
  412. // when key is not in keyEntities, if flag is true, key should be push in res
  413. if (!keyEntities[key] && flag) {
  414. res.push(key);
  415. }
  416. });
  417. }
  418. return res;
  419. }
  420. export function getMotionKeys(eventKey: string, expandedKeys: Set<string>, keyEntities: KeyEntities) {
  421. const res: any[] = [];
  422. const getChild = (itemKey: string) => {
  423. keyEntities[itemKey].children &&
  424. keyEntities[itemKey].children.forEach((item: any) => {
  425. const { key } = item;
  426. res.push(key);
  427. if (expandedKeys.has(key)) {
  428. getChild(key);
  429. }
  430. });
  431. };
  432. getChild(eventKey);
  433. return res;
  434. }
  435. // eslint-disable-next-line max-len
  436. export function calcCheckedKeysForChecked(key: string, keyEntities: KeyEntities, checkedKeys: Set<string>, halfCheckedKeys: Set<string>) {
  437. const descendantKeys = findDescendantKeys([key], keyEntities, true);
  438. const nodeItem = keyEntities[key];
  439. checkedKeys = new Set([...checkedKeys, key]);
  440. const calcCurrLevel = (node: any) => {
  441. if (!node.parent) {
  442. return;
  443. }
  444. // eslint-disable-next-line @typescript-eslint/no-shadow
  445. const { key } = node;
  446. const siblingKeys = findSiblingKeys([key], keyEntities);
  447. // eslint-disable-next-line @typescript-eslint/no-shadow
  448. const allChecked = siblingKeys.every(key => checkedKeys.has(key));
  449. if (!allChecked) {
  450. const ancestorKeys = findAncestorKeys([key], keyEntities, false);
  451. halfCheckedKeys = new Set([...halfCheckedKeys, ...ancestorKeys]);
  452. } else {
  453. const par = node.parent;
  454. checkedKeys.add(par.key);
  455. calcCurrLevel(par);
  456. }
  457. };
  458. calcCurrLevel(nodeItem);
  459. return {
  460. checkedKeys: new Set([...checkedKeys, ...descendantKeys]),
  461. halfCheckedKeys,
  462. };
  463. }
  464. // eslint-disable-next-line max-len
  465. export function calcCheckedKeysForUnchecked(key: string, keyEntities: KeyEntities, checkedKeys: Set<string>, halfCheckedKeys: Set<string>) {
  466. const descendantKeys = findDescendantKeys([key], keyEntities, true);
  467. const nodeItem = keyEntities[key];
  468. descendantKeys.forEach(descendantKey => {
  469. if (checkedKeys.has(descendantKey)) {
  470. checkedKeys.delete(descendantKey);
  471. }
  472. if (halfCheckedKeys.has(descendantKey)) {
  473. halfCheckedKeys.delete(descendantKey);
  474. }
  475. });
  476. const calcCurrLevel = (node: any) => {
  477. const par = node.parent;
  478. // no parent
  479. if (!par) {
  480. return;
  481. }
  482. // Has a parent node, and the parent node is not checked or halfChecked
  483. if (!checkedKeys.has(par.key) && !halfCheckedKeys.has(par.key)) {
  484. return;
  485. }
  486. // Has a parent node, and the parent node is checked or halfChecked
  487. // eslint-disable-next-line @typescript-eslint/no-shadow
  488. const { key } = node;
  489. const siblingKeys = findSiblingKeys([key], keyEntities);
  490. // eslint-disable-next-line @typescript-eslint/no-shadow
  491. const anyChecked = siblingKeys.some(key => checkedKeys.has(key) || halfCheckedKeys.has(key));
  492. const ancestorKeys = findAncestorKeys([key], keyEntities, false);
  493. // If there is checked or halfChecked in the sibling node, you need to change the parent node to halfChecked
  494. if (anyChecked) {
  495. ancestorKeys.forEach(itemKey => {
  496. if (checkedKeys.has(itemKey)) {
  497. checkedKeys.delete(itemKey);
  498. halfCheckedKeys.add(itemKey);
  499. }
  500. });
  501. // If there is no checked or halfChecked in the sibling node, you need to change the parent node to unchecked
  502. } else {
  503. if (checkedKeys.has(par.key)) {
  504. checkedKeys.delete(par.key);
  505. }
  506. if (halfCheckedKeys.has(par.key)) {
  507. halfCheckedKeys.delete(par.key);
  508. }
  509. calcCurrLevel(par);
  510. }
  511. };
  512. nodeItem && calcCurrLevel(nodeItem);
  513. return {
  514. checkedKeys,
  515. halfCheckedKeys,
  516. };
  517. }
  518. export function filterTreeData(info: any) {
  519. const {
  520. showFilteredOnly,
  521. keyEntities,
  522. inputValue,
  523. treeData,
  524. filterTreeNode,
  525. filterProps,
  526. prevExpandedKeys,
  527. } = info;
  528. let filteredOptsKeys = [];
  529. filteredOptsKeys = Object.values(keyEntities)
  530. .filter((item: any) => filter(inputValue, item.data, filterTreeNode, filterProps))
  531. .map((item: any) => item.key);
  532. let expandedOptsKeys = findAncestorKeys(filteredOptsKeys, keyEntities, false);
  533. if (prevExpandedKeys.length) {
  534. const prevExpandedValidKeys = prevExpandedKeys.filter((key: string) => Boolean(keyEntities[key]));
  535. expandedOptsKeys = expandedOptsKeys.concat(prevExpandedValidKeys);
  536. }
  537. const shownChildKeys = findDescendantKeys(filteredOptsKeys, keyEntities, true);
  538. const filteredShownKeys = new Set([...shownChildKeys, ...expandedOptsKeys]);
  539. const flattenNodes = flattenTreeData(treeData, new Set(expandedOptsKeys), showFilteredOnly && filteredShownKeys);
  540. return {
  541. flattenNodes,
  542. filteredKeys: new Set(filteredOptsKeys),
  543. filteredExpandedKeys: new Set(expandedOptsKeys),
  544. filteredShownKeys,
  545. };
  546. }
  547. // return data.value if data.value exist else fall back to key
  548. export function getValueOrKey(data: any) {
  549. if (Array.isArray(data)) {
  550. return data.map(item => get(item, 'value', item.key));
  551. }
  552. return get(data, 'value', data.key);
  553. }
  554. /* Convert value to string */
  555. export function normalizeValue(value: any, withObject: boolean) {
  556. if (withObject && isValid(value)) {
  557. return getValueOrKey(value);
  558. } else {
  559. return value;
  560. }
  561. }
  562. export function updateKeys(keySet: Set<string> | string[], keyEntities: KeyEntities) {
  563. const keyArr = [...keySet];
  564. return keyArr.filter(key => key in keyEntities);
  565. }
  566. export function calcDisabledKeys(keyEntities: KeyEntities) {
  567. const disabledKeys = Object.keys(keyEntities).filter(key => keyEntities[key].data.disabled);
  568. const { checkedKeys } = calcCheckedKeys(disabledKeys, keyEntities);
  569. return checkedKeys;
  570. }
  571. export function calcDropRelativePosition(event: any, treeNode: any) {
  572. const { clientY } = event;
  573. const { top, bottom, height } = treeNode.nodeInstance.getBoundingClientRect();
  574. // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
  575. if (clientY <= top + height * DRAG_OFFSET) {
  576. return -1;
  577. }
  578. if (clientY >= bottom - height * DRAG_OFFSET) {
  579. return 1;
  580. }
  581. return 0;
  582. }
  583. export function getDragNodesKeys(key: string, keyEntities: KeyEntities) {
  584. return findDescendantKeys([key], keyEntities, true);
  585. }
  586. export function calcDropActualPosition(pos: string, relativeDropPos: any) {
  587. const posArr = pos.split('-');
  588. // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
  589. return relativeDropPos + Number(posArr[posArr.length - 1]);
  590. }