@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 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
+ ![Demo gif](./doc/demo.gif)
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,2 @@
1
+ export { splitItems } from "./splitItems";
2
+ export { computeOriginalIndex, computeOriginalIndexAfterDrop } from "./computeOriginalIndex";
@@ -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} />;