utils.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. /* eslint-disable max-len */
  2. /* eslint-disable no-param-reassign */
  3. /* eslint-disable eqeqeq */
  4. import {
  5. cloneDeepWith,
  6. isEqualWith,
  7. get,
  8. filter,
  9. find,
  10. map,
  11. clone as lodashClone,
  12. each,
  13. findIndex,
  14. some,
  15. includes,
  16. toString,
  17. isFunction
  18. } from 'lodash';
  19. import { strings, numbers } from './constants';
  20. import isNullOrUndefined from '../utils/isNullOrUndefined';
  21. import Logger from '../utils/Logger';
  22. export function cloneDeep(value: any, customizer?: (v: any) => any) {
  23. return cloneDeepWith(value, v => {
  24. if (typeof v === 'function') {
  25. return v;
  26. } else if (typeof customizer === 'function') {
  27. return customizer(v);
  28. }
  29. return undefined;
  30. });
  31. }
  32. export function equalWith(value: any, other: any, customizer?: (...args: any[]) => boolean) {
  33. return isEqualWith(value, other, (objVal, othVal, ...rest) => {
  34. if (typeof objVal === 'function' && typeof othVal === 'function') {
  35. return toString(objVal) === toString(othVal);
  36. }
  37. if (typeof customizer === 'function') {
  38. return customizer(objVal, othVal, ...rest);
  39. }
  40. // If customizer returns undefined, comparisons are handled by isEqual instead
  41. return undefined;
  42. });
  43. }
  44. export function getColumnKey(column: any, keyPropNames: any[]): any {
  45. keyPropNames = Array.isArray(keyPropNames) ? keyPropNames : ['key', 'dataIndex'];
  46. let key = null;
  47. each(keyPropNames, propName => {
  48. key = get(column, propName);
  49. if (key != null) {
  50. return false;
  51. }
  52. return undefined;
  53. });
  54. return key;
  55. }
  56. export function mergeColumns(oldColumns: any[] = [], newColumns: any[] = [], keyPropNames: any[] = null, deep = true) {
  57. const finalColumns: any[] = [];
  58. const clone = deep ? cloneDeep : lodashClone;
  59. map(newColumns, newColumn => {
  60. newColumn = { ...newColumn };
  61. const key = getColumnKey(newColumn, keyPropNames);
  62. const oldColumn = key != null && find(oldColumns, item => getColumnKey(item, keyPropNames) === key);
  63. if (oldColumn) {
  64. finalColumns.push(
  65. clone({
  66. ...oldColumn,
  67. ...newColumn,
  68. })
  69. );
  70. } else {
  71. finalColumns.push(clone(newColumn));
  72. }
  73. });
  74. return finalColumns;
  75. }
  76. /**
  77. *
  78. * @param {Array<number>} arr
  79. * @param {number} [beginIndex] begin index, included
  80. * @param {number} [endIndex] end index, not included
  81. * @returns {number}
  82. */
  83. export function arrayAdd(arr: any[] = [], beginIndex = 0, endIndex?: number) {
  84. beginIndex = beginIndex < 0 || typeof beginIndex !== 'number' ? 0 : beginIndex;
  85. endIndex = endIndex > arr.length || typeof endIndex !== 'number' ? arr.length : endIndex;
  86. let result = 0;
  87. each(arr, (value, index) => {
  88. if (index >= beginIndex && index < endIndex) {
  89. result += typeof value === 'number' && !isNaN(value) ? value : 0;
  90. }
  91. });
  92. return result;
  93. }
  94. export function isLastLeftFixed(columns: Record<string, any>[], column: Record<string, any>, checkKeys = ['key']) {
  95. const leftFixedColumns = filter(columns, col => col.fixed === true || col.fixed === 'left');
  96. const index = findIndex(leftFixedColumns, col =>
  97. checkKeys.every(key => col[key] != null && col[key] === column[key])
  98. );
  99. return leftFixedColumns.length > 0 && index === leftFixedColumns.length - 1;
  100. }
  101. export function isFirstFixedRight(columns: Record<string, any>[], column: Record<string, any>, checkKeys = ['key']) {
  102. const rightFixedColumns = filter(columns, col => col.fixed === 'right');
  103. const index = findIndex(rightFixedColumns, col =>
  104. checkKeys.every(key => col[key] != null && col[key] === column[key])
  105. );
  106. return rightFixedColumns.length > 0 && index === 0;
  107. }
  108. export function isAnyFixed(columns: Record<string, any>[], fixedSet = ['left', true, 'right']) {
  109. if (typeof fixedSet === 'string' || typeof fixedSet === 'boolean') {
  110. fixedSet = [fixedSet];
  111. }
  112. return fixedSet.length > 0 && some(columns, col => fixedSet.includes(col.fixed));
  113. }
  114. export function isAnyFixedRight(columns: Record<string, any>[]) {
  115. return some(columns, col => col.fixed === 'right');
  116. }
  117. export function isFixedLeft(column: Record<string, any>) {
  118. return ['left', true].includes(get(column, 'fixed'));
  119. }
  120. export function isFixedRight(column: Record<string, any>) {
  121. return ['right'].includes(get(column, 'fixed'));
  122. }
  123. export function isFixed(column: Record<string, any>) {
  124. return isFixedLeft(column) || isFixedRight(column);
  125. }
  126. export function isInnerColumnKey(key: string | number) {
  127. return [
  128. strings.DEFAULT_KEY_COLUMN_EXPAND,
  129. strings.DEFAULT_KEY_COLUMN_SCROLLBAR,
  130. strings.DEFAULT_KEY_COLUMN_SELECTION,
  131. ].includes(key as any);
  132. }
  133. export function isExpandedColumn(column: Record<string, any>) {
  134. return get(column, 'key') === strings.DEFAULT_KEY_COLUMN_EXPAND;
  135. }
  136. export function isScrollbarColumn(column: Record<string, any>) {
  137. return get(column, 'key') === strings.DEFAULT_KEY_COLUMN_SCROLLBAR;
  138. }
  139. export function isSelectionColumn(column: Record<string, any>) {
  140. return get(column, 'key') === strings.DEFAULT_KEY_COLUMN_SELECTION;
  141. }
  142. export function filterColumns(columns: Record<string, any>[], ignoreKeys = [strings.DEFAULT_KEY_COLUMN_SCROLLBAR as string]) {
  143. return filter(columns, col => !ignoreKeys.includes(col.key));
  144. }
  145. /**
  146. * get width of scroll bar
  147. * @param {Array} columns
  148. * @returns {Number|undefined}
  149. */
  150. export function getScrollbarColumnWidth(columns: Record<string, any>[] = []) {
  151. const len = columns.length;
  152. if (len) {
  153. const lastColumn = columns[len - 1];
  154. if (get(lastColumn, 'key') === strings.DEFAULT_KEY_COLUMN_SCROLLBAR) {
  155. return get(lastColumn, 'width', 0);
  156. }
  157. }
  158. }
  159. export function getRecordKey(record: Record<string, any>, rowKey: string | number | ((record: any) => string | number)) {
  160. if (rowKey === undefined) {
  161. rowKey = 'key';
  162. }
  163. return typeof rowKey === 'function' ? rowKey(record) : get(record, rowKey);
  164. }
  165. /**
  166. * Determine whether the expandedRowKeys includes a key (rowKey will be added to expandedRowKeys when the expand button is clicked)
  167. * @param {*} expandedRowKeys
  168. * @param {*} key
  169. */
  170. export function isExpanded(expandedRowKeys: (string | number)[], key: string | number) {
  171. return key != null && includes(expandedRowKeys, key);
  172. }
  173. /**
  174. * Determine whether the selectedKeysSet includes the key
  175. * @param {Set} selectedRowKeysSet
  176. * @param {String} key
  177. */
  178. export function isSelected(selectedRowKeysSet: Set<string | number>, key: string | number) {
  179. return key !== null && selectedRowKeysSet.has(key);
  180. }
  181. /**
  182. * Whether the key is included in the disabledRowKeysSet
  183. * @param {Set} disabledRowKeysSet
  184. * @param {String} key
  185. */
  186. export function isDisabled(disabledRowKeysSet: Set<string | number>, key: string | number) {
  187. return key !== null && disabledRowKeysSet.has(key);
  188. }
  189. export function getRecord(data: any[], recordKey: string | number, rowKey: string | number | ((record: any) => string | number)) {
  190. if (rowKey === undefined) {
  191. rowKey = 'key';
  192. }
  193. return find(data, record => recordKey != null && recordKey !== '' && getRecordKey(record, rowKey) === recordKey);
  194. }
  195. export function getRecordChildren(record: Record<string, any>, childrenRecordName: string) {
  196. if (childrenRecordName === undefined) {
  197. childrenRecordName = 'children';
  198. }
  199. return get(record, childrenRecordName);
  200. }
  201. export function genExpandedRowKey(recordKey = '', suffix?: string) {
  202. if (suffix === undefined) {
  203. suffix = '__expanded_row';
  204. }
  205. return recordKey + suffix;
  206. }
  207. export function getDefaultVirtualizedRowConfig(size = '', sectionRow = false) {
  208. const config: { height?: number; minHeight?: number } = {};
  209. if (size === 'small') {
  210. config.height = sectionRow ?
  211. numbers.DEFAULT_VIRTUALIZED_SECTION_ROW_SMALL_HEIGHT :
  212. numbers.DEFAULT_VIRTUALIZED_ROW_SMALL_HEIGHT;
  213. config.minHeight = numbers.DEFAULT_VIRTUALIZED_ROW_SMALL_MIN_HEIGHT;
  214. } else if (size === 'middle') {
  215. config.height = sectionRow ?
  216. numbers.DEFAULT_VIRTUALIZED_SECTION_ROW_MIDDLE_HEIGHT :
  217. numbers.DEFAULT_VIRTUALIZED_ROW_MIDDLE_HEIGHT;
  218. config.minHeight = numbers.DEFAULT_VIRTUALIZED_ROW_MIDDLE_MIN_HEIGHT;
  219. } else {
  220. config.height = sectionRow ?
  221. numbers.DEFAULT_VIRTUALIZED_SECTION_ROW_HEIGHT :
  222. numbers.DEFAULT_VIRTUALIZED_ROW_HEIGHT;
  223. config.minHeight = numbers.DEFAULT_VIRTUALIZED_ROW_MIN_HEIGHT;
  224. }
  225. return config;
  226. }
  227. export function flattenColumns(cols: Record<string, any>[], childrenColumnName = 'children'): Record<string, any>[] {
  228. const list = [];
  229. if (Array.isArray(cols) && cols.length) {
  230. for (const col of cols) {
  231. if (Array.isArray(col[childrenColumnName]) && col[childrenColumnName].length) {
  232. list.push(...flattenColumns(col[childrenColumnName], childrenColumnName));
  233. } else {
  234. warnIfNoDataIndex(col);
  235. list.push(col);
  236. }
  237. }
  238. }
  239. return list;
  240. }
  241. export function assignColumnKeys(columns: Record<string, any>[], childrenColumnName = 'children', level = 0) {
  242. const sameLevelCols: Record<string, any>[] = [];
  243. each(columns, (column, index) => {
  244. if (column.key == null) {
  245. // if user give column a dataIndex, use it for backup
  246. const _index = column.dataIndex || index;
  247. column.key = `${level}-${_index}`;
  248. }
  249. if (Array.isArray(column[childrenColumnName]) && column[childrenColumnName].length) {
  250. sameLevelCols.push(...column[childrenColumnName]);
  251. }
  252. });
  253. if (sameLevelCols.length) {
  254. assignColumnKeys(sameLevelCols, childrenColumnName, level + 1);
  255. }
  256. return columns;
  257. }
  258. export function sliceColumnsByLevel(columns: any[], targetLevel = 0, childrenColumnName = 'children', currentLevel = 0) {
  259. const slicedColumns: any[] = [];
  260. if (Array.isArray(columns) && columns.length && currentLevel <= targetLevel) {
  261. columns.forEach(column => {
  262. const children = column[childrenColumnName];
  263. if (Array.isArray(children) && children.length && currentLevel < targetLevel) {
  264. slicedColumns.push(...sliceColumnsByLevel(children, targetLevel, childrenColumnName, currentLevel + 1));
  265. } else {
  266. slicedColumns.push(column);
  267. }
  268. });
  269. }
  270. return slicedColumns;
  271. }
  272. export function getColumnsByLevel(
  273. columns: Record<string, any>[],
  274. targetLevel = 0,
  275. targetColumns: Record<string, any>[] = [],
  276. currentLevel = 0,
  277. childrenColumnName = 'children'
  278. ) {
  279. if (Array.isArray(columns) && columns.length) {
  280. if (targetLevel === currentLevel) {
  281. targetColumns.push(...columns);
  282. } else {
  283. columns.forEach(column => {
  284. getColumnsByLevel(
  285. column[childrenColumnName],
  286. targetLevel,
  287. targetColumns,
  288. currentLevel + 1,
  289. childrenColumnName
  290. );
  291. });
  292. }
  293. }
  294. return targetColumns;
  295. }
  296. export function getAllLevelColumns(columns: Record<string, any>[], childrenColumnName = 'children') {
  297. const all = [];
  298. if (Array.isArray(columns) && columns.length) {
  299. all.push([...columns]);
  300. const sameLevelColumns: Record<string, any>[] = [];
  301. columns.forEach(column => {
  302. const children = column[childrenColumnName];
  303. if (Array.isArray(children) && children.length) {
  304. sameLevelColumns.push(...children);
  305. }
  306. });
  307. if (sameLevelColumns.length) {
  308. all.push(sameLevelColumns);
  309. }
  310. }
  311. return all;
  312. }
  313. export function getColumnByLevelIndex(columns: Record<string, any>[], index: number, level = 0, childrenColumnName = 'children') {
  314. const allLevelColumns = getAllLevelColumns(columns, childrenColumnName);
  315. return allLevelColumns[level][index];
  316. }
  317. export function findColumn(columns: Record<string, any>[], column: Record<string, any>, childrenColumnName = 'children') {
  318. let found: any;
  319. each(columns, item => {
  320. if (item && item.key != null && !found) {
  321. if (item.key === column.key) {
  322. found = item;
  323. }
  324. }
  325. if (item && Array.isArray(item[childrenColumnName]) && !found) {
  326. found = findColumn(item[childrenColumnName], column, childrenColumnName);
  327. }
  328. if (found) {
  329. return false;
  330. }
  331. return undefined;
  332. });
  333. return found;
  334. }
  335. export function expandBtnShouldInRow(props: ExpandBtnShouldInRowProps) {
  336. const { expandedRowRender, dataSource, hideExpandedColumn, childrenRecordName, rowExpandable } = props;
  337. const hasExpandedRowRender = typeof expandedRowRender === 'function';
  338. return (
  339. (hideExpandedColumn && hasExpandedRowRender) ||
  340. (!hasExpandedRowRender && dataSource.some(record => {
  341. const children = get(record, childrenRecordName);
  342. if ((Array.isArray(children) && children.length) || rowExpandable(record)) {
  343. return true;
  344. } else {
  345. return false;
  346. }
  347. }))
  348. );
  349. }
  350. export type ExpandBtnShouldInRowProps = {
  351. expandedRowRender: (record?: Record<string, any>, index?: number, expanded?: boolean) => any;
  352. dataSource: Record<string, any>[];
  353. hideExpandedColumn: boolean;
  354. childrenRecordName: string;
  355. rowExpandable: (record?: Record<string, any>) => boolean;
  356. };
  357. /**
  358. * merge query
  359. * @param {*} query
  360. * @param {*} queries
  361. */
  362. export function mergeQueries(query: Record<string, any>, queries: Record<string, any>[] = []) {
  363. let _mergedQuery;
  364. const idx = queries.findIndex(item => {
  365. if (query.dataIndex === item.dataIndex) {
  366. _mergedQuery = { ...item, ...query };
  367. return true;
  368. }
  369. return false;
  370. });
  371. if (idx > -1) {
  372. queries.splice(idx, 1, _mergedQuery);
  373. } else {
  374. queries.push(_mergedQuery);
  375. }
  376. return [...queries];
  377. }
  378. /**
  379. * Replace the width of the newColumns column with the width of the column after resize
  380. * @param {Object[]} columns columns retain the column width after resize
  381. * @param {Object[]} newColumns
  382. */
  383. export function withResizeWidth(columns: Record<string, any>[], newColumns: Record<string, any>[]) {
  384. const _newColumns = cloneDeep(newColumns);
  385. for (const column of columns) {
  386. if (!isNullOrUndefined(column.width)) {
  387. const currentColumn = column.key;
  388. const columnIndex = findIndex(_newColumns, item => (item as any).key === currentColumn);
  389. if (columnIndex !== -1) {
  390. _newColumns[columnIndex].width = get(column, 'width');
  391. }
  392. }
  393. }
  394. return _newColumns;
  395. }
  396. /**
  397. * Pure function version of the same function in table foundation
  398. * This is not accessible in getDerivedStateFromProps, so fork one out
  399. */
  400. export function getAllDisabledRowKeys({ dataSource, getCheckboxProps, childrenRecordName, rowKey }: GetAllDisabledRowKeysProps): (string | number)[] {
  401. const disabledRowKeys = [];
  402. if (Array.isArray(dataSource) && dataSource.length && typeof getCheckboxProps === 'function') {
  403. for (const record of dataSource) {
  404. const props = getCheckboxProps(record);
  405. const recordKey = typeof rowKey === 'function' ? rowKey(record) : get(record, rowKey);
  406. if (props && props.disabled) {
  407. disabledRowKeys.push(recordKey);
  408. }
  409. const children = get(record, childrenRecordName);
  410. if (Array.isArray(children) && children.length) {
  411. const keys = getAllDisabledRowKeys({ dataSource: children, getCheckboxProps });
  412. disabledRowKeys.push(...keys);
  413. }
  414. }
  415. }
  416. return disabledRowKeys;
  417. }
  418. export interface GetAllDisabledRowKeysProps {
  419. dataSource: Record<string, any>[];
  420. getCheckboxProps: (record?: Record<string, any>) => any;
  421. childrenRecordName?: string;
  422. rowKey?: string | number | ((record: Record<string, any>) => string | number);
  423. }
  424. export function warnIfNoDataIndex(column: Record<string, any>) {
  425. if (typeof column === 'object' && column !== null) {
  426. const { filters, sorter, dataIndex } = column;
  427. const logger = new Logger('[@douyinfe/semi-ui Table]');
  428. if ((Array.isArray(filters) || isFunction(sorter)) && isNullOrUndefined(dataIndex) ) {
  429. logger.warn(`The column with sorter or filter must pass the 'dataIndex' prop`);
  430. }
  431. }
  432. }
  433. /**
  434. * Whether is tree table
  435. */
  436. export function isTreeTable({ dataSource, childrenRecordName = 'children' }: { dataSource: Record<string, any>; childrenRecordName?: string; }) {
  437. let flag = false;
  438. if (Array.isArray(dataSource)) {
  439. for (const data of dataSource) {
  440. const children = get(data, childrenRecordName);
  441. if (Array.isArray(children) && children.length) {
  442. flag = true;
  443. break;
  444. }
  445. }
  446. }
  447. return flag;
  448. }