refactor: table

pull/4639/head
tangjinzhou 2021-09-02 23:06:52 +08:00
parent b8319bdb38
commit c9db47533f
23 changed files with 1608 additions and 1 deletions

View File

@ -0,0 +1,14 @@
import type { UnwrapRef } from 'vue';
import { reactive, toRef } from 'vue';
/**
* Reactively pick fields from a reactive object
*
* @see https://vueuse.js.org/reactivePick
*/
export function reactivePick<T extends object, K extends keyof T>(
obj: T,
...keys: K[]
): { [S in K]: UnwrapRef<T[S]> } {
return reactive(Object.fromEntries(keys.map(k => [k, toRef(obj, k)]))) as any;
}

View File

@ -0,0 +1,214 @@
import classNames from 'ant-design-vue/es/_util/classNames';
import { isValidElement } from 'ant-design-vue/es/_util/props-util';
import { CSSProperties, defineComponent, HTMLAttributes } from 'vue';
import type {
DataIndex,
ColumnType,
RenderedCell,
CustomizeComponent,
CellType,
DefaultRecordType,
AlignType,
CellEllipsisType,
} from '../interface';
import { getPathValue, validateValue } from '../utils/valueUtil';
function isRenderCell<RecordType = DefaultRecordType>(
data: RenderedCell<RecordType>,
): data is RenderedCell<RecordType> {
return data && typeof data === 'object' && !Array.isArray(data) && !isValidElement(data);
}
export interface CellProps<RecordType = DefaultRecordType> {
prefixCls?: string;
className?: string;
record?: RecordType;
/** `record` index. Not `column` index. */
index?: number;
dataIndex?: DataIndex;
customRender?: ColumnType<RecordType>['customRender'];
component?: CustomizeComponent;
children?: any;
colSpan?: number;
rowSpan?: number;
ellipsis?: CellEllipsisType;
align?: AlignType;
// Fixed
fixLeft?: number | false;
fixRight?: number | false;
firstFixLeft?: boolean;
lastFixLeft?: boolean;
firstFixRight?: boolean;
lastFixRight?: boolean;
// Additional
/** @private Used for `expandable` with nest tree */
appendNode?: any;
additionalProps?: Omit<HTMLAttributes, 'style'> & { style?: CSSProperties };
rowType?: 'header' | 'body' | 'footer';
isSticky?: boolean;
column?: ColumnType<RecordType>;
}
export default defineComponent<CellProps>({
name: 'Cell',
props: [] as any,
slots: ['appendNode'],
setup(props) {
return () => {
const {
prefixCls,
className,
record,
index,
dataIndex,
customRender,
children,
component: Component = 'td',
colSpan,
rowSpan,
fixLeft,
fixRight,
firstFixLeft,
lastFixLeft,
firstFixRight,
lastFixRight,
appendNode,
additionalProps = {},
ellipsis,
align,
rowType,
isSticky,
column,
} = props;
const cellPrefixCls = `${prefixCls}-cell`;
// ==================== Child Node ====================
let cellProps: CellType;
let childNode;
if (validateValue(children)) {
childNode = children;
} else {
const value = getPathValue(record, dataIndex);
// Customize render node
childNode = value;
if (customRender) {
const renderData = customRender({ text: value, value, record, index, column });
if (isRenderCell(renderData)) {
childNode = renderData.children;
cellProps = renderData.props;
} else {
childNode = renderData;
}
}
}
// Not crash if final `childNode` is not validate ReactNode
if (
typeof childNode === 'object' &&
!Array.isArray(childNode) &&
!isValidElement(childNode)
) {
childNode = null;
}
if (ellipsis && (lastFixLeft || firstFixRight)) {
childNode = <span class={`${cellPrefixCls}-content`}>{childNode}</span>;
}
const {
colSpan: cellColSpan,
rowSpan: cellRowSpan,
style: cellStyle,
className: cellClassName,
class: cellClass,
...restCellProps
} = cellProps || {};
const mergedColSpan = cellColSpan !== undefined ? cellColSpan : colSpan;
const mergedRowSpan = cellRowSpan !== undefined ? cellRowSpan : rowSpan;
if (mergedColSpan === 0 || mergedRowSpan === 0) {
return null;
}
// ====================== Fixed =======================
const fixedStyle: CSSProperties = {};
const isFixLeft = typeof fixLeft === 'number';
const isFixRight = typeof fixRight === 'number';
if (isFixLeft) {
fixedStyle.position = 'sticky';
fixedStyle.left = `${fixLeft}px`;
}
if (isFixRight) {
fixedStyle.position = 'sticky';
fixedStyle.right = `${fixRight}px`;
}
// ====================== Align =======================
const alignStyle: CSSProperties = {};
if (align) {
alignStyle.textAlign = align;
}
// ====================== Render ======================
let title: string;
const ellipsisConfig: CellEllipsisType = ellipsis === true ? { showTitle: true } : ellipsis;
if (ellipsisConfig && (ellipsisConfig.showTitle || rowType === 'header')) {
debugger;
if (typeof childNode === 'string' || typeof childNode === 'number') {
title = childNode.toString();
} else if (isValidElement(childNode) && typeof childNode.props.children === 'string') {
title = childNode.props.children;
}
}
const componentProps = {
title,
...restCellProps,
...additionalProps,
colSpan: mergedColSpan && mergedColSpan !== 1 ? mergedColSpan : null,
rowSpan: mergedRowSpan && mergedRowSpan !== 1 ? mergedRowSpan : null,
class: classNames(
cellPrefixCls,
className,
{
[`${cellPrefixCls}-fix-left`]: isFixLeft,
[`${cellPrefixCls}-fix-left-first`]: firstFixLeft,
[`${cellPrefixCls}-fix-left-last`]: lastFixLeft,
[`${cellPrefixCls}-fix-right`]: isFixRight,
[`${cellPrefixCls}-fix-right-first`]: firstFixRight,
[`${cellPrefixCls}-fix-right-last`]: lastFixRight,
[`${cellPrefixCls}-ellipsis`]: ellipsis,
[`${cellPrefixCls}-with-append`]: appendNode,
[`${cellPrefixCls}-fix-sticky`]: (isFixLeft || isFixRight) && isSticky,
},
additionalProps.class,
cellClassName,
cellClass,
),
style: {
...additionalProps.style,
...alignStyle,
...fixedStyle,
...cellStyle,
},
};
return (
<Component {...componentProps}>
{appendNode}
{childNode}
</Component>
);
};
},
});

