From f5350aa72f0d40832068219bd2a4a32228d7bafe Mon Sep 17 00:00:00 2001 From: Bart Louwers Date: Fri, 4 Jul 2025 23:43:11 +0200 Subject: [PATCH] Replace react-sortable-hoc with dnd-kit --- package-lock.json | 82 ++++++++++++++++++++++---------- package.json | 3 +- src/components/App.tsx | 3 +- src/components/LayerList.tsx | 61 ++++++++++++++++++------ src/components/LayerListItem.tsx | 49 +++++++++++++++---- 5 files changed, 147 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index 186c46d7..4e080ede 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "2.1.1", "license": "MIT", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "@mapbox/mapbox-gl-rtl-text": "^0.3.0", "@maplibre/maplibre-gl-geocoder": "^1.9.0", "@maplibre/maplibre-gl-inspect": "^1.7.1", @@ -54,7 +56,6 @@ "react-i18next": "^15.6.0", "react-icon-base": "^2.1.2", "react-icons": "^5.5.0", - "react-sortable-hoc": "^2.0.0", "reconnecting-websocket": "^4.4.0", "slugify": "^1.6.6", "string-hash": "^1.1.3", @@ -680,6 +681,59 @@ "ms": "^2.1.1" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@dual-bundle/import-meta-resolve": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", @@ -7551,14 +7605,6 @@ "node": ">= 0.4" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -10967,21 +11013,6 @@ "react-dom": ">=16.8" } }, - "node_modules/react-sortable-hoc": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz", - "integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==", - "dependencies": { - "@babel/runtime": "^7.2.0", - "invariant": "^2.2.4", - "prop-types": "^15.5.7" - }, - "peerDependencies": { - "prop-types": "^15.5.7", - "react": "^16.3.0 || ^17.0.0", - "react-dom": "^16.3.0 || ^17.0.0" - } - }, "node_modules/reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -12662,8 +12693,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tunnel-agent": { "version": "0.6.0", diff --git a/package.json b/package.json index 5d1541db..9703234c 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "react-i18next": "^15.6.0", "react-icon-base": "^2.1.2", "react-icons": "^5.5.0", - "react-sortable-hoc": "^2.0.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "reconnecting-websocket": "^4.4.0", "slugify": "^1.6.6", "string-hash": "^1.1.3", diff --git a/src/components/App.tsx b/src/components/App.tsx index 68bd8eb3..8b6052ed 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -36,7 +36,6 @@ import LayerWatcher from '../libs/layerwatcher' import tokens from '../config/tokens.json' import isEqual from 'lodash.isequal' import Debug from '../libs/debug' -import { SortEnd } from 'react-sortable-hoc'; import { MapOptions } from 'maplibre-gl'; // Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed. @@ -534,7 +533,7 @@ export default class App extends React.Component { }) } - onMoveLayer = (move: SortEnd) => { + onMoveLayer = (move: {oldIndex: number; newIndex: number}) => { let { oldIndex, newIndex } = move; let layers = this.state.mapStyle.layers; oldIndex = clamp(oldIndex, 0, layers.length-1); diff --git a/src/components/LayerList.tsx b/src/components/LayerList.tsx index 3b58d463..ec08c637 100644 --- a/src/components/LayerList.tsx +++ b/src/components/LayerList.tsx @@ -6,7 +6,18 @@ import LayerListGroup from './LayerListGroup' import LayerListItem from './LayerListItem' import ModalAdd from './ModalAdd' -import {SortEndHandler, SortableContainer} from 'react-sortable-hoc'; +import { + DndContext, + PointerSensor, + useSensor, + useSensors, + closestCenter, + DragEndEvent, +} from '@dnd-kit/core'; +import { + SortableContext, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; import type {LayerSpecification} from 'maplibre-gl'; import generateUniqueId from '../libs/document-uid'; import { findClosestCommonPrefix, layerPrefix } from '../libs/layer'; @@ -242,7 +253,6 @@ class LayerListContainerInternal extends React.Component 1, 'maputnik-layer-list-item--error': !!layerError })} - index={idx} key={layer.key} id={layer.key} layerId={layer.id} @@ -319,20 +329,43 @@ class LayerListContainerInternal extends React.Component ) type LayerListProps = LayerListContainerProps & { - onMoveLayer: SortEndHandler + onMoveLayer: (move: {oldIndex: number; newIndex: number}) => void }; -export default class LayerList extends React.Component { - render() { - return false} - /> - } +export default function LayerList(props: LayerListProps) { + const sensors = useSensors(useSensor(PointerSensor)); + + const buildItemIds = () => { + const ids: string[] = []; + const layerIdCount = new Map(); + for (const layer of props.layers) { + const count = layerIdCount.get(layer.id) ?? 0; + ids.push(`layers-list-${layer.id}-${count}`); + layerIdCount.set(layer.id, count + 1); + } + return ids; + }; + + const handleDragEnd = (event: DragEndEvent) => { + const {active, over} = event; + if (!over) return; + const ids = buildItemIds(); + const oldIndex = ids.indexOf(String(active.id)); + const newIndex = ids.indexOf(String(over.id)); + if (oldIndex !== newIndex) { + props.onMoveLayer({oldIndex, newIndex}); + } + }; + + const itemIds = buildItemIds(); + + return ( + + + + + + ); } diff --git a/src/components/LayerListItem.tsx b/src/components/LayerListItem.tsx index 254900d5..cf891496 100644 --- a/src/components/LayerListItem.tsx +++ b/src/components/LayerListItem.tsx @@ -5,16 +5,20 @@ import classnames from 'classnames' import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md' import IconLayer from './IconLayer' -import {SortableElement, SortableHandle} from 'react-sortable-hoc' +import {useSortable} from '@dnd-kit/sortable' +import {CSS} from '@dnd-kit/utilities' type DraggableLabelProps = { layerId: string layerType: string + dragAttributes?: React.HTMLAttributes + dragListeners?: React.HTMLAttributes }; -const DraggableLabel = SortableHandle((props: DraggableLabelProps) => { - return
+const DraggableLabel = (props: DraggableLabelProps) => { + const {dragAttributes, dragListeners} = props; + return
{ {props.layerId}
-}); +}; type IconActionProps = { action: string @@ -80,6 +84,10 @@ type LayerListItemProps = { onLayerCopy?(...args: unknown[]): unknown onLayerDestroy?(...args: unknown[]): unknown onLayerVisibilityToggle?(...args: unknown[]): unknown + dragAttributes?: React.HTMLAttributes + dragListeners?: React.HTMLAttributes + dragRef?: (node: HTMLElement | null) => void + dragStyle?: React.CSSProperties }; class LayerListItem extends React.Component { @@ -105,8 +113,9 @@ class LayerListItem extends React.Component { const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide'; return
  • this.props.onLayerSelect(this.props.layerIndex)} data-wd-key={"layer-list-item:"+this.props.layerId} className={classnames({ @@ -114,7 +123,12 @@ class LayerListItem extends React.Component { "maputnik-layer-list-item-selected": this.props.isSelected, [this.props.className!]: true, })}> - + { } } -const LayerListItemSortable = SortableElement((props: LayerListItemProps) => ); +export default function LayerListItemSortable(props: LayerListItemProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + } = useSortable({id: props.id!}); -export default LayerListItemSortable; + const style = { + transform: CSS.Transform.toString(transform), + transition, + } as React.CSSProperties; + + return ; +}