adds the start of dnd functionality
This commit is contained in:
@@ -9,7 +9,7 @@ import './index.scss';
|
||||
|
||||
const baseClass = 'icon-button';
|
||||
|
||||
const IconButton = ({ iconName, className, ...rest }) => {
|
||||
const IconButton = React.forwardRef(({ iconName, className, ...rest }, ref) => {
|
||||
const classes = [
|
||||
baseClass,
|
||||
className && className,
|
||||
@@ -25,14 +25,16 @@ const IconButton = ({ iconName, className, ...rest }) => {
|
||||
const Icon = icons[iconName] || icons.arrow;
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={classes}
|
||||
{...rest}
|
||||
>
|
||||
<Icon />
|
||||
</Button>
|
||||
<span ref={ref}>
|
||||
<Button
|
||||
className={classes}
|
||||
{...rest}
|
||||
>
|
||||
<Icon />
|
||||
</Button>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
IconButton.defaultProps = {
|
||||
className: '',
|
||||
|
||||
@@ -4,22 +4,22 @@ const splitRowsFromState = (state, name) => {
|
||||
// Take a copy of state
|
||||
const remainingState = { ...state };
|
||||
|
||||
const rowObject = {};
|
||||
const rowsFromStateObject = {};
|
||||
|
||||
// Loop over all keys from state
|
||||
// If the key begins with the name of the parent field,
|
||||
// Add value to rowObject and delete it from remaining state
|
||||
// Add value to rowsFromStateObject and delete it from remaining state
|
||||
Object.keys(state).forEach((key) => {
|
||||
if (key.indexOf(`${name}.`) === 0) {
|
||||
rowObject[key] = state[key];
|
||||
rowsFromStateObject[key] = state[key];
|
||||
delete remainingState[key];
|
||||
}
|
||||
});
|
||||
|
||||
const rows = unflatten(rowObject);
|
||||
const rowsFromState = unflatten(rowsFromStateObject);
|
||||
|
||||
return {
|
||||
rows: rows[name] || [],
|
||||
rowsFromState: rowsFromState[name] || [],
|
||||
remainingState,
|
||||
};
|
||||
};
|
||||
@@ -33,29 +33,46 @@ function fieldReducer(state, action) {
|
||||
|
||||
case 'REMOVE_ROW': {
|
||||
const { rowIndex, name } = action;
|
||||
const { rows, remainingState } = splitRowsFromState(state, name);
|
||||
const { rowsFromState, remainingState } = splitRowsFromState(state, name);
|
||||
|
||||
rows.splice(rowIndex, 1);
|
||||
rowsFromState.splice(rowIndex, 1);
|
||||
|
||||
return {
|
||||
...remainingState,
|
||||
...(flatten({ [name]: rows }, { maxDepth: 3 })),
|
||||
...(flatten({ [name]: rowsFromState }, { maxDepth: 3 })),
|
||||
};
|
||||
}
|
||||
|
||||
case 'ADD_ROW': {
|
||||
const { rowIndex, name, fields } = action;
|
||||
const { rows, remainingState } = splitRowsFromState(state, name);
|
||||
const { rowsFromState, remainingState } = splitRowsFromState(state, name);
|
||||
|
||||
// Get names of sub fields
|
||||
const subFields = fields.reduce((acc, field) => ({ ...acc, [field.name]: {} }), {});
|
||||
|
||||
// Add new object containing subfield names to rows array
|
||||
rows.splice(rowIndex + 1, 0, subFields);
|
||||
// Add new object containing subfield names to rowsFromState array
|
||||
rowsFromState.splice(rowIndex + 1, 0, subFields);
|
||||
|
||||
return {
|
||||
...remainingState,
|
||||
...(flatten({ [name]: rows }, { maxDepth: 3 })),
|
||||
...(flatten({ [name]: rowsFromState }, { maxDepth: 3 })),
|
||||
};
|
||||
}
|
||||
|
||||
case 'MOVE_ROW': {
|
||||
const { moveFromIndex, moveToIndex, name } = action;
|
||||
const { rowsFromState, remainingState } = splitRowsFromState(state, name);
|
||||
|
||||
// copy the row to move
|
||||
const copyOfMovingRow = rowsFromState[moveFromIndex];
|
||||
// delete the row by index
|
||||
rowsFromState.splice(moveFromIndex, 1);
|
||||
// insert row copyOfMovingRow back in
|
||||
rowsFromState.splice(moveToIndex, 0, copyOfMovingRow);
|
||||
|
||||
return {
|
||||
...remainingState,
|
||||
...(flatten({ [name]: rowsFromState }, { maxDepth: 3 })),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
import RenderFields from '../../../RenderFields';
|
||||
import IconButton from '../../../../controls/IconButton';
|
||||
@@ -11,63 +12,89 @@ import './index.scss';
|
||||
const baseClass = 'repeater-section';
|
||||
|
||||
const RepeaterSection = ({
|
||||
addRow, removeRow, rowIndex, parentName, fields, fieldState,
|
||||
addRow, removeRow, rowIndex, parentName, fields, fieldState, forceContentCollapse,
|
||||
}) => {
|
||||
const [isSectionOpen, setIsSectionOpen] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (forceContentCollapse) setIsSectionOpen(false);
|
||||
}, [forceContentCollapse]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${baseClass}`}
|
||||
<Draggable
|
||||
draggableId={`row-${rowIndex}`}
|
||||
index={rowIndex}
|
||||
>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<Pill>
|
||||
{parentName}
|
||||
</Pill>
|
||||
<h4 className={`${baseClass}__header__heading`}>Title Goes Here</h4>
|
||||
{(providedDrag, snapshot) => (
|
||||
<div
|
||||
ref={providedDrag.innerRef}
|
||||
className={baseClass}
|
||||
{...providedDrag.draggableProps}
|
||||
>
|
||||
<div className={`${baseClass}__header`}>
|
||||
<Pill>
|
||||
{parentName}
|
||||
</Pill>
|
||||
<h4 className={`${baseClass}__header__heading`}>Title Goes Here</h4>
|
||||
|
||||
<div className={`${baseClass}__header__controls`}>
|
||||
<IconButton
|
||||
iconName="crosshair"
|
||||
onClick={addRow}
|
||||
size="small"
|
||||
/>
|
||||
<div className={`${baseClass}__header__controls`}>
|
||||
|
||||
<IconButton
|
||||
iconName="crossOut"
|
||||
onClick={removeRow}
|
||||
size="small"
|
||||
/>
|
||||
<IconButton
|
||||
{...providedDrag.dragHandleProps}
|
||||
iconName="crosshair"
|
||||
onClick={addRow}
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={`${baseClass}__collapse__icon ${baseClass}__collapse__icon--${isSectionOpen ? 'open' : 'closed'}`}
|
||||
iconName="arrow"
|
||||
onClick={() => setIsSectionOpen(state => !state)}
|
||||
size="small"
|
||||
/>
|
||||
<IconButton
|
||||
iconName="crosshair"
|
||||
onClick={addRow}
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
iconName="crossOut"
|
||||
onClick={removeRow}
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={`${baseClass}__collapse__icon ${baseClass}__collapse__icon--${isSectionOpen ? 'open' : 'closed'}`}
|
||||
iconName="arrow"
|
||||
onClick={() => setIsSectionOpen(state => !state)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimateHeight
|
||||
className={`${baseClass}__content`}
|
||||
height={isSectionOpen ? 'auto' : 0}
|
||||
duration={150}
|
||||
>
|
||||
<RenderFields
|
||||
key={rowIndex}
|
||||
fields={fields.map((field) => {
|
||||
const fieldName = `${parentName}.${rowIndex}.${field.name}`;
|
||||
return ({
|
||||
...field,
|
||||
name: fieldName,
|
||||
defaultValue: fieldState?.[fieldName]?.value,
|
||||
});
|
||||
})}
|
||||
/>
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimateHeight
|
||||
className={`${baseClass}__content`}
|
||||
height={isSectionOpen ? 'auto' : 0}
|
||||
duration={150}
|
||||
>
|
||||
<RenderFields
|
||||
key={rowIndex}
|
||||
fields={fields.map((field) => {
|
||||
const fieldName = `${parentName}.${rowIndex}.${field.name}`;
|
||||
return ({
|
||||
...field,
|
||||
name: fieldName,
|
||||
defaultValue: fieldState?.[fieldName]?.value,
|
||||
});
|
||||
})}
|
||||
/>
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
RepeaterSection.defaultProps = {
|
||||
forceContentCollapse: false,
|
||||
};
|
||||
|
||||
RepeaterSection.propTypes = {
|
||||
addRow: PropTypes.func.isRequired,
|
||||
removeRow: PropTypes.func.isRequired,
|
||||
@@ -75,6 +102,7 @@ RepeaterSection.propTypes = {
|
||||
parentName: PropTypes.string.isRequired,
|
||||
fields: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
fieldState: PropTypes.shape({}).isRequired,
|
||||
forceContentCollapse: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default RepeaterSection;
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
position: relative;
|
||||
border: $stroke-width solid lighten($gray, 20%);
|
||||
|
||||
+ .repeater-section {
|
||||
margin-top: base(.5);
|
||||
}
|
||||
|
||||
&__collapse__icon {
|
||||
svg {
|
||||
transition: 150ms linear;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
|
||||
import FormContext from '../../Form/Context';
|
||||
import Section from '../../../layout/Section';
|
||||
@@ -13,6 +14,7 @@ const Repeater = (props) => {
|
||||
const [rowCount, setRowCount] = useState(0);
|
||||
const formContext = useContext(FormContext);
|
||||
const { fields: fieldState, dispatchFields } = formContext;
|
||||
const [forceContentCollapse, setForceContentCollapse] = useState(false);
|
||||
|
||||
const {
|
||||
label,
|
||||
@@ -37,32 +39,75 @@ const Repeater = (props) => {
|
||||
setRowCount(rowCount - 1);
|
||||
};
|
||||
|
||||
const moveRow = (rowIndex, moveToIndex) => {
|
||||
dispatchFields({
|
||||
type: 'MOVE_ROW', rowIndex, moveToIndex, name,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setRowCount(defaultValue.length);
|
||||
}, [defaultValue]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Section
|
||||
heading={label}
|
||||
className="repeater"
|
||||
>
|
||||
{rowCount > 0 && Array.from(Array(rowCount).keys()).map((_, rowIndex) => {
|
||||
return (
|
||||
<RepeaterSection
|
||||
key={rowIndex}
|
||||
addRow={() => addRow(rowIndex)}
|
||||
removeRow={() => removeRow(rowIndex)}
|
||||
rowIndex={rowIndex}
|
||||
fieldState={fieldState}
|
||||
fields={fields}
|
||||
parentName={name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
function onBeforeCapture(result) {
|
||||
setForceContentCollapse(true);
|
||||
}
|
||||
|
||||
</Section>
|
||||
</div>
|
||||
function onDragEnd(result) {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moveFromIndex = result.source.index;
|
||||
const moveToIndex = result.destination.index;
|
||||
dispatchFields({
|
||||
type: 'MOVE_ROW', moveFromIndex, moveToIndex, name,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropContext
|
||||
onDragEnd={onDragEnd}
|
||||
onBeforeCapture={onBeforeCapture}
|
||||
>
|
||||
<div className={baseClass}>
|
||||
<Section
|
||||
heading={label}
|
||||
className="repeater"
|
||||
rowCount={rowCount}
|
||||
addInitialRow={() => addRow(0)}
|
||||
>
|
||||
{rowCount !== 0
|
||||
&& (
|
||||
<Droppable droppableId="repeater-drop">
|
||||
{(provided) => {
|
||||
return Array.from(Array(rowCount).keys()).map((_, rowIndex) => {
|
||||
return (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
className={`${baseClass}__row`}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
<RepeaterSection
|
||||
key={rowIndex}
|
||||
addRow={() => addRow(rowIndex)}
|
||||
removeRow={() => removeRow(rowIndex)}
|
||||
moveRow={() => moveRow(rowIndex)}
|
||||
rowIndex={rowIndex}
|
||||
fieldState={fieldState}
|
||||
fields={fields}
|
||||
parentName={name}
|
||||
forceContentCollapse={forceContentCollapse}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}}
|
||||
</Droppable>
|
||||
)}
|
||||
</Section>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
@import '../../../../scss/styles.scss';
|
||||
|
||||
.field-repeater {
|
||||
&__row {
|
||||
+ .field-repeater__row {
|
||||
margin-top: base(.5);
|
||||
}
|
||||
}
|
||||
.content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
import './index.scss';
|
||||
import IconButton from '../../controls/IconButton';
|
||||
|
||||
const baseClass = 'section';
|
||||
|
||||
const Section = (props) => {
|
||||
const { className, heading, children } = props;
|
||||
const Section = React.forwardRef((props, ref) => {
|
||||
const {
|
||||
className, heading, children, rowCount, addInitialRow,
|
||||
} = props;
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
@@ -17,18 +20,21 @@ const Section = (props) => {
|
||||
|
||||
const [isSectionOpen, setIsSectionOpen] = useState(true);
|
||||
|
||||
const iconProps = {};
|
||||
iconProps.iconName = `${rowCount === 0 ? 'crosshair' : 'arrow'}`;
|
||||
iconProps.onClick = rowCount === 0 ? () => addInitialRow() : () => setIsSectionOpen(state => !state);
|
||||
|
||||
return (
|
||||
<section className={classes}>
|
||||
{heading
|
||||
&& (
|
||||
<header>
|
||||
<header ref={ref}>
|
||||
<h2 className={`${baseClass}__heading`}>{heading}</h2>
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<IconButton
|
||||
className={`${baseClass}__collapse__icon ${baseClass}__collapse__icon--${isSectionOpen ? 'open' : 'closed'}`}
|
||||
iconName="arrow"
|
||||
size="small"
|
||||
onClick={() => setIsSectionOpen(state => !state)}
|
||||
{...iconProps}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
@@ -40,14 +46,12 @@ const Section = (props) => {
|
||||
height={isSectionOpen ? 'auto' : 0}
|
||||
duration={150}
|
||||
>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
{children}
|
||||
</AnimateHeight>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
Section.defaultProps = {
|
||||
className: '',
|
||||
|
||||
@@ -28,7 +28,9 @@ section.section {
|
||||
}
|
||||
|
||||
&__content {
|
||||
@include pad;
|
||||
> div {
|
||||
@include pad;
|
||||
}
|
||||
}
|
||||
|
||||
&__controls {
|
||||
|
||||
Reference in New Issue
Block a user