View File

@ -0,0 +1,37 @@
import type { ColumnType } from './interface';
import { INTERNAL_COL_DEFINE } from './utils/legacyUtil';
export interface ColGroupProps<RecordType> {
colWidths: readonly (number | string)[];
columns?: readonly ColumnType<RecordType>[];
columCount?: number;
}
function ColGroup<RecordType>({ colWidths, columns, columCount }: ColGroupProps<RecordType>) {
const cols = [];
const len = columCount || columns.length;
// Only insert col with width & additional props
// Skip if rest col do not have any useful info
let mustInsert = false;
for (let i = len - 1; i >= 0; i -= 1) {
const width = colWidths[i];
const column = columns && columns[i];
const additionalProps = column && column[INTERNAL_COL_DEFINE];
if (width || additionalProps || mustInsert) {
cols.unshift(
<col
key={i}
style={{ width: typeof width === 'number' ? `${width}px` : width }}
{...additionalProps}
/>,
);
mustInsert = true;
}
}
return <colgroup>{cols}</colgroup>;
}
export default ColGroup;

View File

@ -0,0 +1,127 @@
import classNames from 'ant-design-vue/es/_util/classNames';
import { computed, defineComponent } from 'vue';
import { useInjectTable } from '../context/TableContext';
import type {
ColumnsType,
CellType,
StickyOffsets,
ColumnType,
GetComponentProps,
ColumnGroupType,
DefaultRecordType,
} from '../interface';
import HeaderRow from './HeaderRow';
function parseHeaderRows<RecordType>(
rootColumns: ColumnsType<RecordType>,
): CellType<RecordType>[][] {
const rows: CellType<RecordType>[][] = [];
function fillRowCells(
columns: ColumnsType<RecordType>,
colIndex: number,
rowIndex: number = 0,
): number[] {
// Init rows
rows[rowIndex] = rows[rowIndex] || [];
let currentColIndex = colIndex;
const colSpans: number[] = columns.filter(Boolean).map(column => {
const cell: CellType<RecordType> = {
key: column.key,
className: classNames(column.className, column.class),
children: column.title,
column,
colStart: currentColIndex,
};
let colSpan: number = 1;
const subColumns = (column as ColumnGroupType<RecordType>).children;
if (subColumns && subColumns.length > 0) {
colSpan = fillRowCells(subColumns, currentColIndex, rowIndex + 1).reduce(
(total, count) => total + count,
0,
);
cell.hasSubColumns = true;
}
if ('colSpan' in column) {
({ colSpan } = column);
}
if ('rowSpan' in column) {
cell.rowSpan = column.rowSpan;
}
cell.colSpan = colSpan;
cell.colEnd = cell.colStart + colSpan - 1;
rows[rowIndex].push(cell);
currentColIndex += colSpan;
return colSpan;
});
return colSpans;
}
// Generate `rows` cell data
fillRowCells(rootColumns, 0);
// Handle `rowSpan`
const rowCount = rows.length;
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
rows[rowIndex].forEach(cell => {
if (!('rowSpan' in cell) && !cell.hasSubColumns) {
// eslint-disable-next-line no-param-reassign
cell.rowSpan = rowCount - rowIndex;
}
});
}
return rows;
}
export interface HeaderProps<RecordType = DefaultRecordType> {
columns: ColumnsType<RecordType>;
flattenColumns: readonly ColumnType<RecordType>[];
stickyOffsets: StickyOffsets;
onHeaderRow: GetComponentProps<readonly ColumnType<RecordType>[]>;
}
export default defineComponent<HeaderProps>({
name: 'Header',
props: ['columns', 'flattenColumns', 'stickyOffsets', 'onHeaderRow'] as any,
setup(props) {
const tableContext = useInjectTable();
const rows = computed(() => parseHeaderRows(props.columns));
return () => {
const { prefixCls, getComponent } = tableContext;
const { stickyOffsets, flattenColumns, onHeaderRow } = props;
const WrapperComponent = getComponent(['header', 'wrapper'], 'thead');
const trComponent = getComponent(['header', 'row'], 'tr');
const thComponent = getComponent(['header', 'cell'], 'th');
return (
<WrapperComponent class={`${prefixCls}-thead`}>
{rows.value.map((row, rowIndex) => {
const rowNode = (
<HeaderRow
key={rowIndex}
flattenColumns={flattenColumns}
cells={row}
stickyOffsets={stickyOffsets}
rowComponent={trComponent}
cellComponent={thComponent}
onHeaderRow={onHeaderRow}
index={rowIndex}
/>
);
return rowNode;
})}
</WrapperComponent>
);
};
},
});

View File

