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;
|
||||
100
src/generic/DraggableList/SortableItem.jsx
Normal file
100
src/generic/DraggableList/SortableItem.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { intlShape, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import {
|
||||
ActionRow, Card, Icon, IconButtonWithTooltip,
|
||||
} from '@openedx/paragon';
|
||||
import { DragIndicator } from '@openedx/paragon/icons';
|
||||
import messages from './messages';
|
||||
|
||||
const SortableItem = ({
|
||||
id,
|
||||
componentStyle,
|
||||
actions,
|
||||
actionStyle,
|
||||
children,
|
||||
isClickable,
|
||||
onClick,
|
||||
disabled,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
setActivatorNodeRef,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id,
|
||||
animateLayoutChanges: () => false,
|
||||
disabled: {
|
||||
draggable: disabled,
|
||||
},
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
zIndex: isDragging ? 200 : undefined,
|
||||
transition,
|
||||
...componentStyle,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
>
|
||||
<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}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
SortableItem.defaultProps = {
|
||||
componentStyle: null,
|
||||
actions: null,
|
||||
actionStyle: null,
|
||||
isClickable: false,
|
||||
onClick: null,
|
||||
disabled: false,
|
||||
};
|
||||
SortableItem.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
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,
|
||||
};
|
||||
|
||||
export default injectIntl(SortableItem);
|
||||
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;
|
||||
11
src/generic/DraggableList/messages.js
Normal file
11
src/generic/DraggableList/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
tooltipContent: {
|
||||
id: 'authoring.draggableList.tooltip.content',
|
||||
defaultMessage: 'Drag to reorder',
|
||||
description: 'Tooltip content for drag indicator icon',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
Reference in New Issue
Block a user