foundation.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  2. import NavItem from './NavItem';
  3. import { ItemProps, ItemKey } from './itemFoundation';
  4. import { strings } from './constants';
  5. import { get } from 'lodash';
  6. import isNullOrUndefined from '../utils/isNullOrUndefined';
  7. export interface ItemKey2ParentKeysMap {
  8. [key: string]: (string | number)[]
  9. }
  10. export interface OnClickData {
  11. itemKey: ItemKey;
  12. domEvent: any;
  13. isOpen: boolean
  14. }
  15. export interface OnSelectData extends OnClickData {
  16. selectedKeys: ItemKey[];
  17. selectedItems: ItemProps[]
  18. }
  19. export interface OnOpenChangeData extends OnClickData {
  20. openKeys: ItemKey[]
  21. }
  22. export interface NavItemType {
  23. props?: ItemProps;
  24. items?: NavItemType[];
  25. [key: string]: any
  26. }
  27. export interface NavigationAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  28. notifySelect(data: OnSelectData): void;
  29. notifyOpenChange(data: OnOpenChangeData): void;
  30. setIsCollapsed(isCollapsed: boolean): void;
  31. notifyCollapseChange(isCollapsed: boolean): void;
  32. updateItems(items: ItemProps[]): void;
  33. setItemKeysMap(map: { [key: string]: (string | number)[] }): void;
  34. addSelectedKeys(...keys: (string | number)[]): void;
  35. removeSelectedKeys(...keys: (string | number)[]): void;
  36. updateSelectedKeys(keys: (string | number)[], includeParentKeys?: boolean): void;
  37. updateOpenKeys(keys: (string | number)[]): void;
  38. addOpenKeys(...keys: (string | number)[]): void;
  39. removeOpenKeys(...keys: (string | number)[]): void;
  40. setItemsChanged(isChanged: boolean): void
  41. }
  42. export default class NavigationFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<NavigationAdapter<P, S>, P, S> {
  43. constructor(adapter: NavigationAdapter<P, S>) {
  44. super({ ...adapter });
  45. }
  46. /* istanbul ignore next */
  47. static getZeroParentKeys(itemKeysMap = {}, ...itemKeys: ItemKey[]) {
  48. const willAddKeys = [];
  49. if (itemKeys.length) {
  50. for (const itemKey of itemKeys) {
  51. if (Array.isArray(itemKeysMap[itemKey]) && itemKeysMap[itemKey].length) {
  52. const levelZeroParentKey = itemKeysMap[itemKey][0];
  53. if (!isNullOrUndefined(levelZeroParentKey)) {
  54. willAddKeys.push(levelZeroParentKey);
  55. }
  56. }
  57. }
  58. }
  59. return willAddKeys;
  60. }
  61. static buildItemKeysMap(items: NavItemType[] = [], keysMap = {}, parentKeys: (string | number)[] = [], keyPropName = 'itemKey') {
  62. if (Array.isArray(items) && items.length) {
  63. for (const item of items) {
  64. if (Array.isArray(item)) {
  65. NavigationFoundation.buildItemKeysMap(item, keysMap, [...parentKeys], keyPropName);
  66. } else {
  67. let itemKey;
  68. if (item && typeof item === 'object') {
  69. itemKey = item[keyPropName] || (item.props && item.props[keyPropName]);
  70. }
  71. if (itemKey) {
  72. keysMap[itemKey] = [...parentKeys];
  73. // Children is not a recommended usage and may cause some bug-like performance, but some users have already used it, so here we only delete the ts definition instead of deleting the actual code
  74. // children 并不是我们推荐的用法,可能会导致一些像 bug的表现,但是有些用户已经用了,所以此处仅作删除 ts 定义而非删除实际代码的操作
  75. // refer https://github.com/DouyinFE/semi-design/issues/2710
  76. // @ts-ignore
  77. const itemChildren = item.props?.children;
  78. if (Array.isArray(item.items) && item.items.length) {
  79. NavigationFoundation.buildItemKeysMap(
  80. item.items,
  81. keysMap,
  82. [...parentKeys, itemKey],
  83. keyPropName
  84. );
  85. } else if (itemChildren) {
  86. const children = Array.isArray(itemChildren)
  87. ? itemChildren
  88. : [itemChildren];
  89. NavigationFoundation.buildItemKeysMap(
  90. children,
  91. keysMap,
  92. [...parentKeys, itemKey],
  93. keyPropName
  94. );
  95. }
  96. }
  97. }
  98. }
  99. }
  100. return keysMap;
  101. }
  102. /**
  103. * init is called in constructor and componentDidMount.
  104. * if you want to update state in constructor, please add it to return object;
  105. * if you want to update state in componentDidMount, please call adapter in else logic.
  106. * @param {*} lifecycle
  107. * @returns
  108. */
  109. init(lifecycle?: string) {
  110. const { defaultSelectedKeys, selectedKeys } = this.getProps();
  111. let willSelectedKeys = selectedKeys || defaultSelectedKeys || [];
  112. const { itemKeysMap, willOpenKeys, formattedItems } = this.getCalcState();
  113. const parentSelectKeys = this.selectLevelZeroParentKeys(itemKeysMap, willSelectedKeys);
  114. willSelectedKeys = willSelectedKeys.concat(parentSelectKeys);
  115. if (lifecycle === 'constructor') {
  116. return {
  117. selectedKeys: willSelectedKeys,
  118. itemKeysMap,
  119. openKeys: willOpenKeys,
  120. items: formattedItems,
  121. };
  122. } else {
  123. // already include parentSelectKeys, set second parameter to false
  124. this._adapter.updateSelectedKeys(willSelectedKeys, false);
  125. this._adapter.setItemKeysMap(itemKeysMap);
  126. this._adapter.updateOpenKeys(willOpenKeys);
  127. this._adapter.updateItems(formattedItems);
  128. this._adapter.setItemsChanged(true);
  129. }
  130. return undefined;
  131. }
  132. /**
  133. * Get the state to be calculated
  134. */
  135. getCalcState() {
  136. const { itemKeysMap, formattedItems } = this.getFormattedItems();
  137. const willOpenKeys = this.getWillOpenKeys(itemKeysMap);
  138. return { itemKeysMap, willOpenKeys, formattedItems };
  139. }
  140. /**
  141. * Calculate formatted items and itemsKeyMap
  142. */
  143. getFormattedItems() {
  144. const { items, children } = this.getProps();
  145. const formattedItems = this.formatItems(items);
  146. const willHandleItems = Array.isArray(items) && items.length ? formattedItems : children;
  147. const itemKeysMap = NavigationFoundation.buildItemKeysMap(willHandleItems);
  148. return {
  149. itemKeysMap,
  150. formattedItems
  151. };
  152. }
  153. /**
  154. * Calculate the keys that will need to be opened soon
  155. * @param {*} itemKeysMap
  156. */
  157. getWillOpenKeys(itemKeysMap: ItemKey2ParentKeysMap) {
  158. const { defaultOpenKeys, openKeys, defaultSelectedKeys, selectedKeys, mode } = this.getProps();
  159. const { openKeys: stateOpenKeys = [] } = this.getStates();
  160. let willOpenKeys = openKeys || defaultOpenKeys || [];
  161. if (
  162. !(Array.isArray(defaultOpenKeys) ||
  163. Array.isArray(openKeys)) && mode === strings.MODE_VERTICAL && (Array.isArray(defaultSelectedKeys) || Array.isArray(selectedKeys))
  164. ) {
  165. const currentSelectedKeys = Array.isArray(selectedKeys) ? selectedKeys : defaultSelectedKeys;
  166. willOpenKeys = stateOpenKeys.concat(this.getShouldOpenKeys(itemKeysMap, currentSelectedKeys));
  167. willOpenKeys = Array.from(new Set(willOpenKeys));
  168. }
  169. return [...willOpenKeys];
  170. }
  171. getShouldOpenKeys(itemKeysMap: ItemKey2ParentKeysMap = {}, selectedKeys: ItemKey[] = []) {
  172. const willOpenKeySet = new Set();
  173. if (Array.isArray(selectedKeys) && selectedKeys.length) {
  174. selectedKeys.forEach(item => {
  175. if (item) {
  176. const parentKeys = get(itemKeysMap, item);
  177. if (Array.isArray(parentKeys)) {
  178. parentKeys.forEach(k => willOpenKeySet.add(k));
  179. }
  180. }
  181. });
  182. }
  183. return [...willOpenKeySet];
  184. }
  185. destroy() {}
  186. selectLevelZeroParentKeys(itemKeysMap: ItemKey2ParentKeysMap, itemKeys: ItemKey[]) {
  187. const _itemKeysMap = isNullOrUndefined(itemKeysMap) ? this.getState('itemKeysMap') : itemKeysMap;
  188. // console.log(itemKeysMap);
  189. const willAddKeys = [];
  190. if (itemKeys.length) {
  191. for (const itemKey of itemKeys) {
  192. if (Array.isArray(_itemKeysMap[itemKey]) && _itemKeysMap[itemKey].length) {
  193. const levelZeroParentKey = _itemKeysMap[itemKey][0];
  194. if (!isNullOrUndefined(levelZeroParentKey)) {
  195. willAddKeys.push(levelZeroParentKey);
  196. }
  197. }
  198. }
  199. }
  200. if (willAddKeys.length) {
  201. return willAddKeys;
  202. }
  203. return [];
  204. }
  205. formatItems(items: ItemProps[] = []) {
  206. const formattedItems = [];
  207. for (const item of items) {
  208. formattedItems.push(new NavItem(item));
  209. }
  210. return formattedItems;
  211. }
  212. handleSelect(data: OnSelectData) {
  213. this._adapter.notifySelect(data);
  214. }
  215. /* istanbul ignore next */
  216. judgeIfOpen(openKeys: ItemKey[], items: NavItemType[]): boolean {
  217. let shouldBeOpen = false;
  218. const _openKeys = Array.isArray(openKeys) ? openKeys : openKeys && [openKeys];
  219. if (_openKeys && Array.isArray(items) && items.length) {
  220. for (const item of items) {
  221. shouldBeOpen = _openKeys.includes(item.itemKey) || this.judgeIfOpen(_openKeys, item.items);
  222. if (shouldBeOpen) {
  223. break;
  224. }
  225. }
  226. }
  227. return shouldBeOpen;
  228. }
  229. handleCollapseChange() {
  230. const isCollapsed = !this.getState('isCollapsed');
  231. if (!this._isControlledComponent('isCollapsed')) {
  232. this._adapter.setIsCollapsed(isCollapsed);
  233. }
  234. this._adapter.notifyCollapseChange(isCollapsed);
  235. }
  236. handleItemsChange(isChanged: boolean) {
  237. this._adapter.setItemsChanged(isChanged);
  238. }
  239. }