@ -0,0 +1,96 @@
import { defineComponent } from 'vue';
import Cell from '../Cell';
import { useInjectTable } from '../context/TableContext';
import {
CellType,
StickyOffsets,
ColumnType,
CustomizeComponent,
GetComponentProps,
DefaultRecordType,
} from '../interface';
import { getCellFixedInfo } from '../utils/fixUtil';
import { getColumnsKey } from '../utils/valueUtil';
export interface RowProps<RecordType = DefaultRecordType> {
cells: readonly CellType<RecordType>[];
stickyOffsets: StickyOffsets;
flattenColumns: readonly ColumnType<RecordType>[];
rowComponent: CustomizeComponent;
cellComponent: CustomizeComponent;
onHeaderRow: GetComponentProps<readonly ColumnType<RecordType>[]>;
index: number;
}
export default defineComponent<RowProps>({
name: 'HeaderRow',
props: [
'cells',
'stickyOffsets',
'flattenColumns',
'rowComponent',
'cellComponent',
'index',
'onHeaderRow',
] as any,
setup(props: RowProps) {
const tableContext = useInjectTable();
return () => {
const { prefixCls, direction } = tableContext;
const {
cells,
stickyOffsets,
flattenColumns,
rowComponent: RowComponent,
cellComponent: CellComponent,
onHeaderRow,
index,
} = props;
let rowProps;
if (onHeaderRow) {
rowProps = onHeaderRow(
cells.map(cell => cell.column),
index,
);
}
const columnsKey = getColumnsKey(cells.map(cell => cell.column));
return (
<RowComponent {...rowProps}>
{cells.map((cell: CellType, cellIndex) => {
const { column } = cell;
const fixedInfo = getCellFixedInfo(
cell.colStart,
cell.colEnd,
flattenColumns,
stickyOffsets,
direction,
);
let additionalProps;
if (column && column.onHeaderCell) {
additionalProps = cell.column.onHeaderCell(column);
}
return (
<Cell
{...cell}
ellipsis={column.ellipsis}
align={column.align}
component={CellComponent}
prefixCls={prefixCls}
key={columnsKey[cellIndex]}
{...fixedInfo}
additionalProps={additionalProps}
rowType="header"
column={column}
/>
);
})}
</RowComponent>
);
};
},
});

View File

@ -0,0 +1,7 @@
function Panel(_, { slots }) {
return <div>{slots.default?.()}</div>;
}
Panel.displayName = 'Panel';
export default Panel;

View File

@ -0,0 +1,43 @@
import {
ColumnType,
DefaultRecordType,
ColumnsType,
TableLayout,
RenderExpandIcon,
ExpandableType,
RowClassName,
TriggerEventHandler,
ExpandedRowRender,
} from '../interface';
import { inject, InjectionKey, provide } from 'vue';
export interface BodyContextProps<RecordType = DefaultRecordType> {
rowClassName: string | RowClassName<RecordType>;
expandedRowClassName: RowClassName<RecordType>;
columns: ColumnsType<RecordType>;
flattenColumns: readonly ColumnType<RecordType>[];
componentWidth: number;
tableLayout: TableLayout;
fixHeader: boolean;
fixColumn: boolean;
horizonScroll: boolean;
indentSize: number;
expandableType: ExpandableType;
expandRowByClick: boolean;
expandedRowRender: ExpandedRowRender<RecordType>;
expandIcon: RenderExpandIcon<RecordType>;
onTriggerExpand: TriggerEventHandler<RecordType>;
expandIconColumnIndex: number;
}
export const BodyContextKey: InjectionKey<BodyContextProps> = Symbol('BodyContextProps');
export const useProvideBody = (props: BodyContextProps) => {
provide(BodyContextKey, props);
};
export const useInjectBody = () => {
return inject(BodyContextKey, {} as BodyContextProps);
};

View File

@ -0,0 +1,16 @@
import { inject, InjectionKey, provide } from 'vue';
import { Key } from '../interface';
interface ResizeContextProps {
onColumnResize: (columnKey: Key, width: number) => void;
}
export const ResizeContextKey: InjectionKey<ResizeContextProps> = Symbol('ResizeContextProps');
export const useProvideResize = (props: ResizeContextProps) => {
provide(ResizeContextKey, props);
};
export const useInjectResize = () => {
return inject(ResizeContextKey, { onColumnResize: () => {} });
};

View File

@ -0,0 +1,28 @@
import { inject, InjectionKey, provide } from 'vue';
import { GetComponent } from '../interface';
import { FixedInfo } from '../utils/fixUtil';
export interface TableContextProps {
// Table context
prefixCls: string;
getComponent: GetComponent;
scrollbarSize: number;
direction: 'ltr' | 'rtl';
fixedInfoList: readonly FixedInfo[];
isSticky: boolean;
}
export const BodyContextKey: InjectionKey<TableContextProps> = Symbol('TableContextProps');
export const useProvideTable = (props: TableContextProps) => {
provide(BodyContextKey, props);
};
export const useInjectTable = () => {
return inject(BodyContextKey, {} as TableContextProps);
};

View File

