Create chips input
[arvados.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, DropTarget, DropTargetSpec, DropTargetCollector, ConnectDropTarget } from 'react-dnd';
8 import { compose } from 'lodash/fp';
9 interface ChipsProps<Value> {
10     values: Value[];
11     getLabel?: (value: Value) => string;
12     filler?: React.ReactNode;
13     onChange: (value: Value[]) => void;
14 }
15 export class Chips<Value> extends React.Component<ChipsProps<Value>> {
16     render() {
17         const { values, filler } = this.props;
18         return <Grid container spacing={8}>
19             {values.map(this.renderChip)}
20             {filler && <Grid item xs>{filler}</Grid>}
21         </Grid>;
22     }
23
24     renderChip = (value: Value, index: number) =>
25         <Grid item key={index}>
26             <this.chip {...{ value }} />
27         </Grid>
28
29     type = 'chip';
30
31     dragSpec: DragSourceSpec<DraggableChipProps<Value>, { value: Value }> = {
32         beginDrag: ({ value }) => ({ value }),
33         endDrag: ({ value: dragValue }, monitor) => {
34             const result = monitor.getDropResult();
35             if (result) {
36                 const { value: dropValue } = monitor.getDropResult();
37                 const dragIndex = this.props.values.indexOf(dragValue);
38                 const dropIndex = this.props.values.indexOf(dropValue);
39                 const newValues = this.props.values.slice(0);
40                 if (dragIndex < dropIndex) {
41                     newValues.splice(dragIndex, 1);
42                     newValues.splice(dropIndex - 1 || 0, 0, dragValue);
43                 } else if (dragIndex > dropIndex) {
44                     newValues.splice(dragIndex, 1);
45                     newValues.splice(dropIndex, 0, dragValue);
46                 }
47                 this.props.onChange(newValues);
48             }
49         }
50     };
51
52     dragCollector: DragSourceCollector<{}> = connect => ({
53         connectDragSource: connect.dragSource(),
54     })
55
56     dropSpec: DropTargetSpec<DraggableChipProps<Value>> = {
57         drop: ({ value }) => ({ value }),
58     };
59
60     dropCollector: DropTargetCollector<{}> = (connect, monitor) => ({
61         connectDropTarget: connect.dropTarget(),
62         isOver: monitor.isOver(),
63     })
64     chip = compose(
65         DragSource(this.type, this.dragSpec, this.dragCollector),
66         DropTarget(this.type, this.dropSpec, this.dropCollector),
67     )(
68         ({ connectDragSource, connectDropTarget, isOver, value }: DraggableChipProps<Value> & CollectedProps) =>
69             compose(
70                 connectDragSource,
71                 connectDropTarget,
72             )(
73                 <span>
74                     <Chip
75                         color={isOver ? 'primary' : 'default'}
76                         onDelete={this.deleteValue(value)}
77                         label={this.props.getLabel ?
78                             this.props.getLabel(value)
79                             : typeof value === 'object'
80                                 ? JSON.stringify(value)
81                                 : value} />
82                 </span>
83             )
84     );
85
86     deleteValue = (value: Value) => () => {
87         const { values } = this.props;
88         const index = values.indexOf(value);
89         const newValues = values.slice(0);
90         newValues.splice(index, 1);
91         this.props.onChange(newValues);
92     }
93 }
94
95 interface CollectedProps {
96     connectDragSource: ConnectDragSource;
97     connectDropTarget: ConnectDropTarget;
98
99     isOver: boolean;
100 }
101
102 interface DraggableChipProps<Value> {
103     value: Value;
104 }