Merge branch 'github-pr-224'
[arvados.git] / services / workbench2 / 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 React from 'react';
6 import { Chip, Grid, StyleRulesCallback, withStyles } from '@material-ui/core';
7 import {
8     DragSource,
9     DragSourceSpec,
10     DragSourceCollector,
11     ConnectDragSource,
12     DropTarget,
13     DropTargetSpec,
14     DropTargetCollector,
15     ConnectDropTarget
16 } from 'react-dnd';
17 import { compose } from 'lodash/fp';
18 import { WithStyles } from '@material-ui/core/styles';
19 interface ChipsProps<Value> {
20     values: Value[];
21     getLabel?: (value: Value) => string;
22     filler?: React.ReactNode;
23     deletable?: boolean;
24     orderable?: boolean;
25     onChange: (value: Value[]) => void;
26     clickable?: boolean;
27 }
28
29 type CssRules = 'root';
30
31 const styles: StyleRulesCallback<CssRules> = ({ spacing }) => ({
32     root: {
33         margin: `0px -${spacing.unit / 2}px`,
34     },
35 });
36 export const Chips = withStyles(styles)(
37     class Chips<Value> extends React.Component<ChipsProps<Value> & WithStyles<CssRules>> {
38         render() {
39             const { values, filler } = this.props;
40             return <Grid container spacing={8} className={this.props.classes.root}>
41                 {values && values.map(this.renderChip)}
42                 {filler && <Grid item xs>{filler}</Grid>}
43             </Grid>;
44         }
45
46         renderChip = (value: Value, index: number) => {
47             const { deletable, getLabel } = this.props;
48             return <Grid item key={index}>
49                 <Chip onDelete={deletable ? this.deleteValue(value) : undefined}
50                     label={getLabel !== undefined ? getLabel(value) : value} />
51             </Grid>
52         }
53
54         type = 'chip';
55
56         dragSpec: DragSourceSpec<DraggableChipProps<Value>, { value: Value }> = {
57             beginDrag: ({ value }) => ({ value }),
58             endDrag: ({ value: dragValue }, monitor) => {
59                 const result = monitor.getDropResult();
60                 if (result) {
61                     const { value: dropValue } = monitor.getDropResult();
62                     const dragIndex = this.props.values.indexOf(dragValue);
63                     const dropIndex = this.props.values.indexOf(dropValue);
64                     const newValues = this.props.values.slice(0);
65                     if (dragIndex < dropIndex) {
66                         newValues.splice(dragIndex, 1);
67                         newValues.splice(dropIndex - 1 || 0, 0, dragValue);
68                     } else if (dragIndex > dropIndex) {
69                         newValues.splice(dragIndex, 1);
70                         newValues.splice(dropIndex, 0, dragValue);
71                     }
72                     this.props.onChange(newValues);
73                 }
74             }
75         };
76
77         dragCollector: DragSourceCollector<{}> = connect => ({
78             connectDragSource: connect.dragSource(),
79         })
80
81         dropSpec: DropTargetSpec<DraggableChipProps<Value>> = {
82             drop: ({ value }) => ({ value }),
83         };
84
85         dropCollector: DropTargetCollector<{}> = (connect, monitor) => ({
86             connectDropTarget: connect.dropTarget(),
87             isOver: monitor.isOver(),
88         })
89         chip = compose(
90             DragSource(this.type, this.dragSpec, this.dragCollector),
91             DropTarget(this.type, this.dropSpec, this.dropCollector),
92         )(
93             ({ connectDragSource, connectDropTarget, isOver, value }: DraggableChipProps<Value> & CollectedProps) => {
94                 const connect = compose(
95                     connectDragSource,
96                     connectDropTarget,
97                 );
98
99                 const chip =
100                     <span>
101                         <Chip
102                             color={isOver ? 'primary' : 'default'}
103                             onDelete={this.props.deletable
104                                 ? this.deleteValue(value)
105                                 : undefined}
106                             clickable={this.props.clickable}
107                             label={this.props.getLabel ?
108                                 this.props.getLabel(value)
109                                 : typeof value === 'object'
110                                     ? JSON.stringify(value)
111                                     : value} />
112                     </span>;
113
114                 return this.props.orderable
115                     ? connect(chip)
116                     : chip;
117             }
118         );
119
120         deleteValue = (value: Value) => () => {
121             const { values } = this.props;
122             const index = values.indexOf(value);
123             const newValues = values.slice(0);
124             newValues.splice(index, 1);
125             this.props.onChange(newValues);
126         }
127     });
128
129 interface CollectedProps {
130     connectDragSource: ConnectDragSource;
131     connectDropTarget: ConnectDropTarget;
132
133     isOver: boolean;
134 }
135
136 interface DraggableChipProps<Value> {
137     value: Value;
138 }