@dashadmin/react-18-beautiful-dnd-grid 1.3.31
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/README.md +38 -0
- package/package.json +80 -0
- package/src/DragAndDropWrapper/DragAndDropWrapper.tsx +79 -0
- package/src/DragAndDropWrapper/ListManagerItem.spec.tsx +19 -0
- package/src/DragAndDropWrapper/ListManagerItem.tsx +24 -0
- package/src/ListManager.spec.tsx +44 -0
- package/src/ListManager.tsx +31 -0
- package/src/index.ts +1 -0
- package/src/setupTests.ts +31 -0
- package/src/util/omit/index.ts +1 -0
- package/src/withMaxItems/compute/computeOriginalIndex.spec.ts +141 -0
- package/src/withMaxItems/compute/computeOriginalIndex.ts +12 -0
- package/src/withMaxItems/compute/index.ts +2 -0
- package/src/withMaxItems/compute/splitItems.spec.ts +68 -0
- package/src/withMaxItems/compute/splitItems.ts +21 -0
- package/src/withMaxItems/withMaxItems.spec.tsx +50 -0
- package/src/withMaxItems/withMaxItems.tsx +76 -0
- package/src/withReactToItemsChange/withReactToItemsChange.spec.tsx +44 -0
- package/src/withReactToItemsChange/withReactToItemsChange.tsx +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# react-beautiful-dnd-grid
|
|
2
|
+
|
|
3
|
+
🚨 Beware: this package is not production-ready. Feel free to file a PR. 🚨
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm i -S react-beautiful-dnd-grid
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Demo
|
|
12
|
+
|
|
13
|
+
https://stackblitz.com/edit/react-beautiful-dnd-grid-demo
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
import React from "react";
|
|
21
|
+
import { ListManager } from "react-beautiful-dnd-grid";
|
|
22
|
+
|
|
23
|
+
const noop = function() {};
|
|
24
|
+
|
|
25
|
+
const list = [{ id: "0" }, { id: "1" }, { id: "2" }, { id: "3" }, { id: "4" }];
|
|
26
|
+
|
|
27
|
+
const ListElement = props => <div>id: {props.item.id}</div>;
|
|
28
|
+
|
|
29
|
+
const Component = () => (
|
|
30
|
+
<ListManager
|
|
31
|
+
items={list}
|
|
32
|
+
direction="horizontal"
|
|
33
|
+
maxItems={3}
|
|
34
|
+
render={item => <ListElement item={item} />}
|
|
35
|
+
onDragEnd={noop}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dashadmin/react-18-beautiful-dnd-grid",
|
|
3
|
+
"version": "1.3.31",
|
|
4
|
+
"description": "Grid management for react-beautiful-dnd",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"module": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"import": "./src/index.ts",
|
|
12
|
+
"require": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"files": [
|
|
17
|
+
"lib",
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"registry": "https://registry.npmjs.org"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"tsc": "tsc -p tsconfig.json",
|
|
27
|
+
"test": "jest --watch --config jest.config.json",
|
|
28
|
+
"format": "prettier --write 'src/**/*.{ts,tsx}'",
|
|
29
|
+
"build": "echo 'Skipping build for react-18-beautiful-dnd-grid'",
|
|
30
|
+
"_build": "rm -rf lib && mkdir lib && tsc -p tsconfig.build.json --noEmitOnError false",
|
|
31
|
+
"test:pack": "npm run build && npm pack",
|
|
32
|
+
"test:demo": "npm run build && rm -rf demo/src/lib && cp -r lib demo/src/lib",
|
|
33
|
+
"prepublishOnly": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/mathieueveillard/react-beautiful-dnd-grid.git"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"react",
|
|
41
|
+
"dnd",
|
|
42
|
+
"grid",
|
|
43
|
+
"component"
|
|
44
|
+
],
|
|
45
|
+
"author": "Mathieu Eveillard",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/mathieueveillard/react-beautiful-dnd-grid/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/mathieueveillard/react-beautiful-dnd-grid#readme",
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/enzyme": "^3.10.3",
|
|
53
|
+
"@types/enzyme-adapter-react-16": "^1.0.5",
|
|
54
|
+
"@types/jest": "^24.0.18",
|
|
55
|
+
"@types/node": "^12.7.7",
|
|
56
|
+
"@types/react": "^16.9.3",
|
|
57
|
+
"@types/uuid": "^3.4.5",
|
|
58
|
+
"@dashadmin/dash-tsconfig": "workspace:*",
|
|
59
|
+
"enzyme": "^3.10.0",
|
|
60
|
+
"enzyme-adapter-react-16": "^1.14.0",
|
|
61
|
+
"jest": "^24.9.0",
|
|
62
|
+
"jsdom": "^15.1.1",
|
|
63
|
+
"prettier": "^1.18.2",
|
|
64
|
+
"react-dom": "^16.9.0",
|
|
65
|
+
"ts-jest": "^24.1.0",
|
|
66
|
+
"typescript": "^3.6.3"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@hello-pangea/dnd": "^18.0.1",
|
|
70
|
+
"@react-dnd/asap": "^5.0.2",
|
|
71
|
+
"nanoid": "^5.0.8",
|
|
72
|
+
"react": "latest"
|
|
73
|
+
},
|
|
74
|
+
"eslintConfig": {
|
|
75
|
+
"extends": "react-app"
|
|
76
|
+
},
|
|
77
|
+
"prettier": {
|
|
78
|
+
"printWidth": 120
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React, { ReactElement } from "react";
|
|
2
|
+
import { DragDropContext, Droppable, DroppableProvided, DroppableStateSnapshot, DropResult } from "@hello-pangea/dnd";
|
|
3
|
+
import { ListManagerItem } from "./ListManagerItem";
|
|
4
|
+
import { generateId, hashAny } from "../ListManager";
|
|
5
|
+
|
|
6
|
+
interface Location {
|
|
7
|
+
id: string;
|
|
8
|
+
index: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DragAndDropResult {
|
|
12
|
+
source: Location;
|
|
13
|
+
destination: Location;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Chunk {
|
|
17
|
+
id: string;
|
|
18
|
+
items: any[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Props {
|
|
22
|
+
chunks: Chunk[];
|
|
23
|
+
direction: "horizontal" | "vertical";
|
|
24
|
+
render(item: any): ReactElement<{}>;
|
|
25
|
+
onDragEnd(result: DragAndDropResult): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const horizontalStyle: React.CSSProperties = {
|
|
29
|
+
display: "flex",
|
|
30
|
+
flexDirection: "row",
|
|
31
|
+
alignItems: "flex-start"
|
|
32
|
+
};
|
|
33
|
+
{/* @ts-ignore */ }
|
|
34
|
+
export const DragAndDropWrapper: React.StatelessComponent<Props> = ({
|
|
35
|
+
onDragEnd,
|
|
36
|
+
chunks,
|
|
37
|
+
direction,
|
|
38
|
+
render
|
|
39
|
+
}: Props) => {
|
|
40
|
+
return (
|
|
41
|
+
<DragDropContext onDragEnd={mapAndInvoke(onDragEnd)}>
|
|
42
|
+
{chunks.map(({ id: droppableId, items }: Chunk) => (
|
|
43
|
+
<Droppable key={droppableId} droppableId={droppableId} direction={direction}>
|
|
44
|
+
{(provided: DroppableProvided, _: DroppableStateSnapshot) => (
|
|
45
|
+
<div
|
|
46
|
+
ref={provided.innerRef}
|
|
47
|
+
style={direction === "horizontal" ? horizontalStyle : undefined}
|
|
48
|
+
{...provided.droppableProps}
|
|
49
|
+
>
|
|
50
|
+
{items.map((item: any, index: number) => (
|
|
51
|
+
|
|
52
|
+
<ListManagerItem key={hashAny(item)} item={item} index={index} render={render} />
|
|
53
|
+
))}
|
|
54
|
+
{provided.placeholder}
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</Droppable>
|
|
58
|
+
))}
|
|
59
|
+
</DragDropContext>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function mapAndInvoke(onDragEnd: (result: DragAndDropResult) => void) {
|
|
64
|
+
return function({ source, destination }: DropResult): void {
|
|
65
|
+
if (destination !== undefined && destination !== null) {
|
|
66
|
+
const result: DragAndDropResult = {
|
|
67
|
+
source: {
|
|
68
|
+
id: source.droppableId,
|
|
69
|
+
index: source.index
|
|
70
|
+
},
|
|
71
|
+
destination: {
|
|
72
|
+
id: destination.droppableId,
|
|
73
|
+
index: destination.index
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
onDragEnd(result);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { shallow, ShallowWrapper } from "enzyme";
|
|
3
|
+
import { ListManagerItem } from "./ListManagerItem";
|
|
4
|
+
import { Draggable } from "@hello-pangea/dnd";
|
|
5
|
+
|
|
6
|
+
describe("<ListManagerItem />", () => {
|
|
7
|
+
const item = "item";
|
|
8
|
+
const index: number = 0;
|
|
9
|
+
const render = () => <div />;
|
|
10
|
+
|
|
11
|
+
it("should render without crashing", () => {
|
|
12
|
+
shallow(<ListManagerItem item={item} index={index} render={render} />);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should render a <Draggable /> element", () => {
|
|
16
|
+
const wrapper: ShallowWrapper = shallow(<ListManagerItem item={item} index={index} render={render} />);
|
|
17
|
+
expect(wrapper.findWhere(n => n.type() === Draggable).length).toEqual(1);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React, { ReactElement } from "react";
|
|
2
|
+
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
|
3
|
+
import { generateId, hashAny } from "../ListManager";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export interface ListManagerItemProps {
|
|
7
|
+
item: any;
|
|
8
|
+
index: number;
|
|
9
|
+
render(item: any): ReactElement<{}>;
|
|
10
|
+
}
|
|
11
|
+
{/* @ts-ignore */}
|
|
12
|
+
export const ListManagerItem: React.StatelessComponent<ListManagerItemProps> = ({
|
|
13
|
+
item,
|
|
14
|
+
index,
|
|
15
|
+
render
|
|
16
|
+
}: ListManagerItemProps) => (
|
|
17
|
+
<Draggable draggableId={hashAny(item)} index={index}>
|
|
18
|
+
{(provided: DraggableProvided, _: DraggableStateSnapshot) => (
|
|
19
|
+
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
|
20
|
+
{render(item)}
|
|
21
|
+
</div>
|
|
22
|
+
)}
|
|
23
|
+
</Draggable>
|
|
24
|
+
);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { shallow, mount, ReactWrapper } from "enzyme";
|
|
3
|
+
import { ListManager } from "./ListManager";
|
|
4
|
+
import { ListManagerItem } from "./DragAndDropWrapper/ListManagerItem";
|
|
5
|
+
|
|
6
|
+
describe("<ListManager />", () => {
|
|
7
|
+
const item0 = {
|
|
8
|
+
id: "id0"
|
|
9
|
+
};
|
|
10
|
+
const item1 = {
|
|
11
|
+
id: "id1"
|
|
12
|
+
};
|
|
13
|
+
const item2 = {
|
|
14
|
+
id: "id2"
|
|
15
|
+
};
|
|
16
|
+
const item3 = {
|
|
17
|
+
id: "id3"
|
|
18
|
+
};
|
|
19
|
+
const items = [item0, item1, item2];
|
|
20
|
+
|
|
21
|
+
const render = () => <div className="render-props-class" />;
|
|
22
|
+
|
|
23
|
+
const onDragEnd = jest.fn();
|
|
24
|
+
|
|
25
|
+
it("should render without crashing", () => {
|
|
26
|
+
shallow(<ListManager items={items} direction="vertical" maxItems={3} render={render} onDragEnd={onDragEnd} />);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should render a list of <ListManagerItem /> and react to props", () => {
|
|
30
|
+
const wrapper: ReactWrapper = mount(
|
|
31
|
+
<ListManager items={items} direction="vertical" maxItems={3} render={render} onDragEnd={onDragEnd} />
|
|
32
|
+
);
|
|
33
|
+
expect(wrapper.findWhere(n => n.type() === ListManagerItem).length).toEqual(items.length);
|
|
34
|
+
wrapper.setProps({ items: [...items, item3] });
|
|
35
|
+
expect(wrapper.findWhere(n => n.type() === ListManagerItem).length).toEqual(items.length + 1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should render the render props", () => {
|
|
39
|
+
const wrapper: ReactWrapper = mount(
|
|
40
|
+
<ListManager items={items} direction="vertical" maxItems={3} render={render} onDragEnd={onDragEnd} />
|
|
41
|
+
);
|
|
42
|
+
expect(wrapper.exists(".render-props-class")).toEqual(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DragAndDropWrapper } from "./DragAndDropWrapper/DragAndDropWrapper";
|
|
2
|
+
import { withMaxItems } from "./withMaxItems/withMaxItems";
|
|
3
|
+
import { withReactToItemsChange } from "./withReactToItemsChange/withReactToItemsChange";
|
|
4
|
+
//import { nanoid } from 'nanoid/non-secure'
|
|
5
|
+
|
|
6
|
+
export function hashAny(input: any): string {
|
|
7
|
+
const str = typeof input === 'string'
|
|
8
|
+
? input
|
|
9
|
+
: JSON.stringify(input);
|
|
10
|
+
|
|
11
|
+
let hash = 0;
|
|
12
|
+
for (let i = 0; i < str.length; i++) {
|
|
13
|
+
const char = str.charCodeAt(i);
|
|
14
|
+
hash = ((hash << 5) - hash) + char;
|
|
15
|
+
hash = hash & hash;
|
|
16
|
+
}
|
|
17
|
+
return Math.abs(hash).toString(36);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const generateId = (() => {
|
|
21
|
+
let counter = 0;
|
|
22
|
+
return (prefix = 'grid-item-') => {
|
|
23
|
+
counter += 1;
|
|
24
|
+
return `${hashAny(prefix.toString() || Date.now())}${counter}`;
|
|
25
|
+
};
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
const ComponentWithMaxItems = withMaxItems(DragAndDropWrapper, generateId);
|
|
29
|
+
const ComponentWithReactToItemsChange = withReactToItemsChange(ComponentWithMaxItems);
|
|
30
|
+
|
|
31
|
+
export const ListManager = ComponentWithReactToItemsChange;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ListManager } from "./ListManager";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { configure } from "enzyme";
|
|
2
|
+
import Adapter from "enzyme-adapter-react-16";
|
|
3
|
+
|
|
4
|
+
configure({ adapter: new Adapter() });
|
|
5
|
+
|
|
6
|
+
const { JSDOM } = require("jsdom");
|
|
7
|
+
|
|
8
|
+
const jsdom = new JSDOM("<!doctype html><html><body></body></html>");
|
|
9
|
+
const { window } = jsdom;
|
|
10
|
+
|
|
11
|
+
function copyProps(src, target) {
|
|
12
|
+
Object.defineProperties(target, {
|
|
13
|
+
...Object.getOwnPropertyDescriptors(src),
|
|
14
|
+
...Object.getOwnPropertyDescriptors(target)
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
global["window"] = window;
|
|
19
|
+
global["document"] = window.document;
|
|
20
|
+
global["navigator"] = {
|
|
21
|
+
userAgent: "node.js"
|
|
22
|
+
};
|
|
23
|
+
global["requestAnimationFrame"] = function(callback) {
|
|
24
|
+
return setTimeout(callback, 0);
|
|
25
|
+
};
|
|
26
|
+
global["cancelAnimationFrame"] = function(id) {
|
|
27
|
+
clearTimeout(id);
|
|
28
|
+
};
|
|
29
|
+
copyProps(window, global);
|
|
30
|
+
|
|
31
|
+
export default undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Omit<P, O> = Pick<P, Exclude<keyof P, keyof O>>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { computeOriginalIndex, computeOriginalIndexAfterDrop } from "./computeOriginalIndex";
|
|
2
|
+
|
|
3
|
+
describe("computeOriginalIndex", () => {
|
|
4
|
+
it("should compute the index of an item in the original array of items", () => {
|
|
5
|
+
// GIVEN
|
|
6
|
+
const maxItems: number = 4;
|
|
7
|
+
const chunkIndex: number = 0;
|
|
8
|
+
const indexInChunk: number = 0;
|
|
9
|
+
|
|
10
|
+
// WHEN
|
|
11
|
+
const actual: number = computeOriginalIndex(maxItems, chunkIndex, indexInChunk);
|
|
12
|
+
|
|
13
|
+
// THEN
|
|
14
|
+
const expected: number = 0;
|
|
15
|
+
expect(actual).toEqual(expected);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should compute the index of an item in the original array of items", () => {
|
|
19
|
+
// GIVEN
|
|
20
|
+
const maxItems: number = 4;
|
|
21
|
+
const chunkIndex: number = 0;
|
|
22
|
+
const indexInChunk: number = 3;
|
|
23
|
+
|
|
24
|
+
// WHEN
|
|
25
|
+
const actual: number = computeOriginalIndex(maxItems, chunkIndex, indexInChunk);
|
|
26
|
+
|
|
27
|
+
// THEN
|
|
28
|
+
const expected: number = 3;
|
|
29
|
+
expect(actual).toEqual(expected);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should compute the index of an item in the original array of items", () => {
|
|
33
|
+
// GIVEN
|
|
34
|
+
const maxItems: number = 4;
|
|
35
|
+
const chunkIndex: number = 1;
|
|
36
|
+
const indexInChunk: number = 0;
|
|
37
|
+
|
|
38
|
+
// WHEN
|
|
39
|
+
const actual: number = computeOriginalIndex(maxItems, chunkIndex, indexInChunk);
|
|
40
|
+
|
|
41
|
+
// THEN
|
|
42
|
+
const expected: number = 4;
|
|
43
|
+
expect(actual).toEqual(expected);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should compute the index of an item in the original array of items", () => {
|
|
47
|
+
// GIVEN
|
|
48
|
+
const maxItems: number = 4;
|
|
49
|
+
const chunkIndex: number = 1;
|
|
50
|
+
const indexInChunk: number = 1;
|
|
51
|
+
|
|
52
|
+
// WHEN
|
|
53
|
+
const actual: number = computeOriginalIndex(maxItems, chunkIndex, indexInChunk);
|
|
54
|
+
|
|
55
|
+
// THEN
|
|
56
|
+
const expected: number = 5;
|
|
57
|
+
expect(actual).toEqual(expected);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("computeOriginalIndexAfterDrop", () => {
|
|
62
|
+
it("should compute the index of an item in the original array of items after drop", () => {
|
|
63
|
+
// GIVEN
|
|
64
|
+
const maxItems: number = 4;
|
|
65
|
+
const sourceChunkIndex: number = 0;
|
|
66
|
+
const destinationChunkIndex: number = 0;
|
|
67
|
+
const indexInChunk: number = 1;
|
|
68
|
+
|
|
69
|
+
// WHEN
|
|
70
|
+
const actual: number = computeOriginalIndexAfterDrop(
|
|
71
|
+
maxItems,
|
|
72
|
+
sourceChunkIndex,
|
|
73
|
+
destinationChunkIndex,
|
|
74
|
+
indexInChunk
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// THEN
|
|
78
|
+
const expected: number = 1;
|
|
79
|
+
expect(actual).toEqual(expected);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should compute the index of an item in the original array of items after drop", () => {
|
|
83
|
+
// GIVEN
|
|
84
|
+
const maxItems: number = 4;
|
|
85
|
+
const sourceChunkIndex: number = 1;
|
|
86
|
+
const destinationChunkIndex: number = 0;
|
|
87
|
+
const indexInChunk: number = 1;
|
|
88
|
+
|
|
89
|
+
// WHEN
|
|
90
|
+
const actual: number = computeOriginalIndexAfterDrop(
|
|
91
|
+
maxItems,
|
|
92
|
+
sourceChunkIndex,
|
|
93
|
+
destinationChunkIndex,
|
|
94
|
+
indexInChunk
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// THEN
|
|
98
|
+
const expected: number = 1;
|
|
99
|
+
expect(actual).toEqual(expected);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should compute the index of an item in the original array of items after drop", () => {
|
|
103
|
+
// GIVEN
|
|
104
|
+
const maxItems: number = 4;
|
|
105
|
+
const sourceChunkIndex: number = 0;
|
|
106
|
+
const destinationChunkIndex: number = 1;
|
|
107
|
+
const indexInChunk: number = 0;
|
|
108
|
+
|
|
109
|
+
// WHEN
|
|
110
|
+
const actual: number = computeOriginalIndexAfterDrop(
|
|
111
|
+
maxItems,
|
|
112
|
+
sourceChunkIndex,
|
|
113
|
+
destinationChunkIndex,
|
|
114
|
+
indexInChunk
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// THEN
|
|
118
|
+
const expected: number = 3;
|
|
119
|
+
expect(actual).toEqual(expected);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should compute the index of an item in the original array of items after drop", () => {
|
|
123
|
+
// GIVEN
|
|
124
|
+
const maxItems: number = 4;
|
|
125
|
+
const sourceChunkIndex: number = 0;
|
|
126
|
+
const destinationChunkIndex: number = 1;
|
|
127
|
+
const indexInChunk: number = 1;
|
|
128
|
+
|
|
129
|
+
// WHEN
|
|
130
|
+
const actual: number = computeOriginalIndexAfterDrop(
|
|
131
|
+
maxItems,
|
|
132
|
+
sourceChunkIndex,
|
|
133
|
+
destinationChunkIndex,
|
|
134
|
+
indexInChunk
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// THEN
|
|
138
|
+
const expected: number = 4;
|
|
139
|
+
expect(actual).toEqual(expected);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function computeOriginalIndex(maxItems: number, chunkIndex: number, indexInChunk: number): number {
|
|
2
|
+
return chunkIndex * maxItems + indexInChunk;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function computeOriginalIndexAfterDrop(
|
|
6
|
+
maxItems: number,
|
|
7
|
+
sourceChunkIndex: number,
|
|
8
|
+
destinationChunkIndex: number,
|
|
9
|
+
indexInChunk: number
|
|
10
|
+
): number {
|
|
11
|
+
return destinationChunkIndex * maxItems + indexInChunk + (sourceChunkIndex < destinationChunkIndex ? -1 : 0);
|
|
12
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { splitItems } from "./splitItems";
|
|
2
|
+
import { Chunk } from "../withMaxItems";
|
|
3
|
+
|
|
4
|
+
describe("splitItems", () => {
|
|
5
|
+
it("should split items according to maxItems (less items than maxItems)", () => {
|
|
6
|
+
// GIVEN
|
|
7
|
+
const maxItems: number = 4;
|
|
8
|
+
const items = [{ id: "0" }, { id: "1" }, { id: "2" }];
|
|
9
|
+
const createId: () => string = jest.fn().mockReturnValue("id");
|
|
10
|
+
|
|
11
|
+
// WHEN
|
|
12
|
+
const actual: Chunk[] = splitItems(maxItems, items, createId);
|
|
13
|
+
|
|
14
|
+
// THEN
|
|
15
|
+
const expected: Chunk[] = [
|
|
16
|
+
{
|
|
17
|
+
id: "id",
|
|
18
|
+
items: [{ id: "0" }, { id: "1" }, { id: "2" }]
|
|
19
|
+
}
|
|
20
|
+
];
|
|
21
|
+
expect(actual).toEqual(expected);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should split items according to maxItems (as many items as maxItems)", () => {
|
|
25
|
+
// GIVEN
|
|
26
|
+
const maxItems: number = 4;
|
|
27
|
+
const items = [{ id: "0" }, { id: "1" }, { id: "2" }, { id: "3" }];
|
|
28
|
+
const createId: () => string = jest.fn().mockReturnValue("id");
|
|
29
|
+
|
|
30
|
+
// WHEN
|
|
31
|
+
const actual: Chunk[] = splitItems(maxItems, items, createId);
|
|
32
|
+
|
|
33
|
+
// THEN
|
|
34
|
+
const expected: Chunk[] = [
|
|
35
|
+
{
|
|
36
|
+
id: "id",
|
|
37
|
+
items: [{ id: "0" }, { id: "1" }, { id: "2" }, { id: "3" }]
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
expect(actual).toEqual(expected);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should split items according to maxItems (more items than maxItems)", () => {
|
|
44
|
+
// GIVEN
|
|
45
|
+
const maxItems: number = 4;
|
|
46
|
+
const items = [{ id: "0" }, { id: "1" }, { id: "2" }, { id: "3" }, { id: "4" }];
|
|
47
|
+
const createId: () => string = jest
|
|
48
|
+
.fn()
|
|
49
|
+
.mockReturnValueOnce("id0")
|
|
50
|
+
.mockReturnValueOnce("id1");
|
|
51
|
+
|
|
52
|
+
// WHEN
|
|
53
|
+
const actual: Chunk[] = splitItems(maxItems, items, createId);
|
|
54
|
+
|
|
55
|
+
// THEN
|
|
56
|
+
const expected: Chunk[] = [
|
|
57
|
+
{
|
|
58
|
+
id: "id0",
|
|
59
|
+
items: [{ id: "0" }, { id: "1" }, { id: "2" }, { id: "3" }]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "id1",
|
|
63
|
+
items: [{ id: "4" }]
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
expect(actual).toEqual(expected);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Chunk } from "../withMaxItems";
|
|
2
|
+
|
|
3
|
+
export function splitItems(maxItems: number, items: any[], createId: () => string): Chunk[] {
|
|
4
|
+
const slicedItems: any[][] = sliceIntoItems(maxItems, items);
|
|
5
|
+
return slicedItems.map(mapToChunk(createId));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function sliceIntoItems(maxItems: number, items: any[]): any[][] {
|
|
9
|
+
const numberOfSlices: number = Math.ceil(items.length / maxItems);
|
|
10
|
+
const sliceIndexes: number[] = Array.apply(null, Array(numberOfSlices)).map((_, index: number) => index);
|
|
11
|
+
return sliceIndexes.map((index: number) => items.slice(index * maxItems, index * maxItems + maxItems));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mapToChunk(createId: () => string) {
|
|
15
|
+
return function(items: any[]): Chunk {
|
|
16
|
+
return {
|
|
17
|
+
id: createId(),
|
|
18
|
+
items
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Props, withMaxItems, WithMaxItemsProps, DragAndDropResult } from "./withMaxItems";
|
|
3
|
+
import { mount, ReactWrapper } from "enzyme";
|
|
4
|
+
|
|
5
|
+
describe("withMaxItems", () => {
|
|
6
|
+
it("Should split items in chunks according to the maxItems props", () => {
|
|
7
|
+
// GIVEN
|
|
8
|
+
const items = [
|
|
9
|
+
{
|
|
10
|
+
id: "id0"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "id1"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "id2"
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
const dragAndDropResult: DragAndDropResult = {
|
|
20
|
+
source: {
|
|
21
|
+
id: "id1",
|
|
22
|
+
index: 0
|
|
23
|
+
},
|
|
24
|
+
destination: {
|
|
25
|
+
id: "id0",
|
|
26
|
+
index: 0
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const Component: React.StatelessComponent<Props> = ({ onDragEnd }: Props) => (
|
|
30
|
+
<div onClick={() => onDragEnd(dragAndDropResult)} />
|
|
31
|
+
);
|
|
32
|
+
const createId = jest
|
|
33
|
+
.fn()
|
|
34
|
+
.mockReturnValueOnce("id0")
|
|
35
|
+
.mockReturnValueOnce("id1");
|
|
36
|
+
const props: WithMaxItemsProps = {
|
|
37
|
+
items,
|
|
38
|
+
maxItems: 2,
|
|
39
|
+
onDragEnd: jest.fn()
|
|
40
|
+
};
|
|
41
|
+
const ComponentWithMaxItems = withMaxItems(Component, createId);
|
|
42
|
+
|
|
43
|
+
// WHEN
|
|
44
|
+
const wrapper: ReactWrapper = mount(<ComponentWithMaxItems {...props} />);
|
|
45
|
+
wrapper.simulate("click");
|
|
46
|
+
|
|
47
|
+
// THEN
|
|
48
|
+
expect(props.onDragEnd).toHaveBeenCalledWith(2, 0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Omit } from "../util/omit";
|
|
3
|
+
import { splitItems, computeOriginalIndex, computeOriginalIndexAfterDrop } from "./compute";
|
|
4
|
+
|
|
5
|
+
interface Location {
|
|
6
|
+
id: string;
|
|
7
|
+
index: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DragAndDropResult {
|
|
11
|
+
source: Location;
|
|
12
|
+
destination: Location;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Chunk {
|
|
16
|
+
id: string;
|
|
17
|
+
items: any[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Props {
|
|
21
|
+
chunks: Chunk[];
|
|
22
|
+
onDragEnd(result: DragAndDropResult): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WithMaxItemsProps {
|
|
26
|
+
items: any[];
|
|
27
|
+
maxItems?: number;
|
|
28
|
+
onDragEnd(sourceIndex: number, destinationIndex: number): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface WithMaxItemsState {
|
|
32
|
+
maxItems: number;
|
|
33
|
+
items: any[];
|
|
34
|
+
chunks: Chunk[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const withMaxItems = <T, P extends Props>(
|
|
38
|
+
Component: React.FC<P>,
|
|
39
|
+
createId: () => string
|
|
40
|
+
): React.FC<Omit<P, Props> & WithMaxItemsProps> => {
|
|
41
|
+
const ComponentWithMaxItems: React.FC<Omit<P, Props> & WithMaxItemsProps> = (props) => {
|
|
42
|
+
const [state, setState] = React.useState<WithMaxItemsState>(() => {
|
|
43
|
+
const maxItems: number = props.maxItems && props.maxItems > 0 ? props.maxItems : props.items.length;
|
|
44
|
+
return {
|
|
45
|
+
maxItems,
|
|
46
|
+
items: props.items,
|
|
47
|
+
chunks: splitItems(maxItems, props.items, createId)
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const findChunkIndex = (id: string): number => {
|
|
52
|
+
return state.chunks.findIndex((chunk: Chunk) => chunk.id === id);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onDragEnd = ({ source, destination }: DragAndDropResult): void => {
|
|
56
|
+
if (destination) {
|
|
57
|
+
const { index: indexInSourceChunk, id: sourceChunkId } = source;
|
|
58
|
+
const { index: indexInDestinationChunk, id: destinationChunkId } = destination;
|
|
59
|
+
const sourceChunkIndex: number = findChunkIndex(sourceChunkId);
|
|
60
|
+
const destinationChunkIndex: number = findChunkIndex(destinationChunkId);
|
|
61
|
+
const sourceIndex: number = computeOriginalIndex(state.maxItems, sourceChunkIndex, indexInSourceChunk);
|
|
62
|
+
const destinationIndex: number = computeOriginalIndexAfterDrop(
|
|
63
|
+
state.maxItems,
|
|
64
|
+
sourceChunkIndex,
|
|
65
|
+
destinationChunkIndex,
|
|
66
|
+
indexInDestinationChunk
|
|
67
|
+
);
|
|
68
|
+
props.onDragEnd(sourceIndex, destinationIndex);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const { items, maxItems, onDragEnd: _, ...rest } = props;
|
|
73
|
+
return <Component {...{ chunks: state.chunks, onDragEnd }} {...((rest as unknown) as P)} />;
|
|
74
|
+
};
|
|
75
|
+
return ComponentWithMaxItems;
|
|
76
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Props, withReactToItemsChange } from "./withReactToItemsChange";
|
|
3
|
+
import { mount, ReactWrapper } from "enzyme";
|
|
4
|
+
|
|
5
|
+
describe("withReactToItemsChange", () => {
|
|
6
|
+
it("Should react to any change in items propided as props", () => {
|
|
7
|
+
// GIVEN
|
|
8
|
+
const items = [
|
|
9
|
+
{
|
|
10
|
+
id: "id0",
|
|
11
|
+
order: 0
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "id1",
|
|
15
|
+
order: 1
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "id2",
|
|
19
|
+
order: 2
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
const callMeWhenComponentDidMount = jest.fn();
|
|
23
|
+
class Component extends React.Component<Props, {}> {
|
|
24
|
+
componentDidMount() {
|
|
25
|
+
callMeWhenComponentDidMount();
|
|
26
|
+
}
|
|
27
|
+
render() {
|
|
28
|
+
return <div />;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const props: Props = {
|
|
32
|
+
items
|
|
33
|
+
};
|
|
34
|
+
const ComponentWithReactToItemsChange = withReactToItemsChange(Component);
|
|
35
|
+
|
|
36
|
+
// WHEN
|
|
37
|
+
const wrapper: ReactWrapper = mount(<ComponentWithReactToItemsChange {...props} />);
|
|
38
|
+
items[1].order = -1;
|
|
39
|
+
wrapper.setProps({ items });
|
|
40
|
+
|
|
41
|
+
// THEN
|
|
42
|
+
expect(callMeWhenComponentDidMount).toHaveBeenCalledTimes(2);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { generateId, hashAny } from "../ListManager";
|
|
3
|
+
|
|
4
|
+
export interface Props {
|
|
5
|
+
items: any[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const withReactToItemsChange = <P extends Props>(Component: React.FC<P>): React.FC<P> => (
|
|
9
|
+
props: P
|
|
10
|
+
) => <Component key={hashAny(props.items)} {...props} />;
|