feat: reorder components in unit page [FC-00083] (#1816)
Reorders components in unit page via drag and drop. This PR also refactors and moves draggable list and sortable item components to appropriate location. Course authors will be affected by this change.
This commit is contained in:
101
src/generic/DraggableList/DraggableList.jsx
Normal file
101
src/generic/DraggableList/DraggableList.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragOverlay,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||
|
||||
const DraggableList = ({
|
||||
itemList,
|
||||
setState,
|
||||
updateOrder,
|
||||
children,
|
||||
renderOverlay,
|
||||
activeId,
|
||||
setActiveId,
|
||||
}) => {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback((event) => {
|
||||
const { active, over } = event;
|
||||
if (active.id !== over.id) {
|
||||
let updatedArray;
|
||||
setState(() => {
|
||||
const [activeElement] = itemList.filter(item => item.id === active.id);
|
||||
const [overElement] = itemList.filter(item => item.id === over.id);
|
||||
const oldIndex = itemList.indexOf(activeElement);
|
||||
const newIndex = itemList.indexOf(overElement);
|
||||
updatedArray = arrayMove(itemList, oldIndex, newIndex);
|
||||
return updatedArray;
|
||||
});
|
||||
updateOrder()(updatedArray);
|
||||
}
|
||||
setActiveId?.(null);
|
||||
}, [updateOrder, setActiveId]);
|
||||
|
||||
const handleDragStart = useCallback((event) => {
|
||||
setActiveId?.(event.active.id);
|
||||
}, [setActiveId]);
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={itemList}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{children}
|
||||
</SortableContext>
|
||||
{renderOverlay && createPortal(
|
||||
<DragOverlay>
|
||||
{renderOverlay(activeId)}
|
||||
</DragOverlay>,
|
||||
document.body,
|
||||
)}
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
DraggableList.defaultProps = {
|
||||
renderOverlay: undefined,
|
||||
activeId: null,
|
||||
setActiveId: () => {},
|
||||
};
|
||||
|
||||
DraggableList.propTypes = {
|
||||
itemList: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
setState: PropTypes.func.isRequired,
|
||||
updateOrder: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
renderOverlay: PropTypes.func,
|
||||
activeId: PropTypes.string,
|
||||
setActiveId: PropTypes.func,
|
||||
};
|
||||
|
||||
export default DraggableList;
|
||||
@@ -4,19 +4,20 @@ import { intlShape, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import {
|
||||
Col, Icon, Row,
|
||||
ActionRow, Card, Icon, IconButtonWithTooltip,
|
||||
} from '@openedx/paragon';
|
||||
import { DragIndicator } from '@openedx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SortableItem = ({
|
||||
id,
|
||||
category,
|
||||
isDraggable,
|
||||
isDroppable,
|
||||
componentStyle,
|
||||
actions,
|
||||
actionStyle,
|
||||
children,
|
||||
isClickable,
|
||||
onClick,
|
||||
disabled,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
@@ -26,74 +27,72 @@ const SortableItem = ({
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
setActivatorNodeRef,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id,
|
||||
data: {
|
||||
category,
|
||||
},
|
||||
disabled: {
|
||||
draggable: !isDraggable,
|
||||
droppable: !isDroppable,
|
||||
},
|
||||
animateLayoutChanges: () => false,
|
||||
disabled: {
|
||||
draggable: disabled,
|
||||
},
|
||||
});
|
||||
|
||||
const style = {
|
||||
position: 'relative',
|
||||
zIndex: isDragging ? 200 : undefined,
|
||||
transform: CSS.Translate.toString(transform),
|
||||
zIndex: isDragging ? 200 : undefined,
|
||||
transition,
|
||||
background: 'white',
|
||||
padding: '1rem 1.5rem',
|
||||
marginBottom: '1.5rem',
|
||||
borderRadius: '0.35rem',
|
||||
boxShadow: '0 0 .125rem rgba(0, 0, 0, .15), 0 0 .25rem rgba(0, 0, 0, .15)',
|
||||
...componentStyle,
|
||||
};
|
||||
|
||||
return (
|
||||
<Row
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="mx-0"
|
||||
>
|
||||
<Col className="extend-margin px-0">
|
||||
<Card
|
||||
style={style}
|
||||
className="mx-0"
|
||||
isClickable={isClickable}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ActionRow style={actionStyle}>
|
||||
{actions}
|
||||
{!disabled && (
|
||||
<IconButtonWithTooltip
|
||||
key="drag-to-reorder-icon"
|
||||
ref={setActivatorNodeRef}
|
||||
tooltipPlacement="top"
|
||||
tooltipContent={intl.formatMessage(messages.tooltipContent)}
|
||||
src={DragIndicator}
|
||||
iconAs={Icon}
|
||||
variant="light"
|
||||
alt={intl.formatMessage(messages.tooltipContent)}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
)}
|
||||
</ActionRow>
|
||||
{children}
|
||||
</Col>
|
||||
{isDraggable && (
|
||||
<button
|
||||
ref={setActivatorNodeRef}
|
||||
key="drag-to-reorder-icon"
|
||||
aria-label={intl.formatMessage(messages.tooltipContent)}
|
||||
className="btn-icon btn-icon-secondary btn-icon-md"
|
||||
type="button"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<span className="btn-icon__icon-container">
|
||||
<Icon src={DragIndicator} />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SortableItem.defaultProps = {
|
||||
componentStyle: null,
|
||||
isDroppable: true,
|
||||
isDraggable: true,
|
||||
actions: null,
|
||||
actionStyle: null,
|
||||
isClickable: false,
|
||||
onClick: null,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
SortableItem.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
category: PropTypes.string.isRequired,
|
||||
isDroppable: PropTypes.bool,
|
||||
isDraggable: PropTypes.bool,
|
||||
children: PropTypes.node.isRequired,
|
||||
actions: PropTypes.node,
|
||||
actionStyle: PropTypes.shape({}),
|
||||
componentStyle: PropTypes.shape({}),
|
||||
isClickable: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
5
src/generic/DraggableList/index.jsx
Normal file
5
src/generic/DraggableList/index.jsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import DraggableList from './DraggableList';
|
||||
import SortableItem from './SortableItem';
|
||||
|
||||
export { SortableItem };
|
||||
export default DraggableList;
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const DragContext = React.createContext({ activeId: '', overId: '', children: undefined });
|
||||
|
||||
const DragContextProvider = ({ activeId, overId, children }) => {
|
||||
const contextValue = React.useMemo(() => ({
|
||||
activeId,
|
||||
overId,
|
||||
}), [activeId, overId]);
|
||||
return (
|
||||
<DragContext.Provider
|
||||
value={contextValue}
|
||||
>
|
||||
{children}
|
||||
</DragContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
DragContextProvider.defaultProps = {
|
||||
activeId: '',
|
||||
overId: '',
|
||||
};
|
||||
|
||||
DragContextProvider.propTypes = {
|
||||
activeId: PropTypes.string,
|
||||
overId: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default DragContextProvider;
|
||||
@@ -1,362 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
DndContext,
|
||||
closestCorners,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
sortableKeyboardCoordinates,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||
|
||||
import DragContextProvider from './DragContextProvider';
|
||||
import { COURSE_BLOCK_NAMES } from '../../constants';
|
||||
import {
|
||||
moveSubsectionOver,
|
||||
moveUnitOver,
|
||||
moveSubsection,
|
||||
moveUnit,
|
||||
dragHelpers,
|
||||
} from './utils';
|
||||
|
||||
const DraggableList = ({
|
||||
items,
|
||||
setSections,
|
||||
restoreSectionList,
|
||||
handleSectionDragAndDrop,
|
||||
handleSubsectionDragAndDrop,
|
||||
handleUnitDragAndDrop,
|
||||
children,
|
||||
}) => {
|
||||
const prevContainerInfo = React.useRef();
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
const [activeId, setActiveId] = React.useState();
|
||||
const [currentOverId, setCurrentOverId] = React.useState();
|
||||
|
||||
const findItemInfo = (id) => {
|
||||
// search id in sections
|
||||
const sectionIndex = items.findIndex((section) => section.id === id);
|
||||
if (sectionIndex !== -1) {
|
||||
return {
|
||||
index: sectionIndex,
|
||||
item: items[sectionIndex],
|
||||
category: COURSE_BLOCK_NAMES.chapter.id,
|
||||
parent: 'root',
|
||||
};
|
||||
}
|
||||
|
||||
// search id in subsections
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const section = items[index];
|
||||
const subsectionIndex = section.childInfo.children.findIndex((subsection) => subsection.id === id);
|
||||
if (subsectionIndex !== -1) {
|
||||
return {
|
||||
index: subsectionIndex,
|
||||
item: section.childInfo.children[subsectionIndex],
|
||||
category: COURSE_BLOCK_NAMES.sequential.id,
|
||||
parentIndex: index,
|
||||
parent: section,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// search id in units
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const section = items[index];
|
||||
for (let subIndex = 0; subIndex < section.childInfo.children.length; subIndex++) {
|
||||
const subsection = section.childInfo.children[subIndex];
|
||||
const unitIndex = subsection.childInfo.children.findIndex((unit) => unit.id === id);
|
||||
if (unitIndex !== -1) {
|
||||
return {
|
||||
index: unitIndex,
|
||||
item: subsection.childInfo.children[unitIndex],
|
||||
category: COURSE_BLOCK_NAMES.vertical.id,
|
||||
parentIndex: subIndex,
|
||||
parent: subsection,
|
||||
grandParentIndex: index,
|
||||
grandParent: section,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// For reasons unknown, onDragEnd is not being triggered by dnd-kit while
|
||||
// testing drag over functions. The main functions responsible to move units
|
||||
// & subsections across parents are already tested as part of move blocks by
|
||||
// index in CourseOutline.test.jsx, just these functions which determine the
|
||||
// new index and parent are ignored.
|
||||
// See https://github.com/openedx/frontend-app-course-authoring/pull/859#discussion_r1519199622
|
||||
// for more details.
|
||||
/* istanbul ignore next */
|
||||
const subsectionDragOver = (active, over, activeInfo, overInfo) => {
|
||||
if (
|
||||
activeInfo.parent.id === overInfo.parent.id
|
||||
|| activeInfo.parent.id === overInfo.item.id
|
||||
|| (activeInfo.category === overInfo.category && !overInfo.parent.actions.childAddable)
|
||||
|| (activeInfo.parent.category === overInfo.category && !overInfo.item.actions.childAddable)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Find the new index for the item
|
||||
let overSectionIndex;
|
||||
let newIndex;
|
||||
if (overInfo.category === COURSE_BLOCK_NAMES.chapter.id) {
|
||||
// We're at the root droppable of a container
|
||||
newIndex = overInfo.item.childInfo.children.length + 1;
|
||||
overSectionIndex = overInfo.index;
|
||||
setCurrentOverId(overInfo.item.id);
|
||||
} else {
|
||||
const modifier = dragHelpers.isBelowOverItem(active, over) ? 1 : 0;
|
||||
newIndex = overInfo.index >= 0 ? overInfo.index + modifier : overInfo.item.childInfo.children.length + 1;
|
||||
overSectionIndex = overInfo.parentIndex;
|
||||
setCurrentOverId(overInfo.parent.id);
|
||||
}
|
||||
|
||||
setSections((prev) => {
|
||||
const [prevCopy] = moveSubsectionOver(
|
||||
[...prev],
|
||||
activeInfo.parentIndex,
|
||||
activeInfo.index,
|
||||
overSectionIndex,
|
||||
newIndex,
|
||||
);
|
||||
return prevCopy;
|
||||
});
|
||||
if (prevContainerInfo.current === null || prevContainerInfo.current === undefined) {
|
||||
prevContainerInfo.current = activeInfo.parent.id;
|
||||
}
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
const unitDragOver = (active, over, activeInfo, overInfo) => {
|
||||
if (
|
||||
activeInfo.parent.id === overInfo.parent.id
|
||||
|| activeInfo.parent.id === overInfo.item.id
|
||||
|| (activeInfo.category === overInfo.category && !overInfo.parent.actions.childAddable)
|
||||
|| (activeInfo.parent.category === overInfo.category && !overInfo.item.actions.childAddable)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let overSubsectionIndex;
|
||||
let overSectionIndex;
|
||||
// Find the indexes for the items
|
||||
let newIndex;
|
||||
if (overInfo.category === COURSE_BLOCK_NAMES.sequential.id) {
|
||||
// We're at the root droppable of a container
|
||||
newIndex = overInfo.item.childInfo.children.length + 1;
|
||||
overSubsectionIndex = overInfo.index;
|
||||
overSectionIndex = overInfo.parentIndex;
|
||||
setCurrentOverId(overInfo.item.id);
|
||||
} else {
|
||||
const modifier = dragHelpers.isBelowOverItem(active, over) ? 1 : 0;
|
||||
newIndex = overInfo.index >= 0 ? overInfo.index + modifier : overInfo.item.childInfo.children.length + 1;
|
||||
overSubsectionIndex = overInfo.parentIndex;
|
||||
overSectionIndex = overInfo.grandParentIndex;
|
||||
setCurrentOverId(overInfo.parent.id);
|
||||
}
|
||||
|
||||
setSections((prev) => {
|
||||
const [prevCopy] = moveUnitOver(
|
||||
[...prev],
|
||||
activeInfo.grandParentIndex,
|
||||
activeInfo.parentIndex,
|
||||
activeInfo.index,
|
||||
overSectionIndex,
|
||||
overSubsectionIndex,
|
||||
newIndex,
|
||||
);
|
||||
return prevCopy;
|
||||
});
|
||||
if (prevContainerInfo.current === null || prevContainerInfo.current === undefined) {
|
||||
prevContainerInfo.current = activeInfo.grandParent.id;
|
||||
}
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
const handleDragOver = (event) => {
|
||||
const { active, over } = event;
|
||||
if (!active || !over) {
|
||||
return;
|
||||
}
|
||||
const { id } = active;
|
||||
const { id: overId } = over;
|
||||
|
||||
// Find the containers
|
||||
const activeInfo = findItemInfo(id);
|
||||
const overInfo = findItemInfo(overId);
|
||||
if (!activeInfo || !overInfo) {
|
||||
return;
|
||||
}
|
||||
switch (activeInfo.category) {
|
||||
case COURSE_BLOCK_NAMES.sequential.id:
|
||||
subsectionDragOver(active, over, activeInfo, overInfo);
|
||||
break;
|
||||
case COURSE_BLOCK_NAMES.vertical.id:
|
||||
unitDragOver(active, over, activeInfo, overInfo);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
const { active, over } = event;
|
||||
if (!active || !over) {
|
||||
return;
|
||||
}
|
||||
setActiveId(null);
|
||||
setCurrentOverId(null);
|
||||
const { id } = active;
|
||||
const { id: overId } = over;
|
||||
|
||||
const activeInfo = findItemInfo(id);
|
||||
const overInfo = findItemInfo(overId);
|
||||
if (!activeInfo || !overInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
activeInfo.category !== overInfo.category
|
||||
|| (activeInfo.parent !== 'root' && activeInfo.parentIndex !== overInfo.parentIndex)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeInfo.index !== overInfo.index || prevContainerInfo.current) {
|
||||
switch (activeInfo.category) {
|
||||
case COURSE_BLOCK_NAMES.chapter.id:
|
||||
setSections((prev) => {
|
||||
const result = arrayMove(prev, activeInfo.index, overInfo.index);
|
||||
handleSectionDragAndDrop(result.map(section => section.id), restoreSectionList);
|
||||
return result;
|
||||
});
|
||||
break;
|
||||
case COURSE_BLOCK_NAMES.sequential.id:
|
||||
setSections((prev) => {
|
||||
const [prevCopy, result] = moveSubsection(
|
||||
[...prev],
|
||||
activeInfo.parentIndex,
|
||||
activeInfo.index,
|
||||
overInfo.index,
|
||||
);
|
||||
handleSubsectionDragAndDrop(
|
||||
activeInfo.parent.id,
|
||||
prevContainerInfo.current,
|
||||
result.map(subsection => subsection.id),
|
||||
restoreSectionList,
|
||||
);
|
||||
return prevCopy;
|
||||
});
|
||||
break;
|
||||
case COURSE_BLOCK_NAMES.vertical.id:
|
||||
setSections((prev) => {
|
||||
const [prevCopy, result] = moveUnit(
|
||||
[...prev],
|
||||
activeInfo.grandParentIndex,
|
||||
activeInfo.parentIndex,
|
||||
activeInfo.index,
|
||||
overInfo.index,
|
||||
);
|
||||
handleUnitDragAndDrop(
|
||||
activeInfo.grandParent.id,
|
||||
prevContainerInfo.current,
|
||||
activeInfo.parent.id,
|
||||
result.map(unit => unit.id),
|
||||
restoreSectionList,
|
||||
);
|
||||
return prevCopy;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
prevContainerInfo.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragStart = (event) => {
|
||||
const { active } = event;
|
||||
const { id } = active;
|
||||
|
||||
setActiveId(id);
|
||||
};
|
||||
|
||||
const customClosestCorners = ({
|
||||
active, droppableContainers, droppableRects, ...args
|
||||
}) => {
|
||||
const activeCategory = active.data?.current?.category;
|
||||
const filteredContainers = droppableContainers.filter(
|
||||
(container) => {
|
||||
switch (activeCategory) {
|
||||
case COURSE_BLOCK_NAMES.chapter.id:
|
||||
return container.data?.current?.category === activeCategory;
|
||||
case COURSE_BLOCK_NAMES.sequential.id:
|
||||
return [activeCategory, COURSE_BLOCK_NAMES.chapter.id].includes(container.data?.current?.category);
|
||||
case COURSE_BLOCK_NAMES.vertical.id:
|
||||
return [activeCategory, COURSE_BLOCK_NAMES.sequential.id].includes(container.data?.current?.category);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
);
|
||||
return closestCorners({
|
||||
active, droppableContainers: filteredContainers, droppableRects, ...args,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
sensors={sensors}
|
||||
collisionDetection={customClosestCorners}
|
||||
onDragOver={handleDragOver}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<DragContextProvider activeId={activeId} overId={currentOverId}>
|
||||
{children}
|
||||
</DragContextProvider>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
DraggableList.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
childInfo: PropTypes.shape({
|
||||
children: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
childInfo: PropTypes.shape({
|
||||
children: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
}).isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
}).isRequired,
|
||||
})).isRequired,
|
||||
setSections: PropTypes.func.isRequired,
|
||||
restoreSectionList: PropTypes.func.isRequired,
|
||||
handleSectionDragAndDrop: PropTypes.func.isRequired,
|
||||
handleSubsectionDragAndDrop: PropTypes.func.isRequired,
|
||||
handleUnitDragAndDrop: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default DraggableList;
|
||||
@@ -1,5 +0,0 @@
|
||||
.extend-margin {
|
||||
.item-children {
|
||||
margin-right: -2.75rem;
|
||||
}
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
|
||||
export const dragHelpers = {
|
||||
copyBlockChildren: (block) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
block.childInfo = { ...block.childInfo };
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
block.childInfo.children = [...block.childInfo.children];
|
||||
return block;
|
||||
},
|
||||
setBlockChildren: (block, children) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
block.childInfo.children = children;
|
||||
return block;
|
||||
},
|
||||
setBlockChild: (block, child, id) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
block.childInfo.children[id] = child;
|
||||
return block;
|
||||
},
|
||||
insertChild: (block, child, index) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
block.childInfo.children = [
|
||||
...block.childInfo.children.slice(0, index),
|
||||
child,
|
||||
...block.childInfo.children.slice(index, block.childInfo.children.length),
|
||||
];
|
||||
return block;
|
||||
},
|
||||
isBelowOverItem: (active, over) => over
|
||||
&& active.rect.current.translated
|
||||
&& active.rect.current.translated.top
|
||||
> over.rect.top + over.rect.height,
|
||||
};
|
||||
|
||||
export const moveSubsectionOver = (
|
||||
prevCopy,
|
||||
activeSectionIdx,
|
||||
activeSubsectionIdx,
|
||||
overSectionIdx,
|
||||
newIndex,
|
||||
) => {
|
||||
let activeSection = dragHelpers.copyBlockChildren({ ...prevCopy[activeSectionIdx] });
|
||||
let overSection = dragHelpers.copyBlockChildren({ ...prevCopy[overSectionIdx] });
|
||||
const subsection = activeSection.childInfo.children[activeSubsectionIdx];
|
||||
|
||||
overSection = dragHelpers.insertChild(overSection, subsection, newIndex);
|
||||
|
||||
activeSection = dragHelpers.setBlockChildren(
|
||||
activeSection,
|
||||
activeSection.childInfo.children.filter((item) => item.id !== subsection.id),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
prevCopy[activeSectionIdx] = activeSection;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
prevCopy[overSectionIdx] = overSection;
|
||||
return [prevCopy, overSection.childInfo.children];
|
||||
};
|
||||
|
||||
export const moveUnitOver = (
|
||||
prevCopy,
|
||||
activeSectionIdx,
|
||||
activeSubsectionIdx,
|
||||
activeUnitIdx,
|
||||
overSectionIdx,
|
||||
overSubsectionIdx,
|
||||
newIndex,
|
||||
) => {
|
||||
const activeSection = dragHelpers.copyBlockChildren({ ...prevCopy[activeSectionIdx] });
|
||||
let activeSubsection = dragHelpers.copyBlockChildren(
|
||||
{ ...activeSection.childInfo.children[activeSubsectionIdx] },
|
||||
);
|
||||
|
||||
let overSection = { ...prevCopy[overSectionIdx] };
|
||||
if (overSection.id === activeSection.id) {
|
||||
overSection = activeSection;
|
||||
}
|
||||
|
||||
overSection = dragHelpers.copyBlockChildren(overSection);
|
||||
let overSubsection = dragHelpers.copyBlockChildren(
|
||||
{ ...overSection.childInfo.children[overSubsectionIdx] },
|
||||
);
|
||||
|
||||
const unit = activeSubsection.childInfo.children[activeUnitIdx];
|
||||
overSubsection = dragHelpers.insertChild(overSubsection, unit, newIndex);
|
||||
overSection = dragHelpers.setBlockChild(overSection, overSubsection, overSubsectionIdx);
|
||||
|
||||
activeSubsection = dragHelpers.setBlockChildren(
|
||||
activeSubsection,
|
||||
activeSubsection.childInfo.children.filter((item) => item.id !== unit.id),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
prevCopy[activeSectionIdx] = dragHelpers.setBlockChild(activeSection, activeSubsection, activeSubsectionIdx);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
prevCopy[overSectionIdx] = overSection;
|
||||
return [prevCopy, overSubsection.childInfo.children];
|
||||
};
|
||||
|
||||
export const moveSubsection = (
|
||||
prevCopy,
|
||||
sectionIdx,
|
||||
currentIdx,
|
||||
newIdx,
|
||||
) => {
|
||||
let section = dragHelpers.copyBlockChildren({ ...prevCopy[sectionIdx] });
|
||||
|
||||
const result = arrayMove(section.childInfo.children, currentIdx, newIdx);
|
||||
section = dragHelpers.setBlockChildren(section, result);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
prevCopy[sectionIdx] = section;
|
||||
return [prevCopy, result];
|
||||
};
|
||||
|
||||
export const moveUnit = (
|
||||
prevCopy,
|
||||
sectionIdx,
|
||||
subsectionIdx,
|
||||
currentIdx,
|
||||
newIdx,
|
||||
) => {
|
||||
let section = dragHelpers.copyBlockChildren({ ...prevCopy[sectionIdx] });
|
||||
let subsection = dragHelpers.copyBlockChildren({ ...section.childInfo.children[subsectionIdx] });
|
||||
|
||||
const result = arrayMove(subsection.childInfo.children, currentIdx, newIdx);
|
||||
subsection = dragHelpers.setBlockChildren(subsection, result);
|
||||
section = dragHelpers.setBlockChild(section, subsection, subsectionIdx);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
prevCopy[sectionIdx] = section;
|
||||
return [prevCopy, result];
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if section can be moved by given step.
|
||||
* Inner function returns false if the new index after moving by given step
|
||||
* is out of bounds of item length.
|
||||
* If it is within bounds, returns draggable flag of the item in the new index.
|
||||
* This helps us avoid moving the item to a position of unmovable item.
|
||||
* @param {Array} items
|
||||
* @returns {(id, step) => bool}
|
||||
*/
|
||||
export const canMoveSection = (sections) => (id, step) => {
|
||||
const newId = id + step;
|
||||
const indexCheck = newId >= 0 && newId < sections.length;
|
||||
if (!indexCheck) {
|
||||
return false;
|
||||
}
|
||||
const newItem = sections[newId];
|
||||
return newItem.actions.draggable;
|
||||
};
|
||||
|
||||
export const possibleSubsectionMoves = (sections, sectionIndex, section, subsections) => (index, step) => {
|
||||
if (!subsections[index]?.actions?.draggable) {
|
||||
return {};
|
||||
}
|
||||
if ((step === -1 && index >= 1) || (step === 1 && subsections.length - index >= 2)) {
|
||||
// move subsection inside its own parent section
|
||||
return {
|
||||
fn: moveSubsection,
|
||||
args: [
|
||||
sections,
|
||||
sectionIndex,
|
||||
index,
|
||||
index + step,
|
||||
],
|
||||
sectionId: section.id,
|
||||
};
|
||||
} if (step === -1 && index === 0 && sectionIndex > 0) {
|
||||
// move subsection to last position of previous section
|
||||
if (!sections[sectionIndex + step]?.actions?.childAddable) {
|
||||
// return if previous section doesn't allow adding subsections
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
fn: moveSubsectionOver,
|
||||
args: [
|
||||
sections,
|
||||
sectionIndex,
|
||||
index,
|
||||
sectionIndex + step,
|
||||
sections[sectionIndex + step].childInfo.children.length + 1,
|
||||
],
|
||||
sectionId: sections[sectionIndex + step].id,
|
||||
};
|
||||
} if (step === 1 && index === subsections.length - 1 && sectionIndex < sections.length - 1) {
|
||||
// move subsection to first position of next section
|
||||
if (!sections[sectionIndex + step]?.actions?.childAddable) {
|
||||
// return if next section doesn't allow adding subsections
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
fn: moveSubsectionOver,
|
||||
args: [
|
||||
sections,
|
||||
sectionIndex,
|
||||
index,
|
||||
sectionIndex + step,
|
||||
0,
|
||||
],
|
||||
sectionId: sections[sectionIndex + step].id,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const possibleUnitMoves = (
|
||||
sections,
|
||||
sectionIndex,
|
||||
subsectionIndex,
|
||||
section,
|
||||
subsection,
|
||||
units,
|
||||
) => (index, step) => {
|
||||
if (!units[index].actions.draggable) {
|
||||
return {};
|
||||
}
|
||||
if ((step === -1 && index >= 1) || (step === 1 && units.length - index >= 2)) {
|
||||
return {
|
||||
fn: moveUnit,
|
||||
args: [
|
||||
sections,
|
||||
sectionIndex,
|
||||
subsectionIndex,
|
||||
index,
|
||||
index + step,
|
||||
],
|
||||
sectionId: section.id,
|
||||
subsectionId: subsection.id,
|
||||
};
|
||||
} if (step === -1 && index === 0) {
|
||||
if (subsectionIndex > 0) {
|
||||
// move unit to last position of previous subsection inside same section.
|
||||
if (!sections[sectionIndex].childInfo.children[subsectionIndex + step]?.actions?.childAddable) {
|
||||
// return if previous subsection doesn't allow adding subsections
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
fn: moveUnitOver,
|
||||
args: [
|
||||
sections,
|
||||
sectionIndex,
|
||||
subsectionIndex,
|
||||
index,
|
||||
sectionIndex,
|
||||
subsectionIndex + step,
|
||||
sections[sectionIndex].childInfo.children[subsectionIndex + step].childInfo.children.length + 1,
|
||||
],
|
||||
sectionId: section.id,
|
||||
subsectionId: sections[sectionIndex].childInfo.children[subsectionIndex + step].id,
|
||||
};
|
||||
} if (sectionIndex > 0) {
|
||||
// move unit to last position of previous subsection inside previous section.
|
||||
const newSectionIndex = sectionIndex + step;
|
||||
if (sections[newSectionIndex].childInfo.children.length === 0) {
|
||||
// return if previous section has no subsections.
|
||||
return {};
|
||||
}
|
||||
const newSubsectionIndex = sections[newSectionIndex].childInfo.children.length - 1;
|
||||
if (!sections[newSectionIndex].childInfo.children[newSubsectionIndex]?.actions?.childAddable) {
|
||||
// return if previous subsection doesn't allow adding subsections
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
fn: moveUnitOver,
|
||||
args: [
|
||||
sections,
|
||||
sectionIndex,
|
||||
subsectionIndex,
|
||||
index,
|
||||
newSectionIndex,
|
||||
newSubsectionIndex,
|
||||
sections[newSectionIndex].childInfo.children[newSubsectionIndex].childInfo.children.length + 1,
|
||||
],
|
||||
sectionId: sections[newSectionIndex].id,
|
||||
subsectionId: sections[newSectionIndex].childInfo.children[newSubsectionIndex].id,
|
||||
};
|
||||
}
|
||||
} else if (step === 1 && index === units.length - 1) {
|
||||
if (subsectionIndex < sections[sectionIndex].childInfo.children.length - 1) {
|
||||
// move unit to first position of next subsection inside same section.
|
||||
if (!sections[sectionIndex].childInfo.children[subsectionIndex + step]?.actions?.childAddable) {
|
||||
// return if next subsection doesn't allow adding subsections
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
fn: moveUnitOver,
|
||||
args: [
|
||||
sections,
|
||||
sectionIndex,
|
||||
subsectionIndex,
|
||||
index,
|
||||
sectionIndex,
|
||||
subsectionIndex + step,
|
||||
0,
|
||||
],
|
||||
sectionId: section.id,
|
||||
subsectionId: sections[sectionIndex].childInfo.children[subsectionIndex + step].id,
|
||||
};
|
||||
} if (sectionIndex < sections.length - 1) {
|
||||
// move unit to first position of next subsection inside next section.
|
||||
const newSectionIndex = sectionIndex + step;
|
||||
if (sections[newSectionIndex].childInfo.children.length === 0) {
|
||||
// return if next section has no subsections.
|
||||
return {};
|
||||
}
|
||||
const newSubsectionIndex = 0;
|
||||
if (!sections[newSectionIndex].childInfo.children[newSubsectionIndex]?.actions?.childAddable) {
|
||||
// return if next subsection doesn't allow adding subsections
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
fn: moveUnitOver,
|
||||
args: [
|
||||
sections,
|
||||
sectionIndex,
|
||||
subsectionIndex,
|
||||
index,
|
||||
newSectionIndex,
|
||||
newSubsectionIndex,
|
||||
0,
|
||||
],
|
||||
sectionId: sections[newSectionIndex].id,
|
||||
subsectionId: sections[newSectionIndex].childInfo.children[newSubsectionIndex].id,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
@@ -11,6 +11,5 @@
|
||||
@import "./tag-count/TagCount";
|
||||
@import "./modal-dropzone/ModalDropzone";
|
||||
@import "./configure-modal/ConfigureModal";
|
||||
@import "./drag-helper/SortableItem";
|
||||
@import "./block-type-utils";
|
||||
@import "./modal-iframe"
|
||||
|
||||
Reference in New Issue
Block a user