foundation.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. /* eslint-disable max-depth */
  2. /* eslint-disable max-len */
  3. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  4. import NavItem from './NavItem';
  5. import { ItemProps } from './itemFoundation';
  6. import { strings } from './constants';
  7. import { get } from 'lodash-es';
  8. import isNullOrUndefined from '../utils/isNullOrUndefined';
  9. export interface ItemKey2ParentKeysMap {
  10. [key: string]: (string | number)[];
  11. }
  12. export interface OnClickData {
  13. itemKey: string | number;
  14. domEvent: any;
  15. isOpen: boolean;
  16. }
  17. export interface OnSelectData extends OnClickData {
  18. selectedKeys: (string | number)[];
  19. selectedItems: ItemProps[];
  20. }
  21. export interface OnOpenChangeData extends OnClickData {
  22. openKeys: (string | number)[];
  23. }
  24. export interface NavItemType {
  25. props?: ItemProps;
  26. items?: NavItemType[];
  27. [key: string]: any;
  28. }
  29. export interface NavigationAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  30. notifySelect(data: OnSelectData): void;
  31. notifyOpenChange(data: OnOpenChangeData): void;
  32. setIsCollapsed(isCollapsed: boolean): void;
  33. notifyCollapseChange(isCollapsed: boolean): void;
  34. updateItems(items: ItemProps[]): void;
  35. setItemKeysMap(map: { [key: string]: (string | number)[] }): void;
  36. addSelectedKeys(...keys: (string | number)[]): void;
  37. removeSelectedKeys(...keys: (string | number)[]): void;
  38. updateSelectedKeys(keys: (string | number)[]): void;
  39. updateOpenKeys(keys: (string | number)[]): void;
  40. addOpenKeys(...keys: (string | number)[]): void;
  41. removeOpenKeys(...keys: (string | number)[]): void;
  42. setItemsChanged(isChanged: boolean): void;
  43. }
  44. export default class NavigationFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<NavigationAdapter<P, S>, P, S> {
  45. constructor(adapter: NavigationAdapter<P, S>) {
  46. super({ ...adapter });
  47. }
  48. static getZeroParentKeys(itemKeysMap = {}, ...itemKeys: (string | number)[]) {
  49. const willAddKeys = [];
  50. if (itemKeys.length) {
  51. for (const itemKey of itemKeys) {
  52. if (Array.isArray(itemKeysMap[itemKey]) && itemKeysMap[itemKey].length) {
  53. const levelZeroParentKey = itemKeysMap[itemKey][0];
  54. if (!isNullOrUndefined(levelZeroParentKey)) {
  55. willAddKeys.push(levelZeroParentKey);
  56. }
  57. }
  58. }
  59. }
  60. return willAddKeys;
  61. }
  62. static buildItemKeysMap(items: NavItemType[] = [], keysMap = {}, parentKeys: (string | number)[] = [], keyPropName = 'itemKey') {
  63. if (Array.isArray(items) && items.length) {
  64. for (const item of items) {
  65. if (Array.isArray(item)) {
  66. NavigationFoundation.buildItemKeysMap(item, keysMap, [...parentKeys], keyPropName);
  67. } else {
  68. let itemKey;
  69. if (item && typeof item === 'object') {
  70. itemKey = item[keyPropName] || (item.props && item.props[keyPropName]);
  71. }
  72. if (itemKey) {
  73. keysMap[itemKey] = [...parentKeys];
  74. if (Array.isArray(item.items) && item.items.length) {
  75. NavigationFoundation.buildItemKeysMap(
  76. item.items,
  77. keysMap,
  78. [...parentKeys, itemKey],
  79. keyPropName
  80. );
  81. } else if (item.props && item.props.children) {
  82. const children = Array.isArray(item.props.children)
  83. ? item.props.children
  84. : [item.props.children];
  85. NavigationFoundation.buildItemKeysMap(
  86. children,
  87. keysMap,
  88. [...parentKeys, itemKey],
  89. keyPropName
  90. );
  91. }
  92. }
  93. }
  94. }
  95. }
  96. return keysMap;
  97. }
  98. /**
  99. * init is called in constructor and componentDidMount.
  100. * if you want to update state in constructor, please add it to return object;
  101. * if you want to update state in componentDidMount, please call adapter in else logic.
  102. * @param {*} lifecycle
  103. * @returns
  104. */
  105. init(lifecycle: string) {
  106. const { defaultSelectedKeys, selectedKeys } = this.getProps();
  107. let willSelectedKeys = selectedKeys || defaultSelectedKeys || [];
  108. const { itemKeysMap, willOpenKeys, formatedItems } = this.getCalcState();
  109. const parentSelectKeys = this.selectLevelZeroParentKeys(itemKeysMap, willSelectedKeys);
  110. willSelectedKeys = willSelectedKeys.concat(parentSelectKeys);
  111. if (lifecycle === 'constructor') {
  112. return {
  113. selectedKeys: willSelectedKeys,
  114. itemKeysMap,
  115. openKeys: willOpenKeys,
  116. items: formatedItems,
  117. };
  118. } else {
  119. this._adapter.updateSelectedKeys(willSelectedKeys);
  120. this._adapter.setItemKeysMap(itemKeysMap);
  121. this._adapter.updateOpenKeys(willOpenKeys);
  122. this._adapter.updateItems(formatedItems);
  123. this._adapter.setItemsChanged(true);
  124. }
  125. return undefined;
  126. }
  127. /**
  128. * Get the state to be calculated
  129. */
  130. getCalcState() {
  131. const { itemKeysMap, formatedItems } = this.getFormatedItems();
  132. const willOpenKeys = this.getWillOpenKeys(itemKeysMap);
  133. return { itemKeysMap, willOpenKeys, formatedItems };
  134. }
  135. /**
  136. * Calculate formatted items and itemsKeyMap
  137. */
  138. getFormatedItems() {
  139. const { items, children } = this.getProps();
  140. const formatedItems = this.formatItems(items);
  141. const willHandleItems = Array.isArray(items) && items.length ? formatedItems : children;
  142. const itemKeysMap = NavigationFoundation.buildItemKeysMap(willHandleItems);
  143. return {
  144. itemKeysMap,
  145. formatedItems
  146. };
  147. }
  148. /**
  149. * Calculate the keys that will need to be opened soon
  150. * @param {*} itemKeysMap
  151. */
  152. getWillOpenKeys(itemKeysMap: ItemKey2ParentKeysMap) {
  153. const { defaultOpenKeys, openKeys, defaultSelectedKeys, selectedKeys, mode } = this.getProps();
  154. let willOpenKeys = openKeys || defaultOpenKeys || [];
  155. if (
  156. !(Array.isArray(defaultOpenKeys) ||
  157. Array.isArray(openKeys)) && mode === strings.MODE_VERTICAL && (Array.isArray(defaultSelectedKeys) || Array.isArray(selectedKeys))
  158. ) {
  159. const currentSelectedKeys = Array.isArray(selectedKeys) ? selectedKeys : defaultSelectedKeys;
  160. willOpenKeys = this.getShouldOpenKeys(itemKeysMap, currentSelectedKeys);
  161. }
  162. return [...willOpenKeys];
  163. }
  164. getItemKey(item: string | number, keyPropName = 'itemKey') {
  165. if (item && typeof item === 'object') {
  166. return item[keyPropName];
  167. }
  168. return item;
  169. }
  170. getShouldOpenKeys(itemKeysMap: ItemKey2ParentKeysMap = {}, selectedKeys: string | number[] = []) {
  171. const willOpenKeySet = new Set();
  172. if (Array.isArray(selectedKeys) && selectedKeys.length) {
  173. selectedKeys.forEach(item => {
  174. if (item) {
  175. const parentKeys = get(itemKeysMap, this.getItemKey(item));
  176. if (Array.isArray(parentKeys)) {
  177. parentKeys.forEach(k => willOpenKeySet.add(k));
  178. }
  179. }
  180. });
  181. }
  182. return [...willOpenKeySet];
  183. }
  184. destroy() {} // eslint-disable-line
  185. selectLevelZeroParentKeys(itemKeysMap: ItemKey2ParentKeysMap, ...itemKeys: (string | number)[]) {
  186. const _itemKeysMap = isNullOrUndefined(itemKeysMap) ? this.getState('itemKeysMap') : itemKeysMap;
  187. // console.log(itemKeysMap);
  188. const willAddKeys = [];
  189. if (itemKeys.length) {
  190. for (const itemKey of itemKeys) {
  191. if (Array.isArray(_itemKeysMap[itemKey]) && _itemKeysMap[itemKey].length) {
  192. const levelZeroParentKey = _itemKeysMap[itemKey][0];
  193. if (!isNullOrUndefined(levelZeroParentKey)) {
  194. willAddKeys.push(levelZeroParentKey);
  195. }
  196. }
  197. }
  198. }
  199. if (willAddKeys.length) {
  200. return willAddKeys;
  201. }
  202. return [];
  203. }
  204. formatItems(items: ItemProps[] = []) {
  205. const formatedItems = [];
  206. for (const item of items) {
  207. formatedItems.push(new NavItem(item));
  208. }
  209. return formatedItems;
  210. }
  211. handleSelect(data: OnSelectData) {
  212. this._adapter.notifySelect(data);
  213. }
  214. judgeIfOpen(openKeys: (string | number)[], items: NavItemType[]): boolean {
  215. let shouldBeOpen = false;
  216. const _openKeys = Array.isArray(openKeys) ? openKeys : openKeys && [openKeys];
  217. if (_openKeys && Array.isArray(items) && items.length) {
  218. for (const item of items) {
  219. shouldBeOpen = _openKeys.includes(item.itemKey) || this.judgeIfOpen(_openKeys, item.items);
  220. if (shouldBeOpen) {
  221. break;
  222. }
  223. }
  224. }
  225. return shouldBeOpen;
  226. }
  227. handleCollapseChange() {
  228. const isCollapsed = !this.getState('isCollapsed');
  229. if (!this._isControlledComponent('isCollapsed')) {
  230. this._adapter.setIsCollapsed(isCollapsed);
  231. }
  232. this._adapter.notifyCollapseChange(isCollapsed);
  233. }
  234. handleItemsChange(isChanged: boolean) {
  235. this._adapter.setItemsChanged(isChanged);
  236. }
  237. }