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