@ -0,0 +1,238 @@
import { warning } from 'ant-design-vue/es/vc-util/warning';
import { computed, ComputedRef, Ref, watchEffect } from 'vue';
import type {
ColumnsType,
ColumnType,
FixedType,
Key,
GetRowKey,
TriggerEventHandler,
RenderExpandIcon,
ColumnGroupType,
} from '../interface';
import { INTERNAL_COL_DEFINE } from '../utils/legacyUtil';
export function convertChildrenToColumns<RecordType>(
children: any[] = [],
): ColumnsType<RecordType> {
return children.map(({ key, props }) => {
const { children: nodeChildren, ...restProps } = props;
const column = {
key,
...restProps,
};
if (nodeChildren) {
column.children = convertChildrenToColumns(nodeChildren);
}
return column;
});
}
function flatColumns<RecordType>(columns: ColumnsType<RecordType>): ColumnType<RecordType>[] {
return columns.reduce((list, column) => {
const { fixed } = column;
// Convert `fixed='true'` to `fixed='left'` instead
const parsedFixed = fixed === true ? 'left' : fixed;
const subColumns = (column as ColumnGroupType<RecordType>).children;
if (subColumns && subColumns.length > 0) {
return [
...list,
...flatColumns(subColumns).map(subColum => ({
fixed: parsedFixed,
...subColum,
})),
];
}
return [
...list,
{
...column,
fixed: parsedFixed,
},
];
}, []);
}
function warningFixed(flattenColumns: readonly { fixed?: FixedType }[]) {
let allFixLeft = true;
for (let i = 0; i < flattenColumns.length; i += 1) {
const col = flattenColumns[i];
if (allFixLeft && col.fixed !== 'left') {
allFixLeft = false;
} else if (!allFixLeft && col.fixed === 'left') {
warning(false, `Index ${i - 1} of \`columns\` missing \`fixed='left'\` prop.`);
break;
}
}
let allFixRight = true;
for (let i = flattenColumns.length - 1; i >= 0; i -= 1) {
const col = flattenColumns[i];
if (allFixRight && col.fixed !== 'right') {
allFixRight = false;
} else if (!allFixRight && col.fixed === 'right') {
warning(false, `Index ${i + 1} of \`columns\` missing \`fixed='right'\` prop.`);
break;
}
}
}
function revertForRtl<RecordType>(columns: ColumnsType<RecordType>): ColumnsType<RecordType> {
return columns.map(column => {
const { fixed, ...restProps } = column;
// Convert `fixed='left'` to `fixed='right'` instead
let parsedFixed = fixed;
if (fixed === 'left') {
parsedFixed = 'right';
} else if (fixed === 'right') {
parsedFixed = 'left';
}
return {
fixed: parsedFixed,
...restProps,
};
});
}
/**
* Parse `columns` & `children` into `columns`.
*/
function useColumns<RecordType>(
{
prefixCls,
columns: baseColumns,
// children,
expandable,
expandedKeys,
getRowKey,
onTriggerExpand,
expandIcon,
rowExpandable,
expandIconColumnIndex,
direction,
expandRowByClick,
columnWidth,
fixed,
}: {
prefixCls?: Ref<string>;
columns?: Ref<ColumnsType<RecordType>>;
// children?: React.ReactNode;
expandable: Ref<boolean>;
expandedKeys: Ref<Set<Key>>;
getRowKey: GetRowKey<RecordType>;
onTriggerExpand: TriggerEventHandler<RecordType>;
expandIcon?: Ref<RenderExpandIcon<RecordType>>;
rowExpandable?: Ref<(record: RecordType) => boolean>;
expandIconColumnIndex?: Ref<number>;
direction?: Ref<'ltr' | 'rtl'>;
expandRowByClick?: Ref<boolean>;
columnWidth?: Ref<number | string>;
fixed?: Ref<FixedType>;
},
transformColumns: (columns: ColumnsType<RecordType>) => ColumnsType<RecordType>,
): [ComputedRef<ColumnsType<RecordType>>, ComputedRef<readonly ColumnType<RecordType>[]>] {
// const baseColumns = React.useMemo<ColumnsType<RecordType>>(
// () => columns || convertChildrenToColumns(children),
// [columns, children],
// );
// Add expand column
const withExpandColumns = computed<ColumnsType<RecordType>>(() => {
if (expandable.value) {
const expandColIndex = expandIconColumnIndex.value || 0;
const prevColumn = baseColumns[expandColIndex];
let fixedColumn: FixedType | null;
if ((fixed.value === 'left' || fixed.value) && !expandIconColumnIndex.value) {
fixedColumn = 'left';
} else if (
(fixed.value === 'right' || fixed.value) &&
expandIconColumnIndex.value === baseColumns.value.length
) {
fixedColumn = 'right';
} else {
fixedColumn = prevColumn ? prevColumn.fixed : null;
}
const expandedKeysValue = expandedKeys.value;
const rowExpandableValue = rowExpandable.value;
const expandIconValue = expandIcon.value;
const prefixClsValue = prefixCls.value;
const expandRowByClickValue = expandRowByClick.value;
const expandColumn = {
[INTERNAL_COL_DEFINE]: {
class: `${prefixCls.value}-expand-icon-col`,
},
title: '',
fixed: fixedColumn,
class: `${prefixCls.value}-row-expand-icon-cell`,
width: columnWidth.value,
render: (_, record, index) => {
const rowKey = getRowKey(record, index);
const expanded = expandedKeysValue.has(rowKey);
const recordExpandable = rowExpandableValue ? rowExpandableValue(record) : true;
const icon = expandIconValue({
prefixCls: prefixClsValue,
expanded,
expandable: recordExpandable,
record,
onExpand: onTriggerExpand,
});
if (expandRowByClickValue) {
return <span onClick={e => e.stopPropagation()}>{icon}</span>;
}
return icon;
},
};
// Insert expand column in the target position
const cloneColumns = baseColumns.value.slice();
if (expandColIndex >= 0) {
cloneColumns.splice(expandColIndex, 0, expandColumn);
}
return cloneColumns;
}
return baseColumns.value;
});
const mergedColumns = computed(() => {
let finalColumns = withExpandColumns.value;
if (transformColumns) {
finalColumns = transformColumns(finalColumns);
}
// Always provides at least one column for table display
if (!finalColumns.length) {
finalColumns = [
{
customRender: () => null,
},
];
}
return finalColumns;
});
const flattenColumns = computed(() => {
if (direction.value === 'rtl') {
return revertForRtl(flatColumns(mergedColumns.value));
}
return flatColumns(mergedColumns.value);
});
// Only check out of production since it's waste for each render
if (process.env.NODE_ENV !== 'production') {
watchEffect(() => {
setTimeout(() => {
warningFixed(flattenColumns.value);
});
});
}
return [mergedColumns, flattenColumns];
}
export default useColumns;

