utils.ts 17 KB

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