Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions examples/StateForm-list-draggable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* eslint-disable react/prop-types */

import React from 'react';
import HTML5Backend from 'react-dnd-html5-backend';
import { DndProvider } from 'react-dnd';
import Form from '../src';
import Input from './components/Input';
import LabelField from './components/LabelField';
import useDraggable from './components/useDraggable';

const { List, useForm } = Form;

type LabelFieldProps = Parameters<typeof LabelField>[0];
interface DraggableProps extends LabelFieldProps{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LabelFieldProps{ => LabelFieldProps {

Seems still missing types. Could you try to remove .git/hooks and reinstall node_modules to make father pre-commit hooks work. This will auto prettier the code when commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please reference to following code. The default config don't include the examples directory.
so father pre-commit don't work for it.

https://github.com/umijs/father/blob/86e4eb154c212072cd937d6a87fd91762159e069/packages/father/src/preCommit.ts#L113

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me add PR for this. Thanks for notice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add "examples" to the regex of pre-commit by manual. after then, I get following errors by exec "father pre-commit".

'react-dnd-html5-backend' and 'react-dnd' should only be used for example. it should be listed in devDependencies.

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for feedback. Add PR for this: umijs/fabric#6

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated @umi/fabric, please check.

id : string|number,
index : number,
move : (from:number, to :number)=>void,
}
const DisableDraggable = {
onDragStart(event) {
event.stopPropagation();
event.preventDefault();
},
draggable: true,
};
const Draggable :React.FunctionComponent<DraggableProps> = ({ id, index, move, children }) => {
const { ref, isDragging } = useDraggable('list-draggable', id, index, move);
return <div ref={ref} style={{
opacity: isDragging ? 0.5 : 1,
}}>
{children}
</div>;
};
const Demo = () => {
const [form] = useForm();

return (
<DndProvider backend={HTML5Backend}>
<div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

miss 2 space as of child node.

<h3>Draggable List of Form</h3>
<p>You can set Field as List and sortable by drag and drop</p>

<Form
form={form}
onValuesChange={(_, values) => {
console.log('values:', values);
}}
style={{ border: '1px solid red', padding: 15 }}
>
<List name="users">
{(fields, { add, remove, move }) => {
return (
<div>
<h4>List of `users`</h4>
{fields.map((field, index) => (
<Draggable move={move}
index={index} id={field.key} {...field} rules={[{ required: true }]}>
<LabelField {...field} rules={[{ required: true }]}>
{control => (
<div style={{ position: 'relative' }}>
<Input {...DisableDraggable} {...control} />
<a style={{ position: 'absolute', top: 12, right: -300 }} onClick={() => {
remove(index);
}}>
Remove
</a>
</div>
)}
</LabelField>
</Draggable>
))}

<button
type="button"
onClick={() => {
add();
}}
>
+ New User
</button>
</div>
);
}}
</List>
</Form>
</div>
</DndProvider>
);
};

export default Demo;
81 changes: 81 additions & 0 deletions examples/components/useDraggable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useRef } from 'react';
import { DragObjectWithType, useDrag, useDrop } from 'react-dnd';

type DragWithIndex = DragObjectWithType & {
index : number,
};
export default function useDraggable(type : string,
id : string|number,
index : number, move : (from : number, to : number)=>void) {
const ref = useRef(null);
const [, drop] = useDrop({
accept: type,
hover(item : DragWithIndex, monitor) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
if (dragIndex === undefined || dragIndex === null) return;
const hoverIndex = index;

// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}

// Determine rectangle on screen
const hoverBoundingRect = ref.current.getBoundingClientRect();

// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2;

// Determine mouse position
const clientOffset = monitor.getClientOffset();

// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
const hoverClientX = clientOffset.x - hoverBoundingRect.left;

// console.log(hoverBoundingRect,hoverMiddleY,clientOffset,hoverClientY,
// dragIndex,hoverIndex
// );
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%

// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY
&& hoverClientX < hoverMiddleX
) {
return;
}

// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY
&& hoverClientX > hoverMiddleX
) {
return;
}

// Time to actually perform the action
move(dragIndex, hoverIndex);

// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
item.index = hoverIndex;
},
});
const [{ isDragging }, drag] = useDrag({
item: { type, id, index },
collect: monitor => ({
isDragging: monitor.isDragging(),
}),
});
drag(drop(ref));
return {
ref, isDragging,
};
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
"father": "^2.13.2",
"np": "^5.0.3",
"react": "^v16.9.0-alpha.0",
"react-dnd": "^8.0.3",
"react-dnd-html5-backend": "^8.0.3",
"react-dom": "^v16.9.0-alpha.0",
"react-redux": "^4.4.10",
"react-router": "^3.0.0",
Expand Down
32 changes: 29 additions & 3 deletions src/List.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import warning from 'warning';
import { arrayMove } from './utils/arrayMove';
import { InternalNamePath, NamePath, StoreValue } from './interface';
import FieldContext from './FieldContext';
import Field from './Field';
Expand All @@ -13,6 +14,7 @@ interface ListField {
interface ListOperations {
add: () => void;
remove: (index: number) => void;
move: (from: number, to: number) => void;
}

interface ListProps {
Expand Down Expand Up @@ -49,7 +51,10 @@ const List: React.FunctionComponent<ListProps> = ({ name, children }) => {
<Field name={[]} shouldUpdate={shouldUpdate}>
{({ value = [], onChange }) => {
const { getFieldValue } = context;

const getNewValue = () => {
const values = getFieldValue(prefixName || []) as StoreValue[];
return values || [];
};
/**
* Always get latest value in case user update fields by `form` api.
*/
Expand All @@ -59,11 +64,11 @@ const List: React.FunctionComponent<ListProps> = ({ name, children }) => {
keyManager.keys = [...keyManager.keys, keyManager.id];
keyManager.id += 1;

const newValue = (getFieldValue(prefixName) || []) as StoreValue[];
const newValue = getNewValue();
onChange([...newValue, undefined]);
},
remove: (index: number) => {
const newValue = (getFieldValue(prefixName) || []) as StoreValue[];
const newValue = getNewValue();

// Do not handle out of range
if (index < 0 || index >= newValue.length) {
Expand All @@ -82,6 +87,27 @@ const List: React.FunctionComponent<ListProps> = ({ name, children }) => {
// Trigger store change
onChange(newValue.filter((_, id) => id !== index));
},
move(from: number, to: number) {
if (from === to) {
return;
}
const newValue = getNewValue();

// Do not handle out of range
if (from < 0 || from >= newValue.length) {
return;
}

// Do not handle out of range
if (to < 0 || to >= newValue.length) {
return;
}

keyManager.keys = arrayMove(keyManager.keys, from, to);

// Trigger store change
onChange(arrayMove(newValue, from, to));
},
};

return children(
Expand Down
35 changes: 35 additions & 0 deletions src/utils/arrayMove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export function arrayMove<T>(array: T[], moveIndex: number, toIndex: number) {
/* #move - Moves an array item from one position in an array to another.
Note: This is a pure function so a new array will be returned, instead
of altering the array argument.
Arguments:
1. array (String) : Array in which to move an item. (required)
2. moveIndex (Object) : The index of the item to move. (required)
3. toIndex (Object) : The index to move item at moveIndex to. (required)
*/
const item = array[moveIndex];
const { length } = array;
const diff = moveIndex - toIndex;

if (diff > 0) {
// move left
return [
...array.slice(0, toIndex),
item,
...array.slice(toIndex, moveIndex),
...array.slice(moveIndex + 1, length),
];
}
if (diff < 0) {
// move right
return [
...array.slice(0, moveIndex),
...array.slice(moveIndex + 1, toIndex + 1),
item,
...array.slice(toIndex + 1, length),
];
}
return array;
}
30 changes: 30 additions & 0 deletions tests/list.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,36 @@ describe('Form.List', () => {
matchKey(1, '1');
matchKey(2, '2');

// Move
act(() => {
operation.move(2, 0);
});

// noneffective move
act(() => {
operation.move(-1, 0);
});

// noneffective move
act(() => {
operation.move(0, 10);
});

// noneffective move
act(() => {
operation.move(0, 0);
});
wrapper.update();
matchKey(0, '2');
matchKey(1, '0');
matchKey(2, '1');

// Revert Move
act(() => {
operation.move(0, 2);
});
wrapper.update();

// Modify
await changeValue(getField(getList(), 1), '222');
expect(form.getFieldsValue()).toEqual({
Expand Down