View File

@ -0,0 +1,85 @@
import type { Ref } from 'vue';
import { computed } from 'vue';
import type { GetRowKey, Key } from '../interface';
// recursion (flat tree structure)
function flatRecord<T>(
record: T,
indent: number,
childrenColumnName: string,
expandedKeys: Set<Key>,
getRowKey: GetRowKey<T>,
) {
const arr = [];
arr.push({
record,
indent,
});
const key = getRowKey(record);
const expanded = expandedKeys?.has(key);
if (record && Array.isArray(record[childrenColumnName]) && expanded) {
// expanded state, flat record
for (let i = 0; i < record[childrenColumnName].length; i += 1) {
const tempArr = flatRecord(
record[childrenColumnName][i],
indent + 1,
childrenColumnName,
expandedKeys,
getRowKey,
);
arr.push(...tempArr);
}
}
return arr;
}
/**
* flat tree data on expanded state
*
* @export
* @template T
* @param {*} data : table data
* @param {string} childrenColumnName :
* @param {Set<Key>} expandedKeys : keys
* @param {GetRowKey<T>} getRowKey : rowKey
* @returns flattened data
*/
export default function useFlattenRecords<T>(
dataRef: Ref<[]>,
childrenColumnNameRef: Ref<string>,
expandedKeysRef: Ref<Set<Key>>,
getRowKey: GetRowKey<T>,
) {
const arr: Ref<{ record: T; indent: number }[]> = computed(() => {
const childrenColumnName = childrenColumnNameRef.value;
const expandedKeys = expandedKeysRef.value;
const data = dataRef.value;
if (expandedKeys?.size) {
const temp: { record: T; indent: number }[] = [];
// collect flattened record
for (let i = 0; i < data?.length; i += 1) {
const record = data[i];
temp.push(...flatRecord<T>(record, 0, childrenColumnName, expandedKeys, getRowKey));
}
return temp;
}
return data?.map(item => {
return {
record: item,
indent: 0,
};
});
});
return arr;
}

View File

@ -0,0 +1,79 @@
import type { Ref, UnwrapRef } from 'vue';
import { getCurrentInstance, onBeforeUnmount, ref } from 'vue';
export type Updater<State> = (prev: State) => State;
/**
* Execute code before next frame but async
*/
export function useLayoutState<State>(
defaultState: State,
): [Ref<State>, (updater: Updater<State>) => void] {
const stateRef = ref<State>(defaultState);
// const [, forceUpdate] = useState({});
const lastPromiseRef = ref<Promise<void>>(null);
const updateBatchRef = ref<Updater<State>[]>([]);
const instance = getCurrentInstance();
function setFrameState(updater: Updater<State>) {
updateBatchRef.value.push(updater);
const promise = Promise.resolve();
lastPromiseRef.value = promise;
promise.then(() => {
if (lastPromiseRef.value === promise) {
const prevBatch = updateBatchRef.value;
const prevState = stateRef.value;
updateBatchRef.value = [];
prevBatch.forEach(batchUpdater => {
stateRef.value = batchUpdater(stateRef.value as State) as UnwrapRef<State>;
});
lastPromiseRef.value = null;
if (prevState !== stateRef.value) {
instance.update();
}
}
});
}
onBeforeUnmount(() => {
lastPromiseRef.value = null;
});
return [stateRef as Ref<State>, setFrameState];
}
/** Lock frame, when frame pass reset the lock. */
export function useTimeoutLock<State>(
defaultState?: State,
): [(state: UnwrapRef<State>) => void, () => UnwrapRef<State> | null] {
const frameRef = ref<State | null>(defaultState || null);
const timeoutRef = ref<number>();
function cleanUp() {
window.clearTimeout(timeoutRef.value);
}
function setState(newState: UnwrapRef<State>) {
frameRef.value = newState;
cleanUp();
timeoutRef.value = window.setTimeout(() => {
frameRef.value = null;
timeoutRef.value = undefined;
}, 100);
}
function getState() {
return frameRef.value;
}
onBeforeUnmount(() => {
cleanUp();
});
return [setState, getState];
}

View File

@ -0,0 +1,40 @@
import canUseDom from '../../_util/canUseDom';
import type { ComputedRef, Ref } from 'vue';
import { computed } from 'vue';
import type { TableSticky } from '../interface';
// fix ssr render
const defaultContainer = canUseDom() ? window : null;
/** Sticky header hooks */
export default function useSticky(
stickyRef: Ref<boolean | TableSticky>,
prefixClsRef: Ref<string>,
): ComputedRef<{
isSticky: boolean;
offsetHeader: number;
offsetSummary: number;
offsetScroll: number;
stickyClassName: string;
container: Window | HTMLElement;
}> {
return computed(() => {
const {
offsetHeader = 0,
offsetSummary = 0,
offsetScroll = 0,
getContainer = () => defaultContainer,
} = typeof stickyRef.value === 'object' ? stickyRef.value : {};
const container = getContainer() || defaultContainer;
const isSticky = !!stickyRef.value;
return {
isSticky,
stickyClassName: isSticky ? `${prefixClsRef.value}-sticky-holder` : '',
offsetHeader,
offsetSummary,
offsetScroll,
container,
};
});
}

View File

@ -0,0 +1,55 @@
import type { ComputedRef, Ref } from 'vue';
import { computed } from 'vue';
import type { StickyOffsets } from '../interface';
/**
* Get sticky column offset width
*/
function useStickyOffsets(
colWidthsRef: Ref<number[]>,
columnCountRef: Ref<number>,
directionRef: Ref<'ltr' | 'rtl'>,
) {
const stickyOffsets: ComputedRef<StickyOffsets> = computed(() => {
const leftOffsets: number[] = [];
const rightOffsets: number[] = [];
let left = 0;
let right = 0;
const colWidths = colWidthsRef.value;
const columnCount = columnCountRef.value;
const direction = directionRef.value;
for (let start = 0; start < columnCount; start += 1) {
if (direction === 'rtl') {
// Left offset
rightOffsets[start] = right;
right += colWidths[start] || 0;
// Right offset
const end = columnCount - start - 1;
leftOffsets[end] = left;
left += colWidths[end] || 0;
} else {
// Left offset
leftOffsets[start] = left;
left += colWidths[start] || 0;
// Right offset
const end = columnCount - start - 1;
rightOffsets[end] = right;
right += colWidths[end] || 0;
}
}
return {
left: leftOffsets,
right: rightOffsets,
};
});
return stickyOffsets;
}
export default useStickyOffsets;

View File

@ -0,0 +1,10 @@
// base rc-table@7.17.2
import Table from './Table';
import { FooterComponents as Summary } from './Footer';
import Column from './sugar/Column';
import ColumnGroup from './sugar/ColumnGroup';
import { INTERNAL_COL_DEFINE } from './utils/legacyUtil';
export { Summary, Column, ColumnGroup, INTERNAL_COL_DEFINE };
export default Table;

View File

@ -0,0 +1,226 @@
/**
* ColumnType which applied in antd: https://ant.design/components/table-cn/#Column
* - defaultSortOrder
* - filterDropdown
* - filterDropdownVisible
* - filtered
* - filteredValue
* - filterIcon
* - filterMultiple
* - filters
* - sorter
* - sortOrder
* - sortDirections
* - onFilter
* - onFilterDropdownVisibleChange
*/
import type { CSSProperties, DefineComponent, FunctionalComponent, HTMLAttributes, Ref } from 'vue';
export type Key = number | string;
export type FixedType = 'left' | 'right' | boolean;
export type DefaultRecordType = Record<string, any>;
export type TableLayout = 'auto' | 'fixed';
// ==================== Row =====================
export type RowClassName<RecordType> = (
record: RecordType,
index: number,
indent: number,
) => string;
// =================== Column ===================
export interface CellType<RecordType = DefaultRecordType> {
key?: Key;
class?: string;
className?: string;
style?: CSSProperties;
children?: any;
column?: ColumnsType<RecordType>[number];
colSpan?: number;
rowSpan?: number;
/** Only used for table header */
hasSubColumns?: boolean;
colStart?: number;
colEnd?: number;
}
export interface RenderedCell<RecordType> {
props?: CellType<RecordType>;
children?: any;
}
export type DataIndex = string | number | readonly (string | number)[];
export type CellEllipsisType = { showTitle?: boolean } | boolean;
interface ColumnSharedType<RecordType> {
title?: any;
key?: Key;
class?: string;
className?: string;
fixed?: FixedType;
onHeaderCell?: GetComponentProps<ColumnsType<RecordType>[number]>;
ellipsis?: CellEllipsisType;
align?: AlignType;
}
export interface ColumnGroupType<RecordType> extends ColumnSharedType<RecordType> {
children: ColumnsType<RecordType>;
}
export type AlignType = 'left' | 'center' | 'right';
export interface ColumnType<RecordType> extends ColumnSharedType<RecordType> {
colSpan?: number;
dataIndex?: DataIndex;
customRender?: (opt: {
value: any;
text: any; // 兼容 V2
record: RecordType;
index: number;
column: ColumnType<RecordType>;
}) => any | RenderedCell<RecordType>;
rowSpan?: number;
width?: number | string;
onCell?: GetComponentProps<RecordType>;
/** @deprecated Please use `onCell` instead */
onCellClick?: (record: RecordType, e: MouseEvent) => void;
}
export type ColumnsType<RecordType = unknown> = readonly (
| ColumnGroupType<RecordType>
| ColumnType<RecordType>
)[];
export type GetRowKey<RecordType> = (record: RecordType, index?: number) => Key;
// ================= Fix Column =================
export interface StickyOffsets {
left: readonly number[];
right: readonly number[];
isSticky?: boolean;
}
// ================= Customized =================
export type GetComponentProps<DataType> = (
data: DataType,
index?: number,
) => Omit<HTMLAttributes, 'style'> & { style?: CSSProperties };
type Component<P> = DefineComponent<P> | FunctionalComponent<P> | string;
export type CustomizeComponent = Component<any>;
export type CustomizeScrollBody<RecordType> = (
data: readonly RecordType[],
info: {
scrollbarSize: number;
ref: Ref<{ scrollLeft: number }>;
onScroll: (info: { currentTarget?: HTMLElement; scrollLeft?: number }) => void;
},
) => any;
export interface TableComponents<RecordType> {
table?: CustomizeComponent;
header?: {
wrapper?: CustomizeComponent;
row?: CustomizeComponent;
cell?: CustomizeComponent;
};
body?:
| CustomizeScrollBody<RecordType>
| {
wrapper?: CustomizeComponent;
row?: CustomizeComponent;
cell?: CustomizeComponent;
};
}
export type GetComponent = (
path: readonly string[],
defaultComponent?: CustomizeComponent,
) => CustomizeComponent;
// =================== Expand ===================
export type ExpandableType = false | 'row' | 'nest';
export interface LegacyExpandableProps<RecordType> {
/** @deprecated Use `expandable.expandedRowKeys` instead */
expandedRowKeys?: Key[];
/** @deprecated Use `expandable.defaultExpandedRowKeys` instead */
defaultExpandedRowKeys?: Key[];
/** @deprecated Use `expandable.expandedRowRender` instead */
expandedRowRender?: ExpandedRowRender<RecordType>;
/** @deprecated Use `expandable.expandRowByClick` instead */
expandRowByClick?: boolean;
/** @deprecated Use `expandable.expandIcon` instead */
expandIcon?: RenderExpandIcon<RecordType>;
/** @deprecated Use `expandable.onExpand` instead */
onExpand?: (expanded: boolean, record: RecordType) => void;
/** @deprecated Use `expandable.onExpandedRowsChange` instead */
onExpandedRowsChange?: (expandedKeys: Key[]) => void;
/** @deprecated Use `expandable.defaultExpandAllRows` instead */
defaultExpandAllRows?: boolean;
/** @deprecated Use `expandable.indentSize` instead */
indentSize?: number;
/** @deprecated Use `expandable.expandIconColumnIndex` instead */
expandIconColumnIndex?: number;
/** @deprecated Use `expandable.expandedRowClassName` instead */
expandedRowClassName?: RowClassName<RecordType>;
/** @deprecated Use `expandable.childrenColumnName` instead */
childrenColumnName?: string;
}
export type ExpandedRowRender<ValueType> = (
record: ValueType,
index: number,
indent: number,
expanded: boolean,
) => any;
export interface RenderExpandIconProps<RecordType> {
prefixCls: string;
expanded: boolean;
record: RecordType;
expandable: boolean;
onExpand: TriggerEventHandler<RecordType>;
}
export type RenderExpandIcon<RecordType> = (props: RenderExpandIconProps<RecordType>) => any;
export interface ExpandableConfig<RecordType> {
expandedRowKeys?: readonly Key[];
defaultExpandedRowKeys?: readonly Key[];
expandedRowRender?: ExpandedRowRender<RecordType>;
expandRowByClick?: boolean;
expandIcon?: RenderExpandIcon<RecordType>;
onExpand?: (expanded: boolean, record: RecordType) => void;
onExpandedRowsChange?: (expandedKeys: readonly Key[]) => void;
defaultExpandAllRows?: boolean;
indentSize?: number;
expandIconColumnIndex?: number;
expandedRowClassName?: RowClassName<RecordType>;
childrenColumnName?: string;
rowExpandable?: (record: RecordType) => boolean;
columnWidth?: number | string;
fixed?: FixedType;
}
// =================== Render ===================
export type PanelRender<RecordType> = (data: readonly RecordType[]) => any;
// =================== Events ===================
export type TriggerEventHandler<RecordType> = (record: RecordType, event: MouseEvent) => void;
// =================== Sticky ===================
export interface TableSticky {
offsetHeader?: number;
offsetSummary?: number;
offsetScroll?: number;
getContainer?: () => Window | HTMLElement;
}

View File

@ -0,0 +1,13 @@
import { FunctionalComponent } from 'vue';
import { ColumnType } from '../interface';
export type ColumnProps<RecordType> = ColumnType<RecordType>;
/* istanbul ignore next */
/**
* This is a syntactic sugar for `columns` prop.
* So HOC will not work on this.
*/
const Column: { <T>(arg: T): FunctionalComponent<ColumnProps<T>> } = () => null;
export default Column;

View File

@ -0,0 +1,13 @@
import { ColumnType } from '../interface';
import { FunctionalComponent } from 'vue';
/* istanbul ignore next */
/**
* This is a syntactic sugar for `columns` prop.
* So HOC will not work on this.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type ColumnGroupProps<RecordType> = ColumnType<RecordType>;
const ColumnGroup: { <T>(arg: T): FunctionalComponent<ColumnGroupProps<T>> } = () => null;
export default ColumnGroup;

View File

@ -0,0 +1,51 @@
import { RenderExpandIconProps, Key, GetRowKey } from '../interface';
export function renderExpandIcon<RecordType>({
prefixCls,
record,
onExpand,
expanded,
expandable,
}: RenderExpandIconProps<RecordType>) {
const expandClassName = `${prefixCls}-row-expand-icon`;
if (!expandable) {
return <span class={[expandClassName, `${prefixCls}-row-spaced`]} />;
}
const onClick = event => {
onExpand(record, event);
event.stopPropagation();
};
return (
<span
class={{
[expandClassName]: true,
[`${prefixCls}-row-expanded`]: expanded,
[`${prefixCls}-row-collapsed`]: !expanded,
}}
onClick={onClick}
/>
);
}
export function findAllChildrenKeys<RecordType>(
data: readonly RecordType[],
getRowKey: GetRowKey<RecordType>,
childrenColumnName: string,
): Key[] {
const keys: Key[] = [];
function dig(list: readonly RecordType[]) {
(list || []).forEach((item, index) => {
keys.push(getRowKey(item, index));
dig((item as any)[childrenColumnName]);
});
}
dig(data);
return keys;
}

View File

@ -0,0 +1,69 @@
import type { StickyOffsets, FixedType } from '../interface';
export interface FixedInfo {
fixLeft: number | false;
fixRight: number | false;
lastFixLeft: boolean;
firstFixRight: boolean;
// For Rtl Direction
lastFixRight: boolean;
firstFixLeft: boolean;
isSticky: boolean;
}
export function getCellFixedInfo(
colStart: number,
colEnd: number,
columns: readonly { fixed?: FixedType }[],
stickyOffsets: StickyOffsets,
direction: 'ltr' | 'rtl',
): FixedInfo {
const startColumn = columns[colStart] || {};
const endColumn = columns[colEnd] || {};
let fixLeft: number;
let fixRight: number;
if (startColumn.fixed === 'left') {
fixLeft = stickyOffsets.left[colStart];
} else if (endColumn.fixed === 'right') {
fixRight = stickyOffsets.right[colEnd];
}
let lastFixLeft = false;
let firstFixRight = false;
let lastFixRight = false;
let firstFixLeft = false;
const nextColumn = columns[colEnd + 1];
const prevColumn = columns[colStart - 1];
if (direction === 'rtl') {
if (fixLeft !== undefined) {
const prevFixLeft = prevColumn && prevColumn.fixed === 'left';
firstFixLeft = !prevFixLeft;
} else if (fixRight !== undefined) {
const nextFixRight = nextColumn && nextColumn.fixed === 'right';
lastFixRight = !nextFixRight;
}
} else if (fixLeft !== undefined) {
const nextFixLeft = nextColumn && nextColumn.fixed === 'left';
lastFixLeft = !nextFixLeft;
} else if (fixRight !== undefined) {
const prevFixRight = prevColumn && prevColumn.fixed === 'right';
firstFixRight = !prevFixRight;
}
return {
fixLeft,
fixRight,
lastFixLeft,
firstFixRight,
lastFixRight,
firstFixLeft,
isSticky: stickyOffsets.isSticky,
};
}

View File

@ -0,0 +1,55 @@
import { warning } from '../../vc-util/warning';
import type { ExpandableConfig, LegacyExpandableProps } from '../interface';
export const INTERNAL_COL_DEFINE = 'RC_TABLE_INTERNAL_COL_DEFINE';
export function getExpandableProps<RecordType>(
props: LegacyExpandableProps<RecordType> & {
expandable?: ExpandableConfig<RecordType>;
},
): ExpandableConfig<RecordType> {
const { expandable, ...legacyExpandableConfig } = props;
if ('expandable' in props) {
return {
...legacyExpandableConfig,
...expandable,
};
}
if (
process.env.NODE_ENV !== 'production' &&
[
'indentSize',
'expandedRowKeys',
'defaultExpandedRowKeys',
'defaultExpandAllRows',
'expandedRowRender',
'expandRowByClick',
'expandIcon',
'onExpand',
'onExpandedRowsChange',
'expandedRowClassName',
'expandIconColumnIndex',
].some(prop => prop in props)
) {
warning(false, 'expanded related props have been moved into `expandable`.');
}
return legacyExpandableConfig;
}
/**
* Returns only data- and aria- key/value pairs
* @param {object} props
*/
export function getDataAndAriaProps(props: object) {
/* eslint-disable no-param-reassign */
return Object.keys(props).reduce((memo, key) => {
if (key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-') {
memo[key] = props[key];
}
return memo;
}, {});
/* eslint-enable */
}

View File

@ -0,0 +1,91 @@
import { Key, DataIndex } from '../interface';
const INTERNAL_KEY_PREFIX = 'RC_TABLE_KEY';
function toArray<T>(arr: T | readonly T[]): T[] {
if (arr === undefined || arr === null) {
return [];
}
return (Array.isArray(arr) ? arr : [arr]) as T[];
}
export function getPathValue<ValueType, ObjectType extends object>(
record: ObjectType,
path: DataIndex,
): ValueType {
// Skip if path is empty
if (!path && typeof path !== 'number') {
return record as unknown as ValueType;
}
const pathList = toArray(path);
let current: ValueType | ObjectType = record;
for (let i = 0; i < pathList.length; i += 1) {
if (!current) {
return null;
}
const prop = pathList[i];
current = current[prop];
}
return current as ValueType;
}
interface GetColumnKeyColumn {
key?: Key;
dataIndex?: DataIndex;
}
export function getColumnsKey(columns: readonly GetColumnKeyColumn[]) {
const columnKeys: Key[] = [];
const keys: Record<Key, boolean> = {};
columns.forEach(column => {
const { key, dataIndex } = column || {};
let mergedKey = key || toArray(dataIndex).join('-') || INTERNAL_KEY_PREFIX;
while (keys[mergedKey]) {
mergedKey = `${mergedKey}_next`;
}
keys[mergedKey] = true;
columnKeys.push(mergedKey);
});
return columnKeys;
}
export function mergeObject<ReturnObject extends object>(
...objects: Partial<ReturnObject>[]
): ReturnObject {
const merged: Partial<ReturnObject> = {};
/* eslint-disable no-param-reassign */
function fillProps(obj: object, clone: object) {
if (clone) {
Object.keys(clone).forEach(key => {
const value = clone[key];
if (value && typeof value === 'object') {
obj[key] = obj[key] || {};
fillProps(obj[key], value);
} else {
obj[key] = value;
}
});
}
}
/* eslint-enable */
objects.forEach(clone => {
fillProps(merged, clone);
});
return merged as ReturnObject;
}
export function validateValue<T>(val: T) {
return val !== null && val !== undefined;
}

View File

@ -5,6 +5,7 @@
"ant-design-vue": ["components/index.ts"], "ant-design-vue": ["components/index.ts"],
"ant-design-vue/es/*": ["components/*"] "ant-design-vue/es/*": ["components/*"]
}, },
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"strictNullChecks": false, "strictNullChecks": false,
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
@ -14,7 +15,6 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noImplicitAny": false, "noImplicitAny": false,
"target": "es6", "target": "es6",
"lib": ["dom", "es2017"],
"skipLibCheck": true, "skipLibCheck": true,
"allowJs": true, "allowJs": true,
"importsNotUsedAsValues": "preserve" "importsNotUsedAsValues": "preserve"