// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 import React from 'react'; import { Chip, Grid, StyleRulesCallback, withStyles } from '@material-ui/core'; import { DragSource, DragSourceSpec, DragSourceCollector, ConnectDragSource, DropTarget, DropTargetSpec, DropTargetCollector, ConnectDropTarget } from 'react-dnd'; import { compose } from 'lodash/fp'; import { WithStyles } from '@material-ui/core/styles'; interface ChipsProps<Value> { values: Value[]; getLabel?: (value: Value) => string; filler?: React.ReactNode; deletable?: boolean; orderable?: boolean; onChange: (value: Value[]) => void; clickable?: boolean; } type CssRules = 'root'; const styles: StyleRulesCallback<CssRules> = ({ spacing }) => ({ root: { margin: `0px -${spacing.unit / 2}px`, }, }); export const Chips = withStyles(styles)( class Chips<Value> extends React.Component<ChipsProps<Value> & WithStyles<CssRules>> { render() { const { values, filler } = this.props; return <Grid container spacing={8} className={this.props.classes.root}> {values && values.map(this.renderChip)} {filler && <Grid item xs>{filler}</Grid>} </Grid>; } renderChip = (value: Value, index: number) => { const { deletable, getLabel } = this.props; return <Grid item key={index}> <Chip onDelete={deletable ? this.deleteValue(value) : undefined} label={getLabel !== undefined ? getLabel(value) : value} /> </Grid> } type = 'chip'; dragSpec: DragSourceSpec<DraggableChipProps<Value>, { value: Value }> = { beginDrag: ({ value }) => ({ value }), endDrag: ({ value: dragValue }, monitor) => { const result = monitor.getDropResult(); if (result) { const { value: dropValue } = monitor.getDropResult(); const dragIndex = this.props.values.indexOf(dragValue); const dropIndex = this.props.values.indexOf(dropValue); const newValues = this.props.values.slice(0); if (dragIndex < dropIndex) { newValues.splice(dragIndex, 1); newValues.splice(dropIndex - 1 || 0, 0, dragValue); } else if (dragIndex > dropIndex) { newValues.splice(dragIndex, 1); newValues.splice(dropIndex, 0, dragValue); } this.props.onChange(newValues); } } }; dragCollector: DragSourceCollector<{}> = connect => ({ connectDragSource: connect.dragSource(), }) dropSpec: DropTargetSpec<DraggableChipProps<Value>> = { drop: ({ value }) => ({ value }), }; dropCollector: DropTargetCollector<{}> = (connect, monitor) => ({ connectDropTarget: connect.dropTarget(), isOver: monitor.isOver(), }) chip = compose( DragSource(this.type, this.dragSpec, this.dragCollector), DropTarget(this.type, this.dropSpec, this.dropCollector), )( ({ connectDragSource, connectDropTarget, isOver, value }: DraggableChipProps<Value> & CollectedProps) => { const connect = compose( connectDragSource, connectDropTarget, ); const chip = <span> <Chip color={isOver ? 'primary' : 'default'} onDelete={this.props.deletable ? this.deleteValue(value) : undefined} clickable={this.props.clickable} label={this.props.getLabel ? this.props.getLabel(value) : typeof value === 'object' ? JSON.stringify(value) : value} /> </span>; return this.props.orderable ? connect(chip) : chip; } ); deleteValue = (value: Value) => () => { const { values } = this.props; const index = values.indexOf(value); const newValues = values.slice(0); newValues.splice(index, 1); this.props.onChange(newValues); } }); interface CollectedProps { connectDragSource: ConnectDragSource; connectDropTarget: ConnectDropTarget; isOver: boolean; } interface DraggableChipProps<Value> { value: Value; }