UNPKG

@plone/volto

Version:
371 lines (332 loc) 10.7 kB
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);