mirror of
https://github.com/zoriya/flood.git
synced 2026-06-06 12:02:13 +00:00
SortableList: switch to clauderic/dnd-kit
This commit is contained in:
@@ -1,42 +1,38 @@
|
||||
import {arrayMove, SortableContext, verticalListSortingStrategy} from '@dnd-kit/sortable';
|
||||
import classnames from 'classnames';
|
||||
import {HTML5toTouch} from 'rdndmb-html5-to-touch';
|
||||
import {DndProvider} from 'react-dnd-multi-backend';
|
||||
import {DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor} from '@dnd-kit/core';
|
||||
import {FC, MouseEvent, ReactNode, useState} from 'react';
|
||||
import {restrictToParentElement, restrictToVerticalAxis} from '@dnd-kit/modifiers';
|
||||
|
||||
import SortableListItem from './SortableListItem';
|
||||
|
||||
export type ListItem = {
|
||||
id: string;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
interface SortableListProps {
|
||||
id: string;
|
||||
className: string;
|
||||
lockedIDs: Array<string>;
|
||||
items: Array<ListItem>;
|
||||
renderItem: (item: ListItem, index: number) => ReactNode;
|
||||
items: string[];
|
||||
renderItem: (id: string, index: number) => ReactNode;
|
||||
onMouseDown?: (event: MouseEvent) => void;
|
||||
onMove?: (items: this['items']) => void;
|
||||
onDrop?: (items: this['items']) => void;
|
||||
}
|
||||
|
||||
const SortableList: FC<SortableListProps> = ({
|
||||
className,
|
||||
id: listID,
|
||||
items,
|
||||
lockedIDs,
|
||||
renderItem,
|
||||
onMouseDown,
|
||||
onMove,
|
||||
onDrop,
|
||||
}: SortableListProps) => {
|
||||
const [currentItems, setCurrentItems] = useState(items);
|
||||
const classes = classnames('sortable-list', className);
|
||||
|
||||
const keyboardSensor = useSensor(KeyboardSensor);
|
||||
const mouseSensor = useSensor(MouseSensor, {activationConstraint: {distance: 10}});
|
||||
const touchSensor = useSensor(TouchSensor, {activationConstraint: {distance: 10}});
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{width: '100%'}}
|
||||
css={{width: '100%', touchAction: 'none'}}
|
||||
role="none"
|
||||
onMouseDown={(event) => {
|
||||
if (onMouseDown) {
|
||||
@@ -44,53 +40,50 @@ const SortableList: FC<SortableListProps> = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DndProvider options={HTML5toTouch}>
|
||||
<ul className={classes}>
|
||||
{currentItems.map((item, index) => {
|
||||
const {id, visible} = item;
|
||||
return (
|
||||
<SortableListItem
|
||||
list={listID}
|
||||
id={id}
|
||||
index={index}
|
||||
isLocked={lockedIDs.includes(id)}
|
||||
isVisible={visible}
|
||||
key={id}
|
||||
onDrop={() => {
|
||||
if (onDrop) {
|
||||
onDrop(currentItems);
|
||||
}
|
||||
}}
|
||||
onMove={(dragIndex, hoverIndex) => {
|
||||
const draggedItem = currentItems[dragIndex];
|
||||
<DndContext
|
||||
sensors={[keyboardSensor, mouseSensor, touchSensor]}
|
||||
onDragEnd={({active, over}) => {
|
||||
if (over == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newItems = currentItems.slice();
|
||||
if (active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the item being dragged.
|
||||
newItems.splice(dragIndex, 1);
|
||||
// Add the item being dragged in its new position.
|
||||
newItems.splice(hoverIndex, 0, draggedItem);
|
||||
const newItems = arrayMove(
|
||||
items,
|
||||
items.findIndex((id) => id === active.id),
|
||||
items.findIndex((id) => id === over.id),
|
||||
);
|
||||
|
||||
setCurrentItems(newItems);
|
||||
setCurrentItems(newItems);
|
||||
|
||||
if (onMove) {
|
||||
onMove(newItems);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{renderItem(item, index)}
|
||||
</SortableListItem>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</DndProvider>
|
||||
if (onDrop) {
|
||||
onDrop(newItems);
|
||||
}
|
||||
}}
|
||||
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
|
||||
autoScroll={false}
|
||||
>
|
||||
<SortableContext items={currentItems} strategy={verticalListSortingStrategy}>
|
||||
<ul className={classes}>
|
||||
{currentItems.map((id, index) => {
|
||||
return (
|
||||
<SortableListItem id={id} disabled={lockedIDs.includes(id)} key={id}>
|
||||
{renderItem(id, index)}
|
||||
</SortableListItem>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SortableList.defaultProps = {
|
||||
onMouseDown: undefined,
|
||||
onMove: undefined,
|
||||
onDrop: undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,102 +1,40 @@
|
||||
import classnames from 'classnames';
|
||||
import {DragElementWrapper, DragPreviewOptions, DragSource, DragSourceOptions, DropTarget} from 'react-dnd';
|
||||
import {FC, ReactNode, useEffect} from 'react';
|
||||
import flow from 'lodash/flow';
|
||||
import {getEmptyImage} from 'react-dnd-html5-backend';
|
||||
import {CSS} from '@dnd-kit/utilities';
|
||||
import {FC, ReactNode} from 'react';
|
||||
import {useSortable} from '@dnd-kit/sortable';
|
||||
|
||||
import {Lock} from '@client/ui/icons';
|
||||
|
||||
interface SortableListItemProps {
|
||||
children?: ReactNode;
|
||||
list: string;
|
||||
id: string;
|
||||
index: number;
|
||||
isVisible: boolean;
|
||||
isDragging?: boolean;
|
||||
isLocked?: boolean;
|
||||
onDrop: () => void;
|
||||
onMove: (sourceIndex: number, targetIndex: number) => void;
|
||||
connectDragPreview: DragElementWrapper<DragPreviewOptions>;
|
||||
connectDragSource: DragElementWrapper<DragSourceOptions>;
|
||||
connectDropTarget: DragElementWrapper<never>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SortableListItem: FC<SortableListItemProps> = (props: SortableListItemProps) => {
|
||||
const {children, isDragging, isLocked, connectDragPreview, connectDragSource, connectDropTarget} = props;
|
||||
const {children, id, disabled} = props;
|
||||
const {attributes, setNodeRef, listeners, transform, transition, isDragging} = useSortable({id, disabled});
|
||||
|
||||
useEffect(() => {
|
||||
connectDragPreview(getEmptyImage(), {
|
||||
captureDraggingState: true,
|
||||
});
|
||||
});
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return connectDragSource(
|
||||
connectDropTarget(
|
||||
<div
|
||||
className={classnames('sortable-list__item', {
|
||||
'sortable-list__item--is-dragging': isDragging,
|
||||
'sortable-list__item--is-locked': isLocked,
|
||||
})}
|
||||
>
|
||||
{isLocked ? <Lock /> : null}
|
||||
{children}
|
||||
</div>,
|
||||
),
|
||||
return (
|
||||
<div
|
||||
className={classnames('sortable-list__item', {
|
||||
'sortable-list__item--is-dragging': isDragging,
|
||||
'sortable-list__item--is-locked': disabled,
|
||||
})}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
{disabled ? <Lock /> : null}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default flow([
|
||||
DragSource(
|
||||
'globally-draggable-item',
|
||||
{
|
||||
beginDrag({list, id, index, isVisible}: SortableListItemProps) {
|
||||
return {list, id, index, isVisible};
|
||||
},
|
||||
|
||||
canDrag({isLocked}: SortableListItemProps) {
|
||||
if (isLocked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
(connect, monitor) => ({
|
||||
connectDragPreview: connect.dragPreview(),
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
),
|
||||
DropTarget(
|
||||
'globally-draggable-item',
|
||||
{
|
||||
drop({onDrop}: SortableListItemProps) {
|
||||
if (onDrop) {
|
||||
onDrop();
|
||||
}
|
||||
},
|
||||
|
||||
hover(props, monitor) {
|
||||
const item: SortableListItemProps = monitor.getItem();
|
||||
|
||||
// Don't replace items with themselves
|
||||
if (props.isLocked || item.index === props.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't drop item to another list
|
||||
if (item.list !== props.list) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onMove(item.index, props.index);
|
||||
item.index = props.index;
|
||||
},
|
||||
},
|
||||
(connect) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
}),
|
||||
),
|
||||
])(SortableListItem) as FC<
|
||||
Omit<SortableListItemProps, 'connectDragPreview' | 'connectDragSource' | 'connectDropTarget'>
|
||||
>;
|
||||
export default SortableListItem;
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Trans} from '@lingui/react';
|
||||
import {Checkbox} from '@client/ui';
|
||||
import DiskUsageStore from '@client/stores/DiskUsageStore';
|
||||
import SettingStore from '@client/stores/SettingStore';
|
||||
import SortableList, {ListItem} from '@client/components/general/SortableList';
|
||||
import SortableList from '@client/components/general/SortableList';
|
||||
|
||||
import type {FloodSettings} from '@shared/types/FloodSettings';
|
||||
|
||||
@@ -13,8 +13,8 @@ interface MountPointsListProps {
|
||||
}
|
||||
|
||||
const MountPointsList: FC<MountPointsListProps> = ({onSettingsChange}: MountPointsListProps) => {
|
||||
const [diskItems, setDiskItems] = useState<ListItem[]>(
|
||||
((): ListItem[] => {
|
||||
const [diskItems, setDiskItems] = useState(
|
||||
(() => {
|
||||
const {mountPoints} = SettingStore.floodSettings;
|
||||
const disks = Object.assign(
|
||||
{},
|
||||
@@ -40,24 +40,28 @@ const MountPointsList: FC<MountPointsListProps> = ({onSettingsChange}: MountPoin
|
||||
})(),
|
||||
);
|
||||
|
||||
const diskItemVisiblity = diskItems.reduce((memo, {id, visible}) => {
|
||||
memo[id] = visible;
|
||||
return memo;
|
||||
}, {} as Record<string, boolean>);
|
||||
|
||||
return (
|
||||
<SortableList
|
||||
id="disks"
|
||||
className="sortable-list--disks"
|
||||
items={diskItems.slice()}
|
||||
items={diskItems.map(({id}) => id)}
|
||||
lockedIDs={[]}
|
||||
onDrop={(items: Array<ListItem>): void => {
|
||||
setDiskItems(items);
|
||||
onDrop={(items) => {
|
||||
setDiskItems(items.map((id) => ({id, visible: diskItemVisiblity[id]})));
|
||||
}}
|
||||
renderItem={(item: ListItem) => {
|
||||
const {id, visible} = item;
|
||||
|
||||
renderItem={(id) => {
|
||||
const checkbox = (
|
||||
<span className="sortable-list__content sortable-list__content--secondary">
|
||||
<Checkbox
|
||||
defaultChecked={visible}
|
||||
defaultChecked={diskItemVisiblity[id]}
|
||||
id={id}
|
||||
onClick={(event) => {
|
||||
diskItemVisiblity[id] = (event.target as HTMLInputElement).checked;
|
||||
|
||||
const newItems = diskItems.map((disk) => {
|
||||
if (disk.id === id) {
|
||||
return {...disk, visible: (event.target as HTMLInputElement).checked};
|
||||
|
||||
+18
-12
@@ -1,4 +1,4 @@
|
||||
import {FC, ReactNode, useRef, useState} from 'react';
|
||||
import {FC, useRef, useState} from 'react';
|
||||
import {Trans} from '@lingui/react';
|
||||
|
||||
import {Checkbox} from '@client/ui';
|
||||
@@ -7,12 +7,11 @@ import SettingStore from '@client/stores/SettingStore';
|
||||
import TorrentListColumns from '@client/constants/TorrentListColumns';
|
||||
|
||||
import type {FloodSettings} from '@shared/types/FloodSettings';
|
||||
import type {TorrentListColumn} from '@client/constants/TorrentListColumns';
|
||||
|
||||
import SortableList from '../../../general/SortableList';
|
||||
import Tooltip from '../../../general/Tooltip';
|
||||
|
||||
import type {ListItem} from '../../../general/SortableList';
|
||||
|
||||
interface TorrentListColumnsListProps {
|
||||
torrentListViewSize: FloodSettings['torrentListViewSize'];
|
||||
onSettingsChange: (changedSettings: Partial<FloodSettings>) => void;
|
||||
@@ -23,6 +22,7 @@ const TorrentListColumnsList: FC<TorrentListColumnsListProps> = ({
|
||||
onSettingsChange,
|
||||
}: TorrentListColumnsListProps) => {
|
||||
const tooltipRef = useRef<Tooltip>(null);
|
||||
|
||||
const [torrentListColumns, setTorrentListColumns] = useState([
|
||||
...SettingStore.floodSettings.torrentListColumns.filter((column) => TorrentListColumns[column.id] != null).slice(),
|
||||
...Object.keys(TorrentListColumns)
|
||||
@@ -33,25 +33,28 @@ const TorrentListColumnsList: FC<TorrentListColumnsListProps> = ({
|
||||
})),
|
||||
]);
|
||||
|
||||
const torrentListColumnVisiblity = torrentListColumns.reduce((memo, {id, visible}) => {
|
||||
memo[id] = visible;
|
||||
return memo;
|
||||
}, {} as Record<string, boolean>);
|
||||
|
||||
const lockedIDs =
|
||||
torrentListViewSize === 'expanded' ? ['name', 'eta', 'downRate', 'percentComplete', 'downTotal', 'upRate'] : [];
|
||||
|
||||
return (
|
||||
<SortableList
|
||||
id="torrent-details"
|
||||
className="sortable-list--torrent-details"
|
||||
items={torrentListColumns}
|
||||
items={torrentListColumns.map(({id}) => id)}
|
||||
lockedIDs={lockedIDs}
|
||||
onMouseDown={(): void => {
|
||||
tooltipRef.current?.dismissTooltip();
|
||||
}}
|
||||
onDrop={(items: Array<ListItem>): void => {
|
||||
const newItems = items.slice();
|
||||
onDrop={(items) => {
|
||||
const newItems = items.map((id) => ({id, visible: torrentListColumnVisiblity[id]}));
|
||||
onSettingsChange({torrentListColumns: newItems as FloodSettings['torrentListColumns']});
|
||||
setTorrentListColumns(newItems);
|
||||
}}
|
||||
renderItem={(item: ListItem, index: number): ReactNode => {
|
||||
const {id, visible} = item as FloodSettings['torrentListColumns'][number];
|
||||
renderItem={(id, index) => {
|
||||
let checkbox = null;
|
||||
let warning = null;
|
||||
|
||||
@@ -59,17 +62,20 @@ const TorrentListColumnsList: FC<TorrentListColumnsListProps> = ({
|
||||
checkbox = (
|
||||
<span className="sortable-list__content sortable-list__content--secondary">
|
||||
<Checkbox
|
||||
defaultChecked={visible}
|
||||
defaultChecked={torrentListColumnVisiblity[id]}
|
||||
id={id}
|
||||
onClick={(event) => {
|
||||
torrentListColumnVisiblity[id] = (event.target as HTMLInputElement).checked;
|
||||
|
||||
const changedTorrentListColumns = torrentListColumns.map((column) => ({
|
||||
id: column.id,
|
||||
visible: column.id === id ? (event.target as HTMLInputElement).checked : column.visible,
|
||||
}));
|
||||
|
||||
setTorrentListColumns(changedTorrentListColumns);
|
||||
onSettingsChange({
|
||||
torrentListColumns: changedTorrentListColumns as FloodSettings['torrentListColumns'],
|
||||
});
|
||||
setTorrentListColumns(changedTorrentListColumns);
|
||||
}}
|
||||
>
|
||||
<Trans id="settings.ui.torrent.details.enabled" />
|
||||
@@ -100,7 +106,7 @@ const TorrentListColumnsList: FC<TorrentListColumnsListProps> = ({
|
||||
<div className="sortable-list__content sortable-list__content__wrapper">
|
||||
{warning}
|
||||
<span className="sortable-list__content sortable-list__content--primary">
|
||||
<Trans id={TorrentListColumns[id]} />
|
||||
<Trans id={TorrentListColumns[id as TorrentListColumn]} />
|
||||
</span>
|
||||
{checkbox}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user