Skip to content

Commit

Permalink
feat: 新增拖拽宽度保存至本地功能
Browse files Browse the repository at this point in the history
  • Loading branch information
hemengke1997 committed Jan 8, 2022
1 parent 108cd7a commit c717369
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 41 deletions.
88 changes: 67 additions & 21 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,97 @@ import { option } from './config';
import isEmpty from 'lodash.isempty';
import useThrottleEffect from './utils/useThrottleEffect';
import useDebounceFn from './utils/useDebounceFn';
import { depthFirstSearch, getUniqueId, ResizableUniqIdPrefix } from './utils';
import { depthFirstSearch, ResizableUniqIdPrefix } from './utils';
import useSafeState from './utils/useSafeState';
import useLocalColumns from './utils/useLocalColumns';
import { GETKEY } from './utils/useGetDataIndexColumns';

type useTableResizableHeaderProps<ColumnType> = {
export type ColumnsState = {
width: number;
};

export type ColumnsStateType = {
/**
* 持久化的类型,支持 localStorage 和 sessionStorage
*
* @param localStorage 设置在关闭浏览器后也是存在的
* @param sessionStorage 关闭浏览器后会丢失
*/
persistenceType?: 'localStorage' | 'sessionStorage';
/** 持久化的key,用于存储到 storage 中 */
persistenceKey?: string;
};

export type useTableResizableHeaderProps<ColumnType> = {
columns: ColumnType[] | undefined;
/** @description 最后一列不能拖动,设置最后一列的最小展示宽度,默认120 */
defaultWidth?: number;
/** @description 拖动最小宽度 默认120 */
minConstraints?: number;
/** @description 拖动最大宽度 默认无穷 */
maxConstraints?: number;
/** @description 是否缓存columns宽度,避免rerender时宽度重置 */
/** @description 是否缓存宽度 */
cache?: boolean;
/** @description 列状态的配置,可以用来操作列拖拽宽度 */
columnsState?: ColumnsStateType;
};

type CacheType = { width: number; index: number };
type Width = number | string;

const WIDTH = 120;
export type ColumnOriginType<T> = {
width?: Width;
dataIndex?: React.Key;
key?: React.Key;
title?: React.ReactNode | string;
children?: T[];
resizable?: boolean;
ellipsis?: any;
};

const getKey = 'dataIndex';
type CacheType = { width?: Width; index: number };

function useTableResizableHeader<ColumnType extends Record<string, any>>(
const WIDTH = 120;

function useTableResizableHeader<ColumnType extends ColumnOriginType<ColumnType> = Record<string, any>>(
props: useTableResizableHeaderProps<ColumnType>,
) {
const { columns, defaultWidth = WIDTH, minConstraints = WIDTH, maxConstraints = Infinity, cache = true } = props;
const {
columns: columnsProp,
defaultWidth = WIDTH,
minConstraints = WIDTH,
maxConstraints = Infinity,
cache = true,
columnsState,
} = props;

// column的宽度缓存,避免render导致columns宽度重置
// add column width cache to avoid column's width reset after render
const widthCache = React.useRef<Map<React.Key, CacheType>>(new Map());

const [resizableColumns, setResizableColumns] = useSafeState<ColumnType[]>(columns || []);
const [resizableColumns, setResizableColumns] = useSafeState<ColumnType[]>([]);

const { localColumns: columns, resetColumns } = useLocalColumns({
columnsState,
columns: columnsProp,
resizableColumns,
});

const [tableWidth, setTableWidth] = useSafeState<number>();

const [triggerRender, forceRender] = React.useReducer((s) => s + 1, 0);

const onMount = React.useCallback(
(id: string) => (width: number) => {
(id: React.Key | undefined) => (width?: number) => {
if (width) {
setResizableColumns((t) => {
const nextColumns = depthFirstSearch(t, (col) => col[getKey] === id, width);
const nextColumns = depthFirstSearch(t, (col) => col[GETKEY] === id, width);

const kvMap = new Map<React.Key, CacheType>();

function dig(cols: ColumnType[]) {
cols.forEach((col, i) => {
const key = col[getKey];
kvMap.set(key, { width: col?.width, index: i });
const key = col[GETKEY];
kvMap.set(key ?? '', { width: col?.width, index: i });
if (col?.children) {
dig(col.children);
}
Expand All @@ -74,26 +117,28 @@ function useTableResizableHeader<ColumnType extends Record<string, any>>(
const getColumns = React.useCallback(
(list: ColumnType[]) => {
const trulyColumns = list?.filter((item) => !isEmpty(item));
const c = trulyColumns.map((col, index) => {
const c = trulyColumns.map((col) => {
return {
...col,
children: col?.children?.length ? getColumns(col.children) : undefined,
onHeaderCell: (column: ColumnType) => {
return {
title: typeof col?.title === 'string' ? col?.title : '',
width: cache ? widthCache.current?.get(column[getKey])?.width || column?.width : column?.width,
onMount: onMount(column?.[getKey]),
onResize: onResize(column?.[getKey]),
width: cache ? widthCache.current?.get(column[GETKEY] ?? '')?.width || column?.width : column?.width,
resizable: column.resizable,
onMount: onMount(column?.[GETKEY]),
onResize: onResize(column?.[GETKEY]),
minWidth: minConstraints,
maxWidth: maxConstraints,
triggerRender,
};
},
width: cache ? widthCache.current?.get(col[getKey])?.width || col?.width : col?.width,
width: cache ? widthCache.current?.get(col[GETKEY] ?? '')?.width || col?.width : col?.width,
ellipsis: typeof col.ellipsis !== 'undefined' ? col.ellipsis : true,
[getKey]: col[getKey] || col.key || getUniqueId(index),
[GETKEY]: col[GETKEY] || col.key,
};
}) as ColumnType[];

return c;
},
[onMount, onResize, widthCache.current, cache],
Expand All @@ -120,9 +165,9 @@ function useTableResizableHeader<ColumnType extends Record<string, any>>(

(function loop(cls: ColumnType[]) {
for (let i = 0; i < cls.length; i++) {
width += Number(cls[i].width) || columns?.[columns.length - 1].width || defaultWidth;
width += Number(cls[i].width) || Number(columns?.[columns.length - 1].width) || defaultWidth;
if (cls[i].children) {
loop(cls[i].children);
loop(cls[i].children as ColumnType[]);
}
}
})(resizableColumns);
Expand Down Expand Up @@ -151,6 +196,7 @@ function useTableResizableHeader<ColumnType extends Record<string, any>>(
resizableColumns,
components,
tableWidth,
resetColumns,
};
}

Expand Down
4 changes: 0 additions & 4 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,3 @@ export function depthFirstSearch<T extends Record<string, any> & { children?: T[
}

export const ResizableUniqIdPrefix = 'resizable-table-id';

export function getUniqueId(index: number) {
return `${ResizableUniqIdPrefix}${index}`;
}
35 changes: 35 additions & 0 deletions src/utils/useGetDataIndexColumns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useMemo } from 'react';
import { ResizableUniqIdPrefix } from '.';
import { ColumnOriginType } from '..';
import useMemoizedFn from './useMemoizedFn';

export const GETKEY = 'dataIndex';

export function getUniqueId(index: number) {
return `${ResizableUniqIdPrefix}-${index}`;
}

/*
** 如果columns没有dataIndex,则按规则添加一个不重复的dataIndex
*/

function useGetDataIndexColumns<T extends ColumnOriginType<T>>(columns: T[] | undefined) {
const getColumns = useMemoizedFn((list: T[] | undefined) => {
const trulyColumns = list;
const c = trulyColumns?.map((col, index) => {
return {
...col,
children: col?.children?.length ? getColumns(col.children) : undefined,
[GETKEY]: col[GETKEY] || col.key || getUniqueId(index),
};
});

return c;
});

const dataIndexColumns = useMemo(() => getColumns(columns), [getColumns]) as T[] | undefined;

return dataIndexColumns || columns;
}

export default useGetDataIndexColumns;
109 changes: 109 additions & 0 deletions src/utils/useLocalColumns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useEffect, useMemo } from 'react';
import type { ColumnOriginType, ColumnsStateType } from '..';
import useGetDataIndexColumns from './useGetDataIndexColumns';
import useMemoizedFn from './useMemoizedFn';

type LocalColumnsProp<T> = {
columnsState?: ColumnsStateType;
resizableColumns?: T[];
columns?: T[];
};

function useLocalColumns<T extends ColumnOriginType<T>>({
columnsState,
resizableColumns,
columns,
}: LocalColumnsProp<T>) {
// 列设置需要每一个column都有dataIndex或key
const columnsProp = useGetDataIndexColumns(columns);

// 初始化本地columns
const initLocalColumns = useMemoizedFn(() => {
const { persistenceType, persistenceKey } = columnsState || {};

if (!persistenceKey || !persistenceType) {
return columnsProp;
}
if (typeof window === 'undefined') return columnsProp;

/** 从持久化中读取数据 */
const storage = window[persistenceType];

try {
const localResizableColumns = JSON.parse(storage?.getItem(persistenceKey) || '{}')?.resizableColumns;
const c = columnsProp?.map((col, i) => ({
...col,
width:
(localResizableColumns as T[])?.find((item, j) => {
if (item.dataIndex && col.dataIndex && item.dataIndex === col.dataIndex) {
return true;
}
if (item.key && col.key && item.key === col.key) {
return true;
}
if (i === j && !col.dataIndex && !col.key) {
return true;
}
return false;
})?.width || col.width,
}));
return c;
} catch (error) {
console.error(error);
}
});

const [localColumns, setLocalColumns] = React.useState<T[] | undefined>(initLocalColumns);

useEffect(() => {
if (!localColumns?.length) {
setLocalColumns(columnsProp);
} else {
setLocalColumns(initLocalColumns());
}
}, [columnsProp]);

/**
* 把resizableColumns存储在本地
*/
React.useEffect(() => {
const { persistenceType, persistenceKey } = columnsState || {};

if (!persistenceKey || !persistenceType || !resizableColumns) {
return;
}
if (typeof window === 'undefined') return;
/** 给持久化中设置数据 */
const storage = window[persistenceType];
try {
storage.setItem(
persistenceKey,
JSON.stringify({
...JSON.parse(storage?.getItem(persistenceKey) || '{}'),
resizableColumns: resizableColumns.map((col) => ({
dataIndex: col.dataIndex,
key: col.key,
title: col.title,
width: col.width,
})),
}),
);
} catch (error) {
console.error(error);
}
}, [resizableColumns]);

/**
* reset
*/
const resetColumns = useMemoizedFn(() => {
setLocalColumns([...(columnsProp || [])]);
});

return {
localColumns: useMemo(() => localColumns, [localColumns]),
resetColumns,
};
}

export default useLocalColumns;
31 changes: 31 additions & 0 deletions src/utils/useMemoizedFn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useMemo, useRef } from 'react';

type noop = (...args: any[]) => any;

function useMemoizedFn<T extends noop>(fn: T) {
if (process.env.NODE_ENV === 'development') {
if (typeof fn !== 'function') {
// eslint-disable-next-line no-console
console.error(`useMemoizedFn expected parameter is a function, got ${typeof fn}`);
}
}

const fnRef = useRef<T>(fn);

// why not write `fnRef.current = fn`?
// https://github.com/alibaba/hooks/issues/728
fnRef.current = useMemo(() => fn, [fn]);

const memoizedFn = useRef<T>();
if (!memoizedFn.current) {
// eslint-disable-next-line func-names
memoizedFn.current = function (...args) {
// eslint-disable-next-line @typescript-eslint/no-invalid-this
return fnRef.current.apply(this, args);
} as T;
}

return memoizedFn.current;
}

export default useMemoizedFn;
16 changes: 0 additions & 16 deletions src/utils/useUniqueId.ts

This file was deleted.

0 comments on commit c717369

Please sign in to comment.