Create base chips field with reordering
[arvados-workbench2.git] / src / components / chips / chips.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import * as React from 'react';
6 import { Chip, Grid } from '@material-ui/core';
7 import { DragSource, DragSourceSpec, DragSourceCollector, ConnectDragSource, DragDropContextProvider, DropTarget, DropTargetSpec, DropTargetCollector, ConnectDropTarget } from 'react-dnd';
8 import HTML5Backend from 'react-dnd-html5-backend';
9 import { compose } from 'lodash/fp';
10 interface ChipsFieldProps<Value> {
11     values: Value[];
12     getLabel?: (value: Value) => string;
13     onChange: (value: Value[]) => void;
14 }
15 export class Chips<Value> extends React.Component<ChipsFieldProps<Value>> {
16     render() {
17         const { values } = this.props;
18         return <DragDropContextProvider backend={HTML5Backend}>
19             <Grid container spacing={8}>
20                 {values.map(this.renderChip)}
21             </Grid>
22         </DragDropContextProvider>;
23     }
24
25     renderChip = (value: Value, index: number) =>
26         <Grid item key={index}>
27             <this.chip {...{ value }} />
28         </Grid>
29
30     type = 'chip';
31
32     dragSpec: DragSourceSpec<DraggableChipProps<Value>, { value: Value }> = {
33         beginDrag: ({ value }) => ({ value }),
34         endDrag: ({ value: dragValue }, monitor) => {
35             const { value: dropValue } = monitor.getDropResult();
36             const dragIndex = this.props.values.indexOf(dragValue);
37             const dropIndex = this.props.values.indexOf(dropValue);
38             const newValues = this.props.values.slice(0);
39             newValues.splice(dragIndex, 1, dropValue);
40             newValues.splice(dropIndex, 1, dragValue);
41             this.props.onChange(newValues);
42         }
43     };
44
45     dragCollector: DragSourceCollector<{}> = connect => ({
46         connectDragSource: connect.dragSource(),
47     })
48
49     dropSpec: DropTargetSpec<DraggableChipProps<Value>> = {
50         drop: ({ value }) => ({ value }),
51     };
52
53     dropCollector: DropTargetCollector<{}> = (connect, monitor) => ({
54         connectDropTarget: connect.dropTarget(),
55         isOver: monitor.isOver(),
56     })
57     chip = compose(
58         DragSource(this.type, this.dragSpec, this.dragCollector),
59         DropTarget(this.type, this.dropSpec, this.dropCollector),
60     )(
61         ({ connectDragSource, connectDropTarget, isOver, value }: DraggableChipProps<Value> & CollectedProps) =>
62             compose(
63                 connectDragSource,
64                 connectDropTarget,
65             )(
66                 <span>
67                     <Chip
68                         color={isOver ? 'primary' : 'default'}
69                         onDelete={this.deleteValue(value)}
70                         label={this.props.getLabel ? this.props.getLabel(value) : JSON.stringify(value)} />
71                 </span>
72             )
73     );
74
75     deleteValue = (value: Value) => () => {
76         const { values } = this.props;
77         const index = values.indexOf(value);
78         const newValues = values.slice(0);
79         newValues.splice(index, 1);
80         this.props.onChange(newValues);
81     }
82 }
83
84 interface CollectedProps {
85     connectDragSource: ConnectDragSource;
86     connectDropTarget: ConnectDropTarget;
87
88     isOver: boolean;
89 }
90
91 interface DraggableChipProps<Value> {
92     value: Value;
93 }