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 { Table as AntdTable, TableColumnProps } from 'antd';
|
||||
import { default as classNames, default as cls } from 'classnames';
|
||||
import React from 'react';
|
||||
import ReactDragListView from 'react-drag-listview';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DndContext } from '../..';
|
||||
import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../';
|
||||
import { SortableContext, useSortable } from '@dnd-kit/sortable';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
|
||||
const isColumnComponent = (schema: Schema) => {
|
||||
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 = () => {
|
||||
return <MenuOutlined className={'drag-handle'} style={{ cursor: 'grab' }} />;
|
||||
const topActiveClass = css`
|
||||
& > 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) => {
|
||||
@ -154,43 +126,127 @@ const usePaginationProps = (pagination1, pagination2) => {
|
||||
|
||||
export const Table: any = observer((props: any) => {
|
||||
const field = useField<ArrayField>();
|
||||
const columns = useTableColumns();
|
||||
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
|
||||
const { pagination: pagination2, ...others2 } = useProps?.() || {};
|
||||
const {
|
||||
const columns = useTableColumns();
|
||||
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
|
||||
const { pagination: pagination2, ...others2 } = useProps?.() || {};
|
||||
const {
|
||||
dragSort = false,
|
||||
showIndex = true,
|
||||
onRowDragEnd,
|
||||
onRowSelectionChange,
|
||||
onChange: onTableChange,
|
||||
rowSelection,
|
||||
rowKey,
|
||||
...others
|
||||
} = { ...others1, ...others2 } as any;
|
||||
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {}))
|
||||
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 = {
|
||||
rowSelection: rowSelection
|
||||
? {
|
||||
type: 'checkbox',
|
||||
selectedRowKeys: field?.data?.selectedRowKeys || [],
|
||||
onChange(selectedRowKeys: any[], selectedRows: any[]) {
|
||||
field.data = field.data || {};
|
||||
field.data.selectedRowKeys = selectedRowKeys;
|
||||
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
||||
},
|
||||
renderCell: (checked, record, index, originNode) => {
|
||||
if (!dragSort && !showIndex) {
|
||||
return originNode;
|
||||
}
|
||||
const current = props?.pagination?.current;
|
||||
const pageSize = props?.pagination?.pageSize || 20;
|
||||
if (current) {
|
||||
index = index + (current - 1) * pageSize;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
checked ? 'checked' : null,
|
||||
css`
|
||||
type: 'checkbox',
|
||||
selectedRowKeys: field?.data?.selectedRowKeys || [],
|
||||
onChange(selectedRowKeys: any[], selectedRows: any[]) {
|
||||
field.data = field.data || {};
|
||||
field.data.selectedRowKeys = selectedRowKeys;
|
||||
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
||||
},
|
||||
renderCell: (checked, record, index, originNode) => {
|
||||
if (!dragSort && !showIndex) {
|
||||
return originNode;
|
||||
}
|
||||
const current = props?.pagination?.current;
|
||||
const pageSize = props?.pagination?.pageSize || 20;
|
||||
if (current) {
|
||||
index = index + (current - 1) * pageSize;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
checked ? 'checked' : null,
|
||||
css`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -213,27 +269,27 @@ export const Table: any = observer((props: any) => {
|
||||
}
|
||||
}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
checked ? 'checked' : null,
|
||||
css`
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
checked ? 'checked' : null,
|
||||
css`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
{dragSort && <SortHandle />}
|
||||
{showIndex && <TableIndex index={index} />}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'nb-origin-node',
|
||||
checked ? 'checked' : null,
|
||||
css`
|
||||
)}
|
||||
>
|
||||
{dragSort && <SortHandle id={getRowKey(record)} />}
|
||||
{showIndex && <TableIndex index={index} />}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'nb-origin-node',
|
||||
checked ? 'checked' : null,
|
||||
css`
|
||||
position: absolute;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
@ -241,20 +297,27 @@ export const Table: any = observer((props: any) => {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
{originNode}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{originNode}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
...rowSelection,
|
||||
}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
...rowSelection,
|
||||
}
|
||||
: 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 (
|
||||
<div
|
||||
className={css`
|
||||
@ -264,20 +327,9 @@ export const Table: any = observer((props: any) => {
|
||||
}
|
||||
`}
|
||||
>
|
||||
<ReactDragListView
|
||||
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;
|
||||
`}
|
||||
>
|
||||
<SortableWrapper>
|
||||
<AntdTable
|
||||
rowKey={defaultRowKey}
|
||||
rowKey={rowKey ?? defaultRowKey}
|
||||
{...others}
|
||||
{...restProps}
|
||||
pagination={paginationProps}
|
||||
@ -290,7 +342,7 @@ export const Table: any = observer((props: any) => {
|
||||
columns={columns}
|
||||
dataSource={field?.value?.slice?.()}
|
||||
/>
|
||||
</ReactDragListView>
|
||||
</SortableWrapper>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user