Skip to content

Instantly share code, notes, and snippets.

@mmintel
Last active March 27, 2020 05:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mmintel/bf7814145fdb69454d40646ef8881641 to your computer and use it in GitHub Desktop.
Save mmintel/bf7814145fdb69454d40646ef8881641 to your computer and use it in GitHub Desktop.
TinaCMS relation field
import React from "react"
import styled, { css } from "styled-components"
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"
import { AddIcon, DragIcon, ReorderIcon, TrashIcon } from "@tinacms/icons"
import {
padding,
color,
radius,
font,
IconButton,
shadow,
} from "@tinacms/styles"
const Relation = ({ multiple, ...props }) => {
if (multiple) {
return <MultipleRelations {...props} />
}
return <SingleRelation {...props} />
}
Relation.defaultProps = {
multiple: false,
noDataText: 'There is no data.',
}
export default Relation;
const SingleRelation = ({ data, itemProps, noDataText, input, field }) => {
const options = data.map(item => itemProps(item));
const selectOptions = [
{
key: null,
label: '---',
},
...options
]
return (
<>
<RelationHeader>
<FieldLabel>{field.label}</FieldLabel>
</RelationHeader>
<RelationBody>
<Select
input={input}
field={field}
options={selectOptions}
noDataText={noDataText}
/>
</RelationBody>
</>
)
};
const Select = ({ input, field, options, noDataText }) => {
return (
<SelectElement>
<select
id={input.name}
value={input.value}
onChange={input.onChange}
disabled={field.disabled}
{...input}
>
{options ? (
options.map(option => (
<option value={option.key} key={option.key}>
{option.label}
</option>
))
) : (
<option>{noDataText}</option>
)}
</select>
</SelectElement>
)
}
const MultipleRelations = ({ data, itemProps, noDataText, input, field, form, sortable }) => {
const [visible, setVisible] = React.useState(false)
const [availableData, setAvailableData] = React.useState(data)
const value = input.value || [];
React.useEffect(() => {
setAvailableData(data);
}, [data])
React.useEffect(() => {
const newAvailableData = data.filter(i => !value.includes(i.key));
setAvailableData(newAvailableData);
}, [value])
const addRelation = React.useCallback(
value => {
form.mutators.insert(field.name, 0, value)
},
[field.name, form.mutators]
)
const moveArrayItem = React.useCallback(
(result) => {
if (!result.destination || !form) return
const name = result.type
form.mutators.move(
name,
result.source.index,
result.destination.index
)
},
[form]
)
const removeRelation = (index, field) => {
form.mutators.remove(field.name, index);
}
return (
<DragDropContext onDragEnd={moveArrayItem}>
<RelationHeader>
<FieldLabel>{field.label}</FieldLabel>
{ !!availableData.length && (
<>
<IconButton
primary
small
onClick={() => setVisible(!visible)}
open={visible}
>
<AddIcon />
</IconButton>
<RelationMenu open={visible}>
<RelationMenuList>
{availableData.map(item => {
const props = itemProps(item);
return (
<RelationOption
key={props.key}
onClick={() => {
addRelation(props.key)
setVisible(false)
}}
>
{props.label}
</RelationOption>
)
})}
</RelationMenuList>
</RelationMenu>
</>
)}
</RelationHeader>
<Droppable droppableId={field.name} type={field.name}>
{provider => (
<RelationBody ref={provider.innerRef}>
{data.length === 0 && (
<EmptyList>{noDataText}</EmptyList>
)}
{value.map((key, index) => {
const item = data.find(item => itemProps(item).key === key)
return (
<RelationListItem
item={item}
form={form}
field={field}
index={index}
key={key}
onRemove={removeRelation}
isDragDisabled={sortable === false || value.length <= 1}
/>
)
})}
{provider.placeholder}
</RelationBody>
)}
</Droppable>
</DragDropContext>
)
}
const RelationListItem = ({ item, form, field, index, onRemove, isDragDisabled }) => {
const handleRemove = React.useCallback(() => {
onRemove(index, field)
}, [form, field, index])
return (
<Draggable
key={index}
type={field.name}
draggableId={`${field.name}.${index}`}
index={index}
isDragDisabled={isDragDisabled}
>
{(provider, snapshot) => (
<ListItem
ref={provider.innerRef}
isDragging={snapshot.isDragging}
{...provider.draggableProps}
{...provider.dragHandleProps}
>
{ !isDragDisabled && (
<DragHandle />
)}
<ItemLabel>
{item && item.label ? (
item.label
) : (
<Placeholder>Unknown Item</Placeholder>
)}
</ItemLabel>
<DeleteButton onClick={handleRemove}>
<TrashIcon />
</DeleteButton>
</ListItem>
)}
</Draggable>
)
}
const SelectElement = styled.div`
display: block;
position: relative;
select {
display: block;
font-family: inherit;
max-width: 100%;
padding: ${padding('small')};
border-radius: ${radius('small')};
background: ${color.grey(0)};
font-size: ${font.size(2)};
line-height: 1.35;
position: relative;
background-color: ${color.grey()};
transition: all 85ms ease-out;
border: 1px solid ${color.grey(2)};
width: 100%;
margin: 0;
appearance: none;
outline: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');
background-repeat: no-repeat;
background-position: right 0.7em top 50%;
background-size: 0.65em auto;
&[disabled] {
background-image: none;
}
}
`
const RelationBody = styled.div`
margin-bottom: 1.5rem;
`
const Placeholder = styled.span`
opacity: 0.3;
text-transform: italic;
`
const ItemLabel = styled.label`
margin: 0;
font-size: ${font.size(2)};
font-weight: 500;
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
align-self: center;
color: inherit;
transition: all 85ms ease-out;
text-align: left;
padding: 0 0.5rem;
pointer-events: none;
${props =>
props.error &&
css`
color: ${color.error()} !important;
`};
`
const DragHandle = styled(function DragHandle({ ...styleProps }) {
return (
<div {...styleProps}>
<DragIcon />
<ReorderIcon />
</div>
)
})`
margin: 0;
flex: 0 0 auto;
width: 2rem;
position: relative;
fill: inherit;
padding: 0.75rem 0;
transition: all 85ms ease-out;
svg {
position: absolute;
left: 50%;
top: 50%;
width: 1.25rem;
height: 1.25rem;
transform: translate3d(-50%, -50%, 0);
transition: all 85ms ease-out;
}
svg:last-child {
opacity: 0;
}
`
const DeleteButton = styled.button`
text-align: center;
flex: 0 0 auto;
border: 0;
background: transparent;
cursor: pointer;
padding: 0.75rem 0.5rem;
margin: 0;
transition: all 85ms ease-out;
&:hover {
background-color: ${color.grey(2)};
}
`
const ListItem = styled.div`
position: relative;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: stretch;
background-color: white;
border: 1px solid ${color.grey(2)};
margin: 0 0 -1px 0;
overflow: visible;
line-height: 1.35;
padding: 0;
font-size: ${font.size(2)};
font-weight: 500;
${ItemLabel} {
color: #282828;
align-self: center;
max-width: 100%;
}
svg {
fill: ${color.grey(3)};
width: 1.25rem;
height: auto;
transition: fill 85ms ease-out;
}
&:hover {
background-color: #f6f6f9;
cursor: grab;
${ItemLabel} {
color: #0084ff;
}
${DeleteButton} {
svg {
fill: ${color.grey(4)};
}
&:hover {
svg {
fill: ${color.grey(8)};
}
}
}
${DragHandle} {
svg {
fill: ${color.grey(8)};
}
svg:first-child {
opacity: 0;
}
svg:last-child {
opacity: 1;
}
}
}
&:first-child {
border-radius: 0.25rem 0.25rem 0 0;
}
&:nth-last-child(2) {
border-radius: 0 0 0.25rem 0.25rem;
&:first-child {
border-radius: ${radius("small")};
}
}
${p =>
p.isDragging &&
css`
border-radius: ${radius("small")};
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.12);
svg {
fill: ${color.grey(8)};
}
${ItemLabel} {
color: #0084ff;
}
${DragHandle} {
svg:first-child {
opacity: 0;
}
svg:last-child {
opacity: 1;
}
}
`};
`
const EmptyList = styled.div`
text-align: center;
border-radius: ${radius("small")};
background-color: ${color.grey(2)};
color: ${color.grey(4)};
line-height: 1.35;
padding: 0.75rem 0;
font-size: ${font.size(2)};
font-weight: 500;
`
const RelationHeader = styled.div`
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
`
const FieldLabel = styled.label`
margin: 0;
font-size: ${font.size(1)};
font-weight: 600;
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: ${color.grey(7)};
transition: all 85ms ease-out;
text-align: left;
${props =>
props.error &&
css`
color: ${color.error()} !important;
`};
`
const RelationMenu = styled.div`
min-width: 12rem;
border-radius: ${radius()};
border: 1px solid #efefef;
display: block;
position: absolute;
top: 0;
right: 0;
transform: translate3d(0, 0, 0) scale3d(0.5, 0.5, 1);
opacity: 0;
pointer-events: none;
transition: all 150ms ease-out;
transform-origin: 100% 0;
box-shadow: ${shadow("big")};
background-color: white;
overflow: hidden;
z-index: 100;
${props =>
props.open &&
css`
opacity: 1;
pointer-events: all;
transform: translate3d(0, 2.25rem, 0) scale3d(1, 1, 1);
`};
`
const RelationMenuList = styled.div`
display: flex;
flex-direction: column;
`
const RelationOption = styled.button`
position: relative;
text-align: center;
font-size: ${font.size(0)};
padding: ${padding("small")};
font-weight: 500;
width: 100%;
background: none;
cursor: pointer;
outline: none;
border: 0;
transition: all 85ms ease-out;
&:hover {
color: ${color.primary()};
background-color: #f6f6f9;
}
&:not(:last-child) {
border-bottom: 1px solid #efefef;
}
`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment