@jswork/react-dnd-list 0.0.0-stage → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +21 -0
- package/README.md +88 -2
- package/dist/components/index.d.ts +15 -0
- package/dist/components/types.d.ts +42 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +6 -0
- package/dist/style.css +8 -0
- package/dist/style.scss +9 -0
- package/package.json +90 -4
- package/src/components/index.tsx +232 -0
- package/src/components/style.scss +9 -0
- package/src/components/types.ts +53 -0
- package/src/main.ts +13 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 afei <1290657123@qq.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,89 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-dnd-list
|
|
2
|
+
> A lightweight, accessible draggable list component built with dnd-kit + @jswork/react-list.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
[![version][version-image]][version-url]
|
|
5
|
+
[![license][license-image]][license-url]
|
|
6
|
+
|
|
7
|
+
## installation
|
|
8
|
+
```shell
|
|
9
|
+
pnpm add @jswork/react-dnd-list
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
> `react` / `react-dom` 是 peerDependencies(>=18)。dnd-kit 与 `@jswork/react-list` 已随包安装。
|
|
13
|
+
|
|
14
|
+
## usage
|
|
15
|
+
1. import css
|
|
16
|
+
```scss
|
|
17
|
+
@import "~@jswork/react-dnd-list/dist/style.css";
|
|
18
|
+
```
|
|
19
|
+
2. import js
|
|
20
|
+
```tsx
|
|
21
|
+
import React, { useState } from 'react';
|
|
22
|
+
import ReactDndList from '@jswork/react-dnd-list';
|
|
23
|
+
|
|
24
|
+
const initial = [
|
|
25
|
+
{ id: 'a', name: 'Alice' },
|
|
26
|
+
{ id: 'b', name: 'Bob' },
|
|
27
|
+
{ id: 'c', name: 'Carol' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export default () => {
|
|
31
|
+
const [items, setItems] = useState(initial);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<ReactDndList
|
|
35
|
+
value={items}
|
|
36
|
+
onChange={setItems}
|
|
37
|
+
handleOnly
|
|
38
|
+
classNames={{ item: 'row', dragging: 'row--active' }}
|
|
39
|
+
slots={{
|
|
40
|
+
item: ({ item, dragHandleProps }) => (
|
|
41
|
+
<>
|
|
42
|
+
<button {...dragHandleProps}>⠿</button>
|
|
43
|
+
<span>{item.name}</span>
|
|
44
|
+
</>
|
|
45
|
+
),
|
|
46
|
+
empty: () => <p>No data</p>,
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## api
|
|
54
|
+
|
|
55
|
+
| prop | type | default | desc |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `value` | `T[]` | 必填 | 受控数据,组件不修改它 |
|
|
58
|
+
| `onChange` | `(next: T[], event: DragEndEvent) => void` | - | 拖拽完成后回传新顺序 |
|
|
59
|
+
| `keyExtractor` | `KeyExtractor<T>` | `'id'` | 与 `@jswork/react-list` 完全一致,支持 `SELF` / `INDEX` / dot path / 函数 |
|
|
60
|
+
| `slots.item` | `Slot<SortableItemContext<T>>` | 必填 | 每行内容;上下文含 `item` / `index` / `data` / `dragHandleProps` / `isDragging` |
|
|
61
|
+
| `slots.empty` | `Slot<{ data: T[] }>` | - | 空状态 |
|
|
62
|
+
| `handleOnly` | `boolean` | `false` | `true` = 仅 `dragHandleProps` 可拖;`false` = 整行可拖 |
|
|
63
|
+
| `direction` | `'vertical' \| 'horizontal'` | `'vertical'` | sortingStrategy 快捷方式 |
|
|
64
|
+
| `disabled` | `boolean` | `false` | 禁用拖拽,`onChange` 不触发 |
|
|
65
|
+
| `className` | `string` | - | 合并到根容器 |
|
|
66
|
+
| `classNames` | `{ root?, item?, dragging?, empty? }` | - | 细粒度 class,全部经 `classnames` 合并 |
|
|
67
|
+
| `dndContextProps` | `DndContextProps` | - | 逃生舱,展开到 `DndContext`;`onDragEnd` 作为前置回调转发 |
|
|
68
|
+
| `sortableContextProps` | `Omit<SortableContextProps, 'items' \| 'children'>` | - | 逃生舱;`items` 由内部接管,`strategy` 优先于 `direction` |
|
|
69
|
+
|
|
70
|
+
导出:`default`、`ReactDndList`、`SELF`、`INDEX`、`reorder` 及全部类型。
|
|
71
|
+
|
|
72
|
+
## important notes
|
|
73
|
+
|
|
74
|
+
- **`handleOnly={true}` 时必须在 slot 里渲染并 spread `dragHandleProps`**,否则整列表拖不动。这是 dnd-kit 的固有约束,组件不做运行时检测。
|
|
75
|
+
- `keyExtractor` 产出的 id 必须唯一;id 重复时 dnd-kit 行为未定义。
|
|
76
|
+
- 不使用 `DragOverlay`:拖起的就是行本身。若根容器设了 `overflow: hidden`,拖到边缘会被裁切 —— 用 `dndContextProps.modifiers` 或自行包裹 `DragOverlay`。
|
|
77
|
+
- 空列表不会创建 `DndContext` / `SortableContext`。
|
|
78
|
+
- 键盘可访问性由 `PointerSensor` + `KeyboardSensor` + `sortableKeyboardCoordinates` 提供(空格拾起、方向键移动、空格放下、Esc 取消)。
|
|
79
|
+
|
|
80
|
+
## preview
|
|
81
|
+
- https://afeiship.github.io/react-dnd-list/
|
|
82
|
+
|
|
83
|
+
## license
|
|
84
|
+
MIT
|
|
85
|
+
|
|
86
|
+
[version-image]: https://img.shields.io/npm/v/@jswork/react-dnd-list
|
|
87
|
+
[version-url]: https://npmjs.org/package/@jswork/react-dnd-list
|
|
88
|
+
[license-image]: https://img.shields.io/npm/l/@jswork/react-dnd-list
|
|
89
|
+
[license-url]: https://github.com/afeiship/react-dnd-list/blob/master/LICENSE.txt
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { UniqueIdentifier } from '@dnd-kit/core';
|
|
3
|
+
import type { ReactDndListProps } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Reorders `value` by moving the item at `ids.indexOf(activeId)` to `ids.indexOf(overId)`.
|
|
6
|
+
* `ids` must be the positional key array of `value`, with unique entries.
|
|
7
|
+
* Returns `null` when either id is unknown or the indices are equal.
|
|
8
|
+
*/
|
|
9
|
+
export declare function reorder<T>(value: T[], ids: UniqueIdentifier[], activeId: UniqueIdentifier, overId: UniqueIdentifier): T[] | null;
|
|
10
|
+
declare function ReactDndList<T>(props: ReactDndListProps<T>): React.JSX.Element;
|
|
11
|
+
declare namespace ReactDndList {
|
|
12
|
+
var displayName: string;
|
|
13
|
+
var version: string;
|
|
14
|
+
}
|
|
15
|
+
export default ReactDndList;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { DndContextProps, DragEndEvent } from '@dnd-kit/core';
|
|
3
|
+
import type { SortableContextProps, useSortable } from '@dnd-kit/sortable';
|
|
4
|
+
import type { ItemContext, KeyExtractor, Slot } from '@jswork/react-list';
|
|
5
|
+
type SortableResult = ReturnType<typeof useSortable>;
|
|
6
|
+
/**
|
|
7
|
+
* Props to spread onto a drag handle element.
|
|
8
|
+
* handleOnly=true: full attributes + listeners + ref.
|
|
9
|
+
* handleOnly=false: listeners live on the row wrapper; only ref is provided.
|
|
10
|
+
*/
|
|
11
|
+
export type DragHandleProps = Partial<SortableResult['attributes']> & Partial<NonNullable<SortableResult['listeners']>> & {
|
|
12
|
+
ref: SortableResult['setActivatorNodeRef'];
|
|
13
|
+
};
|
|
14
|
+
export type SortableItemContext<T> = ItemContext<T> & {
|
|
15
|
+
dragHandleProps: DragHandleProps;
|
|
16
|
+
isDragging: boolean;
|
|
17
|
+
};
|
|
18
|
+
export interface ReactDndListClassNames {
|
|
19
|
+
root?: string;
|
|
20
|
+
item?: string;
|
|
21
|
+
dragging?: string;
|
|
22
|
+
empty?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ReactDndListProps<T> extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'children'> {
|
|
25
|
+
value: T[];
|
|
26
|
+
onChange?: (next: T[], event: DragEndEvent) => void;
|
|
27
|
+
keyExtractor?: KeyExtractor<T>;
|
|
28
|
+
slots: {
|
|
29
|
+
item: Slot<SortableItemContext<T>>;
|
|
30
|
+
empty?: Slot<{
|
|
31
|
+
data: T[];
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
handleOnly?: boolean;
|
|
35
|
+
direction?: 'vertical' | 'horizontal';
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
className?: string;
|
|
38
|
+
classNames?: ReactDndListClassNames;
|
|
39
|
+
dndContextProps?: DndContextProps;
|
|
40
|
+
sortableContextProps?: Omit<SortableContextProps, 'items' | 'children'>;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* name: @jswork/react-dnd-list
|
|
3
|
+
* description: A lightweight, accessible draggable list component built with dnd-kit.
|
|
4
|
+
* homepage: https://js.work
|
|
5
|
+
* version: 1.0.2
|
|
6
|
+
* date: 2026-11-11T12:54:20.265Z
|
|
7
|
+
* license: MIT
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("tslib"),require("react"),require("classnames"),require("@dnd-kit/core"),require("@dnd-kit/sortable"),require("@dnd-kit/utilities"),require("@jswork/react-list")):"function"==typeof define&&define.amd?define(["exports","tslib","react","classnames","@dnd-kit/core","@dnd-kit/sortable","@dnd-kit/utilities","@jswork/react-list"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).ReactDndList={},e.tslib,e.React,e.classNames,e.DndKitCore,e.DndKitSortable,e.DndKitUtilities,e.ReactList)}(this,function(e,t,n,r,i,a,s,o){"use strict";function l(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var d=l(n),u=l(r),c="react-dnd-list";function f(e,t,n,r){var i=t.indexOf(n),s=t.indexOf(r);return i<0||s<0||i===s?null:a.arrayMove(e,i,s)}function m(e){var n=e.id,r=e.item,i=e.index,l=e.data,f=e.slot,m=e.handleOnly,g=e.disabled,b=e.classNames,v=a.useSortable({id:n,disabled:g}),_=v.attributes,y=v.listeners,p=v.setNodeRef,x=v.setActivatorNodeRef,S=v.transform,E=v.transition,N=v.isDragging,D=m?t.__assign(t.__assign(t.__assign({},_),y),{ref:x}):{ref:x},h=m?{}:t.__assign(t.__assign({},_),y);return d.default.createElement("div",t.__assign({ref:p,className:u.default("".concat(c,"__item"),null==b?void 0:b.item,N&&(null==b?void 0:b.dragging)),style:{transform:s.CSS.Transform.toString(S),transition:E}},h),o.renderSlot(f,{item:r,index:i,data:l,dragHandleProps:D,isDragging:N},n))}function g(e){var r,s=e.value,l=e.onChange,g=e.keyExtractor,b=e.slots,v=e.handleOnly,_=void 0!==v&&v,y=e.direction,p=void 0===y?"vertical":y,x=e.disabled,S=void 0!==x&&x,E=e.className,N=e.classNames,D=e.dndContextProps,h=e.sortableContextProps,C=t.__rest(e,["value","onChange","keyExtractor","slots","handleOnly","direction","disabled","className","classNames","dndContextProps","sortableContextProps"]),k=null!=g?g:"id",O=n.useMemo(function(){return s.map(function(e,t){return o.getKey(e,t,s,k)})},[s,k]),P=i.useSensors(i.useSensor(i.PointerSensor),i.useSensor(i.KeyboardSensor,{coordinateGetter:a.sortableKeyboardCoordinates})),L="horizontal"===p?a.horizontalListSortingStrategy:a.verticalListSortingStrategy,j=null!=D?D:{},q=j.onDragEnd,R=t.__rest(j,["onDragEnd"]),K=u.default(c,null==N?void 0:N.root,E,0===s.length&&(null==N?void 0:N.empty)),M=o.ReactList;return 0===s.length?d.default.createElement("div",t.__assign({className:K},C),o.renderSlot(b.empty,{data:s})):d.default.createElement("div",t.__assign({className:K},C),d.default.createElement(i.DndContext,t.__assign({sensors:P,collisionDetection:i.closestCenter},R,{onDragEnd:function(e){if(null==q||q(e),!S&&e.over){var t=f(s,O,e.active.id,e.over.id);t&&(null==l||l(t,e))}}}),d.default.createElement(a.SortableContext,t.__assign({},h,{items:O,strategy:null!==(r=null==h?void 0:h.strategy)&&void 0!==r?r:L}),d.default.createElement(M,{data:s,keyExtractor:k,slots:{item:function(e){return d.default.createElement(m,{id:O[e.index],item:e.item,index:e.index,data:e.data,slot:b.item,handleOnly:_,disabled:S,classNames:N})}}}))))}g.displayName=c,g.version="1.0.2",Object.defineProperty(e,"INDEX",{enumerable:!0,get:function(){return o.INDEX}}),Object.defineProperty(e,"SELF",{enumerable:!0,get:function(){return o.SELF}}),e.ReactDndList=g,e.default=g,e.reorder=f,Object.defineProperty(e,"__esModule",{value:!0})});
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import Component from './components';
|
|
2
|
+
export { SELF, INDEX } from '@jswork/react-list';
|
|
3
|
+
export type { ReactDndListProps, ReactDndListClassNames, SortableItemContext, DragHandleProps, } from './components/types';
|
|
4
|
+
export { reorder } from './components';
|
|
5
|
+
export { Component as ReactDndList };
|
|
6
|
+
export default Component;
|
package/dist/style.css
ADDED
package/dist/style.scss
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,92 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jswork/react-dnd-list",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"homepage": "https://js.work",
|
|
4
|
+
"version": "1.0.2",
|
|
5
|
+
"description": "A lightweight, accessible draggable list component built with dnd-kit.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"typings": "dist/main.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"prettier": "@jswork/style-guide/prettier",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "vite",
|
|
17
|
+
"clean": "rimraf dist",
|
|
18
|
+
"build": "npm run clean && rollup -c",
|
|
19
|
+
"test": "jest",
|
|
20
|
+
"test:watch": "jest --watch",
|
|
21
|
+
"release": "release-it"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@babel/core": "^7.12.10",
|
|
25
|
+
"@jswork/next": "^1.2.2",
|
|
26
|
+
"@jswork/next-replace-in-file": "^1.0.0",
|
|
27
|
+
"@jswork/next-rollup-banner": "^1.0.12",
|
|
28
|
+
"@jswork/presets-tailwind": "^1.1.10",
|
|
29
|
+
"@jswork/react-markdown-props": "^1.0.1",
|
|
30
|
+
"@jswork/style-guide": "^1.0.3",
|
|
31
|
+
"@rollup/plugin-commonjs": "^25.0.3",
|
|
32
|
+
"@rollup/plugin-image": "^2.1.1",
|
|
33
|
+
"@rollup/plugin-node-resolve": "^15.0.1",
|
|
34
|
+
"@rollup/plugin-replace": "^2.3.4",
|
|
35
|
+
"@testing-library/jest-dom": "^5.16.1",
|
|
36
|
+
"@testing-library/react": "^12.1.2",
|
|
37
|
+
"@types/jest": "^27.0.3",
|
|
38
|
+
"@types/react": "^18.0.25",
|
|
39
|
+
"@types/react-dom": "^18.0.8",
|
|
40
|
+
"@types/react-test-renderer": "^17.0.0",
|
|
41
|
+
"@types/testing-library__jest-dom": "^5.14.9",
|
|
42
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
43
|
+
"autoprefixer": "^10.4.19",
|
|
44
|
+
"babel-loader": "^8.2.2",
|
|
45
|
+
"core-js": "^3.8.2",
|
|
46
|
+
"css-loader": "^5.0.1",
|
|
47
|
+
"indent-string": "^4.0.0",
|
|
48
|
+
"jest": "^27.4.5",
|
|
49
|
+
"jest-environment-jsdom": "^27.5.1",
|
|
50
|
+
"postcss": "^8.4.38",
|
|
51
|
+
"react": "^18.2.0",
|
|
52
|
+
"react-dom": "^18.2.0",
|
|
53
|
+
"react-scripts-ts": "^3.1.0",
|
|
54
|
+
"react-test-renderer": "^17.0.1",
|
|
55
|
+
"rimraf": "^3.0.2",
|
|
56
|
+
"rollup": "^2.36.0",
|
|
57
|
+
"rollup-plugin-auto-external": "^2.0.0",
|
|
58
|
+
"rollup-plugin-banner": "^0.2.1",
|
|
59
|
+
"rollup-plugin-copy": "^3.3.0",
|
|
60
|
+
"rollup-plugin-external-globals": "^0.6.1",
|
|
61
|
+
"rollup-plugin-node-externals": "^2.2.0",
|
|
62
|
+
"rollup-plugin-node-resolve": "^5.2.0",
|
|
63
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
64
|
+
"rollup-plugin-scss": "^3.0.0",
|
|
65
|
+
"rollup-plugin-terser": "^7.0.2",
|
|
66
|
+
"rollup-plugin-typescript2": "^0.29.0",
|
|
67
|
+
"sass": "^1.47.0",
|
|
68
|
+
"sass-loader": "^10.1.0",
|
|
69
|
+
"style-loader": "^2.0.0",
|
|
70
|
+
"tailwindcss": "^3.4.3",
|
|
71
|
+
"ts-jest": "^27.1.1",
|
|
72
|
+
"tslib": "^2.9.1",
|
|
73
|
+
"typescript": "^4.9.5",
|
|
74
|
+
"vite": "^5.2.10",
|
|
75
|
+
"vite-plugin-pwa": "^0.19.8"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"@dnd-kit/core": "^6.3.1",
|
|
79
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
80
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
81
|
+
"@jswork/react-list": "^2.0.18",
|
|
82
|
+
"classnames": "^2.5.1"
|
|
83
|
+
},
|
|
84
|
+
"peerDependencies": {
|
|
85
|
+
"react": ">=18",
|
|
86
|
+
"react-dom": ">=18"
|
|
87
|
+
},
|
|
88
|
+
"publishConfig": {
|
|
89
|
+
"access": "public",
|
|
90
|
+
"registry": "https://registry.npmjs.org"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import {
|
|
4
|
+
closestCenter,
|
|
5
|
+
DndContext,
|
|
6
|
+
KeyboardSensor,
|
|
7
|
+
PointerSensor,
|
|
8
|
+
useSensor,
|
|
9
|
+
useSensors,
|
|
10
|
+
} from '@dnd-kit/core';
|
|
11
|
+
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
|
|
12
|
+
import {
|
|
13
|
+
arrayMove,
|
|
14
|
+
horizontalListSortingStrategy,
|
|
15
|
+
SortableContext,
|
|
16
|
+
sortableKeyboardCoordinates,
|
|
17
|
+
useSortable,
|
|
18
|
+
verticalListSortingStrategy,
|
|
19
|
+
} from '@dnd-kit/sortable';
|
|
20
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
21
|
+
import {
|
|
22
|
+
ReactList,
|
|
23
|
+
getKey,
|
|
24
|
+
renderSlot,
|
|
25
|
+
} from '@jswork/react-list';
|
|
26
|
+
import type {
|
|
27
|
+
KeyExtractor,
|
|
28
|
+
ReactListProps,
|
|
29
|
+
Slot,
|
|
30
|
+
} from '@jswork/react-list';
|
|
31
|
+
import type {
|
|
32
|
+
DragHandleProps,
|
|
33
|
+
ReactDndListClassNames,
|
|
34
|
+
ReactDndListProps,
|
|
35
|
+
SortableItemContext,
|
|
36
|
+
} from './types';
|
|
37
|
+
|
|
38
|
+
const CLASS_NAME = 'react-dnd-list';
|
|
39
|
+
const VERSION = '__VERSION__';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reorders `value` by moving the item at `ids.indexOf(activeId)` to `ids.indexOf(overId)`.
|
|
43
|
+
* `ids` must be the positional key array of `value`, with unique entries.
|
|
44
|
+
* Returns `null` when either id is unknown or the indices are equal.
|
|
45
|
+
*/
|
|
46
|
+
export function reorder<T>(
|
|
47
|
+
value: T[],
|
|
48
|
+
ids: UniqueIdentifier[],
|
|
49
|
+
activeId: UniqueIdentifier,
|
|
50
|
+
overId: UniqueIdentifier
|
|
51
|
+
): T[] | null {
|
|
52
|
+
const from = ids.indexOf(activeId);
|
|
53
|
+
const to = ids.indexOf(overId);
|
|
54
|
+
if (from < 0 || to < 0 || from === to) return null;
|
|
55
|
+
return arrayMove(value, from, to);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type SortableRowProps<T> = {
|
|
59
|
+
id: UniqueIdentifier;
|
|
60
|
+
item: T;
|
|
61
|
+
index: number;
|
|
62
|
+
data: T[];
|
|
63
|
+
slot: Slot<SortableItemContext<T>>;
|
|
64
|
+
handleOnly: boolean;
|
|
65
|
+
disabled: boolean;
|
|
66
|
+
classNames?: ReactDndListClassNames;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function SortableRow<T>({
|
|
70
|
+
id,
|
|
71
|
+
item,
|
|
72
|
+
index,
|
|
73
|
+
data,
|
|
74
|
+
slot,
|
|
75
|
+
handleOnly,
|
|
76
|
+
disabled,
|
|
77
|
+
classNames,
|
|
78
|
+
}: SortableRowProps<T>) {
|
|
79
|
+
const {
|
|
80
|
+
attributes,
|
|
81
|
+
listeners,
|
|
82
|
+
setNodeRef,
|
|
83
|
+
setActivatorNodeRef,
|
|
84
|
+
transform,
|
|
85
|
+
transition,
|
|
86
|
+
isDragging,
|
|
87
|
+
} = useSortable({ id, disabled });
|
|
88
|
+
|
|
89
|
+
// Cast: dnd-kit `attributes` (role: string) conflicts with Partial<SyntheticListenerMap>
|
|
90
|
+
// (Record<string, Function> index signature) under TS 4.9 — TS2322. Type-only.
|
|
91
|
+
const dragHandleProps: DragHandleProps = handleOnly
|
|
92
|
+
? ({ ...attributes, ...listeners, ref: setActivatorNodeRef } as DragHandleProps)
|
|
93
|
+
: { ref: setActivatorNodeRef };
|
|
94
|
+
|
|
95
|
+
const wrapperProps = handleOnly
|
|
96
|
+
? {}
|
|
97
|
+
: { ...attributes, ...listeners };
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
ref={setNodeRef}
|
|
102
|
+
className={cx(
|
|
103
|
+
`${CLASS_NAME}__item`,
|
|
104
|
+
classNames?.item,
|
|
105
|
+
isDragging && classNames?.dragging
|
|
106
|
+
)}
|
|
107
|
+
style={{
|
|
108
|
+
transform: CSS.Transform.toString(transform),
|
|
109
|
+
transition,
|
|
110
|
+
}}
|
|
111
|
+
{...wrapperProps}
|
|
112
|
+
>
|
|
113
|
+
{renderSlot<SortableItemContext<T>>(
|
|
114
|
+
slot,
|
|
115
|
+
{
|
|
116
|
+
item,
|
|
117
|
+
index,
|
|
118
|
+
data,
|
|
119
|
+
dragHandleProps,
|
|
120
|
+
isDragging,
|
|
121
|
+
},
|
|
122
|
+
id
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ReactDndList<T>(props: ReactDndListProps<T>) {
|
|
129
|
+
const {
|
|
130
|
+
value,
|
|
131
|
+
onChange,
|
|
132
|
+
keyExtractor,
|
|
133
|
+
slots,
|
|
134
|
+
handleOnly = false,
|
|
135
|
+
direction = 'vertical',
|
|
136
|
+
disabled = false,
|
|
137
|
+
className,
|
|
138
|
+
classNames,
|
|
139
|
+
dndContextProps,
|
|
140
|
+
sortableContextProps,
|
|
141
|
+
...rest
|
|
142
|
+
} = props;
|
|
143
|
+
|
|
144
|
+
const extractor = (keyExtractor ?? 'id') as KeyExtractor<T>;
|
|
145
|
+
|
|
146
|
+
const ids = useMemo<UniqueIdentifier[]>(
|
|
147
|
+
() => value.map((item, index) => getKey(item, index, value, extractor)),
|
|
148
|
+
[value, extractor]
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const sensors = useSensors(
|
|
152
|
+
useSensor(PointerSensor),
|
|
153
|
+
useSensor(KeyboardSensor, {
|
|
154
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const defaultStrategy =
|
|
159
|
+
direction === 'horizontal'
|
|
160
|
+
? horizontalListSortingStrategy
|
|
161
|
+
: verticalListSortingStrategy;
|
|
162
|
+
|
|
163
|
+
const { onDragEnd: userOnDragEnd, ...restDndContextProps } =
|
|
164
|
+
dndContextProps ?? {};
|
|
165
|
+
|
|
166
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
167
|
+
userOnDragEnd?.(event);
|
|
168
|
+
if (disabled || !event.over) return;
|
|
169
|
+
const next = reorder(value, ids, event.active.id, event.over.id);
|
|
170
|
+
if (next) onChange?.(next, event);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const itemSlot: Slot<SortableItemContext<T>> = (ctx) => (
|
|
174
|
+
<SortableRow
|
|
175
|
+
id={ids[ctx.index]}
|
|
176
|
+
item={ctx.item}
|
|
177
|
+
index={ctx.index}
|
|
178
|
+
data={ctx.data}
|
|
179
|
+
slot={slots.item}
|
|
180
|
+
handleOnly={handleOnly}
|
|
181
|
+
disabled={disabled}
|
|
182
|
+
classNames={classNames}
|
|
183
|
+
/>
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const rootClassName = cx(
|
|
187
|
+
CLASS_NAME,
|
|
188
|
+
classNames?.root,
|
|
189
|
+
className,
|
|
190
|
+
value.length === 0 && classNames?.empty
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Cast: react-list's ReactList returns React.ReactNode, which TS < 5.1 rejects as a
|
|
194
|
+
// JSX component type (TS2786). Props stay fully checked via ReactListProps<T>.
|
|
195
|
+
const List = ReactList as React.ComponentType<ReactListProps<T>>;
|
|
196
|
+
|
|
197
|
+
if (value.length === 0) {
|
|
198
|
+
return (
|
|
199
|
+
<div className={rootClassName} {...rest}>
|
|
200
|
+
{renderSlot(slots.empty, { data: value })}
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div className={rootClassName} {...rest}>
|
|
207
|
+
<DndContext
|
|
208
|
+
sensors={sensors}
|
|
209
|
+
collisionDetection={closestCenter}
|
|
210
|
+
{...restDndContextProps}
|
|
211
|
+
onDragEnd={handleDragEnd}
|
|
212
|
+
>
|
|
213
|
+
<SortableContext
|
|
214
|
+
{...sortableContextProps}
|
|
215
|
+
items={ids}
|
|
216
|
+
strategy={sortableContextProps?.strategy ?? defaultStrategy}
|
|
217
|
+
>
|
|
218
|
+
<List
|
|
219
|
+
data={value}
|
|
220
|
+
keyExtractor={extractor}
|
|
221
|
+
slots={{ item: itemSlot }}
|
|
222
|
+
/>
|
|
223
|
+
</SortableContext>
|
|
224
|
+
</DndContext>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
ReactDndList.displayName = CLASS_NAME;
|
|
230
|
+
ReactDndList.version = VERSION;
|
|
231
|
+
|
|
232
|
+
export default ReactDndList;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import type { DndContextProps, DragEndEvent } from '@dnd-kit/core';
|
|
3
|
+
import type {
|
|
4
|
+
SortableContextProps,
|
|
5
|
+
useSortable,
|
|
6
|
+
} from '@dnd-kit/sortable';
|
|
7
|
+
import type {
|
|
8
|
+
ItemContext,
|
|
9
|
+
KeyExtractor,
|
|
10
|
+
Slot,
|
|
11
|
+
} from '@jswork/react-list';
|
|
12
|
+
|
|
13
|
+
type SortableResult = ReturnType<typeof useSortable>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Props to spread onto a drag handle element.
|
|
17
|
+
* handleOnly=true: full attributes + listeners + ref.
|
|
18
|
+
* handleOnly=false: listeners live on the row wrapper; only ref is provided.
|
|
19
|
+
*/
|
|
20
|
+
export type DragHandleProps = Partial<SortableResult['attributes']> &
|
|
21
|
+
Partial<NonNullable<SortableResult['listeners']>> & {
|
|
22
|
+
ref: SortableResult['setActivatorNodeRef'];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type SortableItemContext<T> = ItemContext<T> & {
|
|
26
|
+
dragHandleProps: DragHandleProps;
|
|
27
|
+
isDragging: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export interface ReactDndListClassNames {
|
|
31
|
+
root?: string;
|
|
32
|
+
item?: string;
|
|
33
|
+
dragging?: string;
|
|
34
|
+
empty?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ReactDndListProps<T>
|
|
38
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange' | 'children'> {
|
|
39
|
+
value: T[];
|
|
40
|
+
onChange?: (next: T[], event: DragEndEvent) => void;
|
|
41
|
+
keyExtractor?: KeyExtractor<T>;
|
|
42
|
+
slots: {
|
|
43
|
+
item: Slot<SortableItemContext<T>>;
|
|
44
|
+
empty?: Slot<{ data: T[] }>;
|
|
45
|
+
};
|
|
46
|
+
handleOnly?: boolean;
|
|
47
|
+
direction?: 'vertical' | 'horizontal';
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
className?: string;
|
|
50
|
+
classNames?: ReactDndListClassNames;
|
|
51
|
+
dndContextProps?: DndContextProps;
|
|
52
|
+
sortableContextProps?: Omit<SortableContextProps, 'items' | 'children'>;
|
|
53
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Component from './components';
|
|
2
|
+
|
|
3
|
+
export { SELF, INDEX } from '@jswork/react-list';
|
|
4
|
+
export type {
|
|
5
|
+
ReactDndListProps,
|
|
6
|
+
ReactDndListClassNames,
|
|
7
|
+
SortableItemContext,
|
|
8
|
+
DragHandleProps,
|
|
9
|
+
} from './components/types';
|
|
10
|
+
export { reorder } from './components';
|
|
11
|
+
export { Component as ReactDndList };
|
|
12
|
+
|
|
13
|
+
export default Component;
|