treeUtil.ts 21 KB

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