refactor: replace react-drag-listview with @dnd-kit/sortable (#660)
* refactor: replace react-drag-listview with @dnd-kit/sortable * fix: components are different every time * fix: incorrect border line * fix: when dragging is enabled for the first time, dragging is invalid * fix: the items property of SortableContext must be an array of strings * fix: onRowDragEnd may be non-exists * fix: incorrect row key * feat: wrap SortableContext only when dragging is enabled * fix: improve logic
This commit is contained in:
parent
49a4ab4818
commit
c697ef85a6
@ -4,11 +4,12 @@ import { ArrayField } from '@formily/core';
|
|||||||
import { ISchema, observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
import { ISchema, observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
import { Table as AntdTable, TableColumnProps } from 'antd';
|
import { Table as AntdTable, TableColumnProps } from 'antd';
|
||||||
import { default as classNames, default as cls } from 'classnames';
|
import { default as classNames, default as cls } from 'classnames';
|
||||||
import React from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import ReactDragListView from 'react-drag-listview';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DndContext } from '../..';
|
import { DndContext } from '../..';
|
||||||
import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../';
|
import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../';
|
||||||
|
import { SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
|
||||||
const isColumnComponent = (schema: Schema) => {
|
const isColumnComponent = (schema: Schema) => {
|
||||||
return schema['x-component']?.endsWith('.Column') > -1;
|
return schema['x-component']?.endsWith('.Column') > -1;
|
||||||
@ -67,64 +68,35 @@ const useTableColumns = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const components = {
|
|
||||||
header: {
|
|
||||||
wrapper: (props) => {
|
|
||||||
return (
|
|
||||||
<DndContext>
|
|
||||||
<thead {...props} />
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: (props) => {
|
|
||||||
return (
|
|
||||||
<th
|
|
||||||
{...props}
|
|
||||||
className={cls(
|
|
||||||
props.className,
|
|
||||||
css`
|
|
||||||
max-width: 300px;
|
|
||||||
white-space: nowrap;
|
|
||||||
&:hover .general-schema-designer {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
wrapper: (props) => {
|
|
||||||
return (
|
|
||||||
<DndContext>
|
|
||||||
<tbody {...props} />
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
row: (props) => {
|
|
||||||
return <tr {...props} />;
|
|
||||||
},
|
|
||||||
cell: (props) => (
|
|
||||||
<td
|
|
||||||
{...props}
|
|
||||||
className={classNames(
|
|
||||||
props.className,
|
|
||||||
css`
|
|
||||||
max-width: 300px;
|
|
||||||
white-space: nowrap;
|
|
||||||
.nb-read-pretty-input-number {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const SortHandle = () => {
|
const topActiveClass = css`
|
||||||
return <MenuOutlined className={'drag-handle'} style={{ cursor: 'grab' }} />;
|
& > td {
|
||||||
|
border-top: 2px solid rgba(241, 139, 98, 0.6) !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const bottomActiveClass = css`
|
||||||
|
& > td {
|
||||||
|
border-bottom: 2px solid rgba(241, 139, 98, 0.6) !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SortableRow = (props) => {
|
||||||
|
const id = props['data-row-key']?.toString()
|
||||||
|
const { setNodeRef, isOver, active, over } = useSortable({
|
||||||
|
id
|
||||||
|
})
|
||||||
|
|
||||||
|
const className = (active?.data.current?.sortable.index ?? -1) > (over?.data.current?.sortable.index ?? -1) ? topActiveClass : bottomActiveClass
|
||||||
|
|
||||||
|
return <tr ref={active?.id !== id ? setNodeRef : null} {...props} className={classNames({ [className]: active && isOver })} />
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const SortHandle = (props) => {
|
||||||
|
const { listeners } = useSortable({
|
||||||
|
id: props.id
|
||||||
|
})
|
||||||
|
return <MenuOutlined {...listeners} style={{ cursor: 'grab' }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TableIndex = (props) => {
|
const TableIndex = (props) => {
|
||||||
@ -154,43 +126,127 @@ const usePaginationProps = (pagination1, pagination2) => {
|
|||||||
|
|
||||||
export const Table: any = observer((props: any) => {
|
export const Table: any = observer((props: any) => {
|
||||||
const field = useField<ArrayField>();
|
const field = useField<ArrayField>();
|
||||||
const columns = useTableColumns();
|
const columns = useTableColumns();
|
||||||
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
|
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
|
||||||
const { pagination: pagination2, ...others2 } = useProps?.() || {};
|
const { pagination: pagination2, ...others2 } = useProps?.() || {};
|
||||||
const {
|
const {
|
||||||
dragSort = false,
|
dragSort = false,
|
||||||
showIndex = true,
|
showIndex = true,
|
||||||
onRowDragEnd,
|
|
||||||
onRowSelectionChange,
|
onRowSelectionChange,
|
||||||
onChange: onTableChange,
|
onChange: onTableChange,
|
||||||
rowSelection,
|
rowSelection,
|
||||||
|
rowKey,
|
||||||
...others
|
...others
|
||||||
} = { ...others1, ...others2 } as any;
|
} = { ...others1, ...others2 } as any;
|
||||||
|
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {}))
|
||||||
const paginationProps = usePaginationProps(pagination1, pagination2);
|
const paginationProps = usePaginationProps(pagination1, pagination2);
|
||||||
|
|
||||||
|
const components = useMemo(() => {
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
wrapper: (props) => {
|
||||||
|
return (
|
||||||
|
<DndContext>
|
||||||
|
<thead {...props} />
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: (props) => {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
{...props}
|
||||||
|
className={cls(
|
||||||
|
props.className,
|
||||||
|
css`
|
||||||
|
max-width: 300px;
|
||||||
|
white-space: nowrap;
|
||||||
|
&:hover .general-schema-designer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
wrapper: (props) => {
|
||||||
|
return (
|
||||||
|
<DndContext onDragEnd={(e) => {
|
||||||
|
if (!e.active || !e.over) {
|
||||||
|
console.warn('move cancel')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromIndex = e.active?.data.current?.sortable?.index
|
||||||
|
const toIndex = e.over?.data.current?.sortable?.index
|
||||||
|
const from = field.value[fromIndex];
|
||||||
|
const to = field.value[toIndex];
|
||||||
|
field.move(fromIndex, toIndex);
|
||||||
|
onRowDragEnd({ fromIndex, toIndex, from, to });
|
||||||
|
}}>
|
||||||
|
<tbody {...props} />
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
row: (props) => {
|
||||||
|
return <SortableRow {...props}></SortableRow>
|
||||||
|
},
|
||||||
|
cell: (props) => (
|
||||||
|
<td
|
||||||
|
{...props}
|
||||||
|
className={classNames(
|
||||||
|
props.className,
|
||||||
|
css`
|
||||||
|
max-width: 300px;
|
||||||
|
white-space: nowrap;
|
||||||
|
.nb-read-pretty-input-number {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [field, onRowDragEnd, dragSort])
|
||||||
|
|
||||||
|
const defaultRowKey = (record: any) => {
|
||||||
|
return field.value?.indexOf?.(record);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRowKey = (record: any) => {
|
||||||
|
if (typeof rowKey === 'string') {
|
||||||
|
return record[rowKey]?.toString();
|
||||||
|
} else {
|
||||||
|
return (rowKey ?? defaultRowKey)(record)?.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const restProps = {
|
const restProps = {
|
||||||
rowSelection: rowSelection
|
rowSelection: rowSelection
|
||||||
? {
|
? {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
selectedRowKeys: field?.data?.selectedRowKeys || [],
|
selectedRowKeys: field?.data?.selectedRowKeys || [],
|
||||||
onChange(selectedRowKeys: any[], selectedRows: any[]) {
|
onChange(selectedRowKeys: any[], selectedRows: any[]) {
|
||||||
field.data = field.data || {};
|
field.data = field.data || {};
|
||||||
field.data.selectedRowKeys = selectedRowKeys;
|
field.data.selectedRowKeys = selectedRowKeys;
|
||||||
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
||||||
},
|
},
|
||||||
renderCell: (checked, record, index, originNode) => {
|
renderCell: (checked, record, index, originNode) => {
|
||||||
if (!dragSort && !showIndex) {
|
if (!dragSort && !showIndex) {
|
||||||
return originNode;
|
return originNode;
|
||||||
}
|
}
|
||||||
const current = props?.pagination?.current;
|
const current = props?.pagination?.current;
|
||||||
const pageSize = props?.pagination?.pageSize || 20;
|
const pageSize = props?.pagination?.pageSize || 20;
|
||||||
if (current) {
|
if (current) {
|
||||||
index = index + (current - 1) * pageSize;
|
index = index + (current - 1) * pageSize;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
checked ? 'checked' : null,
|
checked ? 'checked' : null,
|
||||||
css`
|
css`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -213,27 +269,27 @@ export const Table: any = observer((props: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
checked ? 'checked' : null,
|
checked ? 'checked' : null,
|
||||||
css`
|
css`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{dragSort && <SortHandle />}
|
{dragSort && <SortHandle id={getRowKey(record)} />}
|
||||||
{showIndex && <TableIndex index={index} />}
|
{showIndex && <TableIndex index={index} />}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'nb-origin-node',
|
'nb-origin-node',
|
||||||
checked ? 'checked' : null,
|
checked ? 'checked' : null,
|
||||||
css`
|
css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 50%;
|
right: 50%;
|
||||||
transform: translateX(50%);
|
transform: translateX(50%);
|
||||||
@ -241,20 +297,27 @@ export const Table: any = observer((props: any) => {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{originNode}
|
{originNode}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
},
|
);
|
||||||
...rowSelection,
|
},
|
||||||
}
|
...rowSelection,
|
||||||
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
const defaultRowKey = (record: any) => {
|
|
||||||
return field.value?.indexOf?.(record);
|
const SortableWrapper = useCallback<React.FC>(({ children }) => {
|
||||||
};
|
return dragSort ? React.createElement(SortableContext, {
|
||||||
|
items: field.value.map(getRowKey),
|
||||||
|
children: children,
|
||||||
|
}) : React.createElement(React.Fragment, {
|
||||||
|
children
|
||||||
|
})
|
||||||
|
}, [field, dragSort])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={css`
|
className={css`
|
||||||
@ -264,20 +327,9 @@ export const Table: any = observer((props: any) => {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<ReactDragListView
|
<SortableWrapper>
|
||||||
handleSelector={'.drag-handle'}
|
|
||||||
onDragEnd={async (fromIndex, toIndex) => {
|
|
||||||
const from = field.value[fromIndex];
|
|
||||||
const to = field.value[toIndex];
|
|
||||||
field.move(fromIndex, toIndex);
|
|
||||||
onRowDragEnd({ fromIndex, toIndex, from, to });
|
|
||||||
}}
|
|
||||||
lineClassName={css`
|
|
||||||
border-bottom: 2px solid rgba(241, 139, 98, 0.6) !important;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<AntdTable
|
<AntdTable
|
||||||
rowKey={defaultRowKey}
|
rowKey={rowKey ?? defaultRowKey}
|
||||||
{...others}
|
{...others}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
pagination={paginationProps}
|
pagination={paginationProps}
|
||||||
@ -290,7 +342,7 @@ export const Table: any = observer((props: any) => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={field?.value?.slice?.()}
|
dataSource={field?.value?.slice?.()}
|
||||||
/>
|
/>
|
||||||
</ReactDragListView>
|
</SortableWrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user