Skip to content

Commit 0f48380

Browse files
hvsyzombieJ
authored andcommitted
feat: add move operation for List (#20)
* add move operation for List * Fixed format error * Fixed format error * Fixed input can not be selectable Add list-draggable example * Fixed format for eslint * add noneffective move test * remove unnecessary action of draggable list example remove console log for performance * remove lodash-move * add test for array move function * move "arrayMove" function to valueUtil.ts file. rename "arrayMove" to "move". add checking for out of range. add test cases for out of range. * add more test cases for move function. merge edge case check into one if. * upgrade father to latest format draggable example * add missing match key tests.
1 parent 664afb1 commit 0f48380

File tree

7 files changed

+321
-6
lines changed

7 files changed

+321
-6
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/* eslint-disable react/prop-types */
2+
import React from 'react';
3+
4+
/* eslint-enable react/prop-types */
5+
import HTML5Backend from 'react-dnd-html5-backend';
6+
import { DndProvider } from 'react-dnd';
7+
import Form, { List, useForm } from '../src';
8+
import Input from './components/Input';
9+
import LabelField from './components/LabelField';
10+
import useDraggable from './components/useDraggable';
11+
12+
type LabelFieldProps = Parameters<typeof LabelField>[0];
13+
interface DraggableProps extends LabelFieldProps {
14+
id: string | number;
15+
index: number;
16+
move: (from: number, to: number) => void;
17+
}
18+
const DisableDraggable = {
19+
onDragStart(event) {
20+
event.stopPropagation();
21+
event.preventDefault();
22+
},
23+
draggable: true,
24+
};
25+
const Draggable: React.FunctionComponent<DraggableProps> = ({ id, index, move, children }) => {
26+
const { ref, isDragging } = useDraggable('list-draggable', id, index, move);
27+
return (
28+
<div
29+
ref={ref}
30+
style={{
31+
opacity: isDragging ? 0.5 : 1,
32+
}}
33+
>
34+
{children}
35+
</div>
36+
);
37+
};
38+
const Demo = () => {
39+
const [form] = useForm();
40+
41+
return (
42+
<DndProvider backend={HTML5Backend}>
43+
<div>
44+
<h3>Draggable List of Form</h3>
45+
<p>You can set Field as List and sortable by drag and drop</p>
46+
47+
<Form
48+
form={form}
49+
onValuesChange={(_, values) => {
50+
console.log('values:', values);
51+
}}
52+
style={{ border: '1px solid red', padding: 15 }}
53+
>
54+
<List name="users">
55+
{(fields, { add, remove, move }) => (
56+
<div>
57+
<h4>List of `users`</h4>
58+
{fields.map((field, index) => (
59+
<Draggable
60+
move={move}
61+
index={index}
62+
id={field.key}
63+
{...field}
64+
rules={[{ required: true }]}
65+
>
66+
<LabelField {...field} rules={[{ required: true }]}>
67+
{control => (
68+
<div style={{ position: 'relative' }}>
69+
<Input {...DisableDraggable} {...control} />
70+
<a
71+
style={{ position: 'absolute', top: 12, right: -300 }}
72+
onClick={() => {
73+
remove(index);
74+
}}
75+
>
76+
Remove
77+
</a>
78+
</div>
79+
)}
80+
</LabelField>
81+
</Draggable>
82+
))}
83+
84+
<button
85+
type="button"
86+
onClick={() => {
87+
add();
88+
}}
89+
>
90+
+ New User
91+
</button>
92+
</div>
93+
)}
94+
</List>
95+
</Form>
96+
</div>
97+
</DndProvider>
98+
);
99+
};
100+
101+
export default Demo;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { useRef } from 'react';
2+
import { DragObjectWithType, useDrag, useDrop } from 'react-dnd';
3+
4+
type DragWithIndex = DragObjectWithType & {
5+
index : number,
6+
};
7+
export default function useDraggable(type : string,
8+
id : string|number,
9+
index : number, move : (from : number, to : number)=>void) {
10+
const ref = useRef(null);
11+
const [, drop] = useDrop({
12+
accept: type,
13+
hover(item : DragWithIndex, monitor) {
14+
if (!ref.current) {
15+
return;
16+
}
17+
const dragIndex = item.index;
18+
if (dragIndex === undefined || dragIndex === null) return;
19+
const hoverIndex = index;
20+
21+
// Don't replace items with themselves
22+
if (dragIndex === hoverIndex) {
23+
return;
24+
}
25+
26+
// Determine rectangle on screen
27+
const hoverBoundingRect = ref.current.getBoundingClientRect();
28+
29+
// Get vertical middle
30+
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
31+
const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2;
32+
33+
// Determine mouse position
34+
const clientOffset = monitor.getClientOffset();
35+
36+
// Get pixels to the top
37+
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
38+
const hoverClientX = clientOffset.x - hoverBoundingRect.left;
39+
40+
// console.log(hoverBoundingRect,hoverMiddleY,clientOffset,hoverClientY,
41+
// dragIndex,hoverIndex
42+
// );
43+
// Only perform the move when the mouse has crossed half of the items height
44+
// When dragging downwards, only move when the cursor is below 50%
45+
// When dragging upwards, only move when the cursor is above 50%
46+
47+
// Dragging downwards
48+
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY
49+
&& hoverClientX < hoverMiddleX
50+
) {
51+
return;
52+
}
53+
54+
// Dragging upwards
55+
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY
56+
&& hoverClientX > hoverMiddleX
57+
) {
58+
return;
59+
}
60+
61+
// Time to actually perform the action
62+
move(dragIndex, hoverIndex);
63+
64+
// Note: we're mutating the monitor item here!
65+
// Generally it's better to avoid mutations,
66+
// but it's good here for the sake of performance
67+
// to avoid expensive index searches.
68+
item.index = hoverIndex;
69+
},
70+
});
71+
const [{ isDragging }, drag] = useDrag({
72+
item: { type, id, index },
73+
collect: monitor => ({
74+
isDragging: monitor.isDragging(),
75+
}),
76+
});
77+
drag(drop(ref));
78+
return {
79+
ref, isDragging,
80+
};
81+
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@
4646
"enzyme": "^3.1.0",
4747
"enzyme-adapter-react-16": "^1.0.2",
4848
"enzyme-to-json": "^3.1.4",
49-
"father": "^2.13.2",
49+
"father": "^2.13.6",
5050
"np": "^5.0.3",
5151
"react": "^v16.9.0-alpha.0",
52+
"react-dnd": "^8.0.3",
53+
"react-dnd-html5-backend": "^8.0.3",
5254
"react-dom": "^v16.9.0-alpha.0",
5355
"react-redux": "^4.4.10",
5456
"react-router": "^3.0.0",

src/List.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import warning from 'warning';
33
import { InternalNamePath, NamePath, StoreValue } from './interface';
44
import FieldContext from './FieldContext';
55
import Field from './Field';
6-
import { getNamePath } from './utils/valueUtil';
6+
import { move, getNamePath } from './utils/valueUtil';
77

88
interface ListField {
99
name: number;
@@ -13,6 +13,7 @@ interface ListField {
1313
interface ListOperations {
1414
add: () => void;
1515
remove: (index: number) => void;
16+
move: (from: number, to: number) => void;
1617
}
1718

1819
interface ListProps {
@@ -49,7 +50,10 @@ const List: React.FunctionComponent<ListProps> = ({ name, children }) => {
4950
<Field name={[]} shouldUpdate={shouldUpdate}>
5051
{({ value = [], onChange }) => {
5152
const { getFieldValue } = context;
52-
53+
const getNewValue = () => {
54+
const values = getFieldValue(prefixName || []) as StoreValue[];
55+
return values || [];
56+
};
5357
/**
5458
* Always get latest value in case user update fields by `form` api.
5559
*/
@@ -59,11 +63,11 @@ const List: React.FunctionComponent<ListProps> = ({ name, children }) => {
5963
keyManager.keys = [...keyManager.keys, keyManager.id];
6064
keyManager.id += 1;
6165

62-
const newValue = (getFieldValue(prefixName) || []) as StoreValue[];
66+
const newValue = getNewValue();
6367
onChange([...newValue, undefined]);
6468
},
6569
remove: (index: number) => {
66-
const newValue = (getFieldValue(prefixName) || []) as StoreValue[];
70+
const newValue = getNewValue();
6771

6872
// Do not handle out of range
6973
if (index < 0 || index >= newValue.length) {
@@ -82,6 +86,22 @@ const List: React.FunctionComponent<ListProps> = ({ name, children }) => {
8286
// Trigger store change
8387
onChange(newValue.filter((_, id) => id !== index));
8488
},
89+
move(from: number, to: number) {
90+
if (from === to) {
91+
return;
92+
}
93+
const newValue = getNewValue();
94+
95+
// Do not handle out of range
96+
if (from < 0 || from >= newValue.length || to < 0 || to >= newValue.length) {
97+
return;
98+
}
99+
100+
keyManager.keys = move(keyManager.keys, from, to);
101+
102+
// Trigger store change
103+
onChange(move(newValue, from, to));
104+
},
85105
};
86106

87107
return children(

src/utils/valueUtil.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,42 @@ export function defaultGetValueFromEvent(valuePropName: string, ...args: EventAr
120120

121121
return event;
122122
}
123+
124+
/**
125+
* Moves an array item from one position in an array to another.
126+
*
127+
* Note: This is a pure function so a new array will be returned, instead
128+
* of altering the array argument.
129+
*
130+
* @param array Array in which to move an item. (required)
131+
* @param moveIndex The index of the item to move. (required)
132+
* @param toIndex The index to move item at moveIndex to. (required)
133+
*/
134+
export function move<T>(array: T[], moveIndex: number, toIndex: number) {
135+
const { length } = array;
136+
if (moveIndex < 0 || moveIndex >= length || toIndex < 0 || toIndex >= length) {
137+
return array;
138+
}
139+
const item = array[moveIndex];
140+
const diff = moveIndex - toIndex;
141+
142+
if (diff > 0) {
143+
// move left
144+
return [
145+
...array.slice(0, toIndex),
146+
item,
147+
...array.slice(toIndex, moveIndex),
148+
...array.slice(moveIndex + 1, length),
149+
];
150+
}
151+
if (diff < 0) {
152+
// move right
153+
return [
154+
...array.slice(0, moveIndex),
155+
...array.slice(moveIndex + 1, toIndex + 1),
156+
item,
157+
...array.slice(toIndex + 1, length),
158+
];
159+
}
160+
return array;
161+
}

tests/list.test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,62 @@ describe('Form.List', () => {
109109
matchKey(1, '1');
110110
matchKey(2, '2');
111111

112+
// Move
113+
act(() => {
114+
operation.move(2, 0);
115+
});
116+
wrapper.update();
117+
matchKey(0, '2');
118+
matchKey(1, '0');
119+
matchKey(2, '1');
120+
121+
// noneffective move
122+
act(() => {
123+
operation.move(-1, 0);
124+
});
125+
wrapper.update();
126+
matchKey(0, '2');
127+
matchKey(1, '0');
128+
matchKey(2, '1');
129+
130+
// noneffective move
131+
act(() => {
132+
operation.move(0, 10);
133+
});
134+
135+
wrapper.update();
136+
matchKey(0, '2');
137+
matchKey(1, '0');
138+
matchKey(2, '1');
139+
140+
// noneffective move
141+
act(() => {
142+
operation.move(-1, 10);
143+
});
144+
145+
wrapper.update();
146+
matchKey(0, '2');
147+
matchKey(1, '0');
148+
matchKey(2, '1');
149+
150+
// noneffective move
151+
act(() => {
152+
operation.move(0, 0);
153+
});
154+
wrapper.update();
155+
matchKey(0, '2');
156+
matchKey(1, '0');
157+
matchKey(2, '1');
158+
159+
// Revert Move
160+
act(() => {
161+
operation.move(0, 2);
162+
});
163+
wrapper.update();
164+
matchKey(0, '0');
165+
matchKey(1, '1');
166+
matchKey(2, '2');
167+
112168
// Modify
113169
await changeValue(getField(getList(), 1), '222');
114170
expect(form.getFieldsValue()).toEqual({

tests/utils.test.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
1-
import { isSimilar, setValues } from '../src/utils/valueUtil';
1+
import { move, isSimilar, setValues } from '../src/utils/valueUtil';
22
import NameMap from '../src/utils/NameMap';
33

44
describe('utils', () => {
5+
describe('arrayMove', () => {
6+
it('move', () => {
7+
expect(move([0, 1, 2, 3], 0, 2)).toEqual([1, 2, 0, 3]);
8+
expect(move([0, 1, 2, 3], 3, 1)).toEqual([0, 3, 1, 2]);
9+
expect(move([0, 1, 2, 3], 1, 1)).toEqual([0, 1, 2, 3]);
10+
expect(move([0, 1, 2, 3], -1, 3)).toEqual([0, 1, 2, 3]);
11+
expect(move([0, 1, 2, 3], -1, 5)).toEqual([0, 1, 2, 3]);
12+
expect(move([0, 1, 2, 3], 1, 5)).toEqual([0, 1, 2, 3]);
13+
expect(move([0, 1, 2, 3], 0, 0)).toEqual([0, 1, 2, 3]);
14+
expect(move([0, 1, 2, 3], 0, 1)).toEqual([1, 0, 2, 3]);
15+
expect(move([0, 1, 2, 3], 1, 0)).toEqual([1, 0, 2, 3]);
16+
expect(move([0, 1, 2, 3], 2, 3)).toEqual([0, 1, 3, 2]);
17+
expect(move([0, 1, 2, 3], 3, 3)).toEqual([0, 1, 2, 3]);
18+
expect(move([0, 1, 2, 3], 3, 2)).toEqual([0, 1, 3, 2]);
19+
});
20+
});
521
describe('valueUtil', () => {
622
it('isSimilar', () => {
723
expect(isSimilar(1, 1)).toBeTruthy();

0 commit comments

Comments
 (0)