@plone/volto
Version:
Volto
371 lines (332 loc) • 10.7 kB
JSX
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import find from 'lodash/find';
import min from 'lodash/min';
import { flattenTree, getProjection, removeChildrenOf } from './utilities';
import SortableItem from './SortableItem';
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
export function Order({
items = [],
onMoveBlock,
onDeleteBlock,
onSelectBlock,
indentationWidth = 25,
removable,
dndKitCore,
dndKitSortable,
dndKitUtilities,
errors,
}) {
const [activeId, setActiveId] = useState(null);
const [overId, setOverId] = useState(null);
const [offsetLeft, setOffsetLeft] = useState(0);
const [currentPosition, setCurrentPosition] = useState(null);
const {
DndContext,
closestCenter,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
MeasuringStrategy,
defaultDropAnimation,
} = dndKitCore;
const { SortableContext, arrayMove, verticalListSortingStrategy } =
dndKitSortable;
const { CSS } = dndKitUtilities;
const measuring = {
droppable: {
strategy: MeasuringStrategy.Always,
},
};
const dropAnimationConfig = {
keyframes({ transform }) {
return [
{ opacity: 1, transform: CSS.Transform.toString(transform.initial) },
{
opacity: 0,
transform: CSS.Transform.toString({
...transform.final,
x: transform.final.x + 5,
y: transform.final.y + 5,
}),
},
];
},
easing: 'ease-out',
sideEffects({ active }) {
active.node.animate([{ opacity: 0 }, { opacity: 1 }], {
duration: defaultDropAnimation.duration,
easing: defaultDropAnimation.easing,
});
},
};
const flattenedItems = useMemo(
() => removeChildrenOf(flattenTree(items), activeId ? [activeId] : []),
[activeId, items],
);
const projected =
activeId && overId
? getProjection(
flattenedItems,
activeId,
overId,
offsetLeft,
indentationWidth,
arrayMove,
)
: null;
const sensorContext = useRef({
items: flattenedItems,
offset: offsetLeft,
});
const sensors = useSensors(useSensor(PointerSensor));
const sortedIds = useMemo(
() => flattenedItems.map(({ id }) => id),
[flattenedItems],
);
const activeItem = activeId
? flattenedItems.find(({ id }) => id === activeId)
: null;
useEffect(() => {
sensorContext.current = {
items: flattenedItems,
offset: offsetLeft,
};
}, [flattenedItems, offsetLeft]);
const announcements = {
onDragStart({ active }) {
return `Picked up ${active.id}.`;
},
onDragMove({ active, over }) {
return getMovementAnnouncement('onDragMove', active.id, over?.id);
},
onDragOver({ active, over }) {
return getMovementAnnouncement('onDragOver', active.id, over?.id);
},
onDragEnd({ active, over }) {
return getMovementAnnouncement('onDragEnd', active.id, over?.id);
},
onDragCancel({ active }) {
return `Moving was cancelled. ${active.id} was dropped in its original position.`;
},
};
return (
<DndContext
accessibility={{ announcements }}
sensors={sensors}
collisionDetection={closestCenter}
measuring={measuring}
onDragStart={handleDragStart}
onDragMove={handleDragMove}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext items={sortedIds} strategy={verticalListSortingStrategy}>
{flattenedItems.map(({ id, parentId, depth, data }) => (
<SortableItem
key={id}
id={id}
parentId={parentId}
data={data}
depth={min([
id === activeId && projected ? projected.depth : depth,
1,
])}
indentationWidth={indentationWidth}
onRemove={removable ? () => handleRemove(id) : undefined}
onSelectBlock={onSelectBlock}
errors={errors?.[id] || {}}
/>
))}
{createPortal(
<DragOverlay dropAnimation={dropAnimationConfig}>
{activeId && activeItem ? (
<SortableItem
id={activeId}
depth={activeItem.depth}
clone
data={find(flattenedItems, { id: activeId }).data}
indentationWidth={indentationWidth}
/>
) : null}
</DragOverlay>,
document.body,
)}
</SortableContext>
</DndContext>
);
function handleDragStart({ active: { id: activeId } }) {
setActiveId(activeId);
setOverId(activeId);
const activeItem = flattenedItems.find(({ id }) => id === activeId);
if (activeItem) {
setCurrentPosition({
parentId: activeItem.parentId,
overId: activeId,
});
}
document.body.style.setProperty('cursor', 'grabbing');
}
function handleDragMove({ delta }) {
setOffsetLeft(delta.x);
}
function handleDragOver({ over }) {
setOverId(over?.id ?? null);
}
function handleDragEnd({ active, over }) {
if (projected && over) {
const { depth, parentId } = projected;
const clonedItems = JSON.parse(JSON.stringify(flattenedItems));
const overIndex = clonedItems.findIndex(({ id }) => id === over.id);
const activeIndex = clonedItems.findIndex(({ id }) => id === active.id);
const activeTreeItem = clonedItems[activeIndex];
const oldParentId = activeTreeItem.parentId;
clonedItems[activeIndex] = { ...activeTreeItem, depth, parentId };
// Translate position depending on parent
if (parentId === oldParentId) {
// Move from and to toplevel or move within the same grid block
let destIndex = clonedItems[overIndex].index;
if (clonedItems[overIndex].depth > clonedItems[activeIndex].depth) {
destIndex = find(clonedItems, {
id: clonedItems[overIndex].parentId,
}).index;
}
onMoveBlock({
source: {
position: clonedItems[activeIndex].index,
parent: oldParentId,
id: active.id,
},
destination: {
position: destIndex,
parent: parentId,
},
});
} else if (parentId && oldParentId) {
// Move from one gridblock to another
onMoveBlock({
source: {
position: clonedItems[activeIndex].index,
parent: oldParentId,
id: active.id,
},
destination: {
position:
overIndex < activeIndex
? clonedItems[overIndex - 1].parentId
? clonedItems[overIndex - 1].index + 1
: clonedItems[overIndex].index
: overIndex + 1 < clonedItems.length
? clonedItems[overIndex + 1].index
: clonedItems[overIndex].index + 1,
parent: parentId,
},
});
} else if (oldParentId) {
// Moving to the main container from a gridblock
onMoveBlock({
source: {
position: clonedItems[activeIndex].index,
parent: oldParentId,
id: active.id,
},
destination: {
position:
overIndex > activeIndex
? overIndex + 1 < clonedItems.length
? clonedItems[overIndex + 1].index
: clonedItems[overIndex].index + 1
: clonedItems[overIndex].index,
parent: parentId,
},
});
} else {
// Moving from the main container to a gridblock
onMoveBlock({
source: {
position: clonedItems[activeIndex].index,
parent: oldParentId,
id: active.id,
},
destination: {
position:
overIndex < activeIndex
? clonedItems[overIndex - 1].parentId
? clonedItems[overIndex - 1].index + 1
: clonedItems[overIndex].index
: overIndex + 1 < clonedItems.length
? clonedItems[overIndex + 1].index
: clonedItems[overIndex].index + 1,
parent: parentId,
},
});
}
}
resetState();
}
function handleDragCancel() {
resetState();
}
function resetState() {
setOverId(null);
setActiveId(null);
setOffsetLeft(0);
setCurrentPosition(null);
document.body.style.setProperty('cursor', '');
}
function handleRemove(id) {
onDeleteBlock(id);
}
function getMovementAnnouncement(eventName, activeId, overId) {
if (overId && projected) {
if (eventName !== 'onDragEnd') {
if (
currentPosition &&
projected.parentId === currentPosition.parentId &&
overId === currentPosition.overId
) {
return;
} else {
setCurrentPosition({
parentId: projected.parentId,
overId,
});
}
}
const clonedItems = JSON.parse(JSON.stringify(flattenTree(items)));
const overIndex = clonedItems.findIndex(({ id }) => id === overId);
const activeIndex = clonedItems.findIndex(({ id }) => id === activeId);
const sortedItems = arrayMove(clonedItems, activeIndex, overIndex);
const previousItem = sortedItems[overIndex - 1];
let announcement;
const movedVerb = eventName === 'onDragEnd' ? 'dropped' : 'moved';
const nestedVerb = eventName === 'onDragEnd' ? 'dropped' : 'nested';
if (!previousItem) {
const nextItem = sortedItems[overIndex + 1];
announcement = `${activeId} was ${movedVerb} before ${nextItem.id}.`;
} else {
if (projected.depth > previousItem.depth) {
announcement = `${activeId} was ${nestedVerb} under ${previousItem.id}.`;
} else {
let previousSibling = previousItem;
while (previousSibling && projected.depth < previousSibling.depth) {
const parentId = previousSibling.parentId;
previousSibling = sortedItems.find(({ id }) => id === parentId);
}
if (previousSibling) {
announcement = `${activeId} was ${movedVerb} after ${previousSibling.id}.`;
}
}
}
return announcement;
}
return;
}
}
export default injectLazyLibs([
'dndKitCore',
'dndKitSortable',
'dndKitUtilities',
])(Order);