UNPKG

@neo4j-ndl/react

Version:

React implementation of Neo4j Design System

304 lines 15.5 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { closestCenter, defaultDropAnimation, DndContext, DragOverlay, KeyboardSensor, MeasuringStrategy, PointerSensor, useSensor, useSensors, } from '@dnd-kit/core'; import { arrayMove, SortableContext, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { useCallback, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { sortableTreeKeyboardCoordinates } from './tree-view-keyboard-coordinates'; import { buildTree, flattenTree, getProjection, removeChildrenOf, transformToString, } from './tree-view-utils'; import { TreeItemWrapper } from './TreeItemWrapper'; import { SortableTreeViewItem } from './TreeViewItem'; import { TreeViewTextItem } from './TreeViewTextItem'; const indentationWidth = 20; const ACTIVE_PLACEHOLDER_ID = 'ndl-active-tree-item'; const dropAnimationConfig = { keyframes({ transform }) { return [ { opacity: 1, transform: transformToString(transform.initial), }, { opacity: 0, transform: transformToString(Object.assign(Object.assign({}, transform.final), { y: transform.final.y + 5, x: transform.final.x + 5 })), }, ]; }, easing: 'ease-out', sideEffects({ active }) { active.node.animate([{ opacity: 0 }, { opacity: 1 }], { duration: defaultDropAnimation.duration, easing: defaultDropAnimation.easing, }); }, }; const TreeViewComponent = function TreeView({ items, TreeItemComponent, onItemsChanged, htmlAttributes, }) { var _a; const flattenItems = useMemo(() => flattenTree(items), [items]); const [activeId, setActiveId] = useState(null); const [overId, setOverId] = useState(null); const [focusedRowId, setFocusedRowId] = useState(null); const [currentPosition, setCurrentPosition] = useState(null); const [offsetLeft, setOffsetLeft] = useState(0); /* ------------------------------ Setup of Data ------------------------------ */ const flattenedAndRemovedCollapsedItems = useMemo(() => { const collapsedItemsIds = flattenItems .filter((item) => item.canHaveSubItems && item.isCollapsed) .map((item) => item.id); return removeChildrenOf([...flattenItems], activeId !== null ? [activeId, ...collapsedItemsIds] : collapsedItemsIds); }, [activeId, flattenItems]); const activeItem = useMemo(() => (activeId ? flattenItems.find(({ id }) => id === activeId) : null), [activeId, flattenItems]); const flattenedAndRemovedCollapsedItemsWithActive = useMemo(() => { const collapsedItems = flattenItems.reduce((acc, item) => { if (item.canHaveSubItems && item.isCollapsed) { return [...acc, item.id]; } return acc; }, []); const ItemsWithCollapsedRemoved = removeChildrenOf(flattenItems, activeId ? [activeId, ...collapsedItems] : collapsedItems); const newArray = []; for (const item of ItemsWithCollapsedRemoved) { newArray.push(item); if (item.id === activeId) { newArray.push(Object.assign(Object.assign({}, item), { id: ACTIVE_PLACEHOLDER_ID })); } } return newArray; }, [flattenItems, activeId]); if ((activeItem === null || activeItem === void 0 ? void 0 : activeItem.canHaveSubItems) && !(activeItem === null || activeItem === void 0 ? void 0 : activeItem.isCollapsed)) { flattenedAndRemovedCollapsedItems.push(Object.assign(Object.assign({}, activeItem), { id: ACTIVE_PLACEHOLDER_ID })); } const sensorContext = useRef({ items: flattenedAndRemovedCollapsedItemsWithActive, offset: offsetLeft, }); const coordinateGetter = useMemo(() => { sensorContext.current.items = flattenedAndRemovedCollapsedItemsWithActive; sensorContext.current.offset = offsetLeft; return sortableTreeKeyboardCoordinates(sensorContext, true, indentationWidth); }, [flattenedAndRemovedCollapsedItemsWithActive, offsetLeft]); const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: coordinateGetter, })); const projected = useMemo(() => activeId && overId ? getProjection(flattenedAndRemovedCollapsedItemsWithActive, activeId, overId, offsetLeft, indentationWidth) : null, [activeId, overId, flattenedAndRemovedCollapsedItemsWithActive, offsetLeft]); /* ------------------------------ Handlers ------------------------------ */ function handleDragStart({ active }) { const activeId = active.id; setActiveId(activeId); setOverId(activeId); const activeItem = flattenItems.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 }) { var _a; setOverId((_a = over === null || over === void 0 ? void 0 : over.id) !== null && _a !== void 0 ? _a : null); } function resetState() { setOverId(null); setActiveId(null); setOffsetLeft(0); setCurrentPosition(null); document.body.style.setProperty('cursor', ''); } const handleDragEnd = useCallback((event) => { const { active, over } = event; resetState(); let overIndex = flattenItems.findIndex((item) => item.id === (over === null || over === void 0 ? void 0 : over.id)); if ((over === null || over === void 0 ? void 0 : over.id) === ACTIVE_PLACEHOLDER_ID) { overIndex = flattenItems.findIndex((item) => item.id === active.id); } if (projected && over) { const { depth, parentId } = projected; const clonedItems = [...flattenItems]; const parentIndex = clonedItems.findIndex((item) => item.id === parentId); const activeIndex = clonedItems.findIndex(({ id }) => id === active.id); const activeTreeItem = clonedItems[activeIndex]; clonedItems[activeIndex] = Object.assign(Object.assign({}, activeTreeItem), { depth, parentId }); const parentItem = clonedItems[parentIndex]; if (parentItem && parentItem.canHaveSubItems) { clonedItems[parentIndex] = Object.assign(Object.assign({}, parentItem), { isCollapsed: false }); } const newItems = buildTree(arrayMove(clonedItems, activeIndex, overIndex)); onItemsChanged(newItems, { reason: 'dropped', item: activeTreeItem, }); } }, [flattenItems, onItemsChanged, projected]); function handleToggleCollapse(id) { const item = flattenItems.find((item) => item.id === id); if (!item || item.canHaveSubItems !== true) { return; } onItemsChanged(buildTree(flattenItems.map((item) => { if (item.id === id && item.canHaveSubItems === true) { return Object.assign(Object.assign({}, item), { isCollapsed: !item.isCollapsed }); } return item; })), { reason: item.isCollapsed ? 'expanded' : 'collapsed', item, }); } function handleDragCancel() { resetState(); } /* ------------------------------ Keyboard functions ------------------------------ */ const 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 = [ ...flattenedAndRemovedCollapsedItemsWithActive, ]; 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; }; const announcements = { onDragStart({ active }) { return `Picked up ${active.id}.`; }, onDragMove({ active, over }) { return getMovementAnnouncement('onDragMove', active.id, over === null || over === void 0 ? void 0 : over.id); }, onDragOver({ active, over }) { return getMovementAnnouncement('onDragOver', active.id, over === null || over === void 0 ? void 0 : over.id); }, onDragEnd({ active, over }) { return getMovementAnnouncement('onDragEnd', active.id, over === null || over === void 0 ? void 0 : over.id); }, onDragCancel({ active }) { return `Moving was cancelled. ${active.id} was dropped in its original position.`; }, }; /* ------------------------------ Visual ------------------------------ */ function getTrailList() { const trails = {}; // Loop through the items in reverse [...flattenedAndRemovedCollapsedItemsWithActive] .reverse() .forEach((item, itemIndex, reversedList) => { const trailList = []; const nextItem = reversedList[itemIndex - 1]; // Loop through the depth of the item for (let trailDepth = 0; trailDepth < item.depth; trailDepth++) { // Not closest to the item so should be straight or none if (trailDepth < item.depth - 1) { if (nextItem === undefined) { trailList.push('none'); continue; } else if (trails[nextItem === null || nextItem === void 0 ? void 0 : nextItem.id][trailDepth] === 'none') { trailList.push('none'); continue; } else if (nextItem && nextItem.depth <= trailDepth) { trailList.push('none'); continue; } trailList.push('straight'); } else { if ((nextItem && trails[nextItem === null || nextItem === void 0 ? void 0 : nextItem.id][trailDepth] === 'none') || nextItem === undefined || nextItem.depth <= trailDepth) { trailList.push('curved'); } else { trailList.push('straight-curved'); } } } trails[item.id] = trailList; }); return trails; } const measuring = { droppable: { strategy: MeasuringStrategy.Always, }, }; const trails = getTrailList(); const rowIdWithFocus = focusedRowId !== null && focusedRowId !== void 0 ? focusedRowId : (_a = flattenedAndRemovedCollapsedItemsWithActive[0]) === null || _a === void 0 ? void 0 : _a.id; return (_jsx(DndContext, { accessibility: { announcements }, sensors: sensors, collisionDetection: closestCenter, onDragEnd: handleDragEnd, onDragStart: handleDragStart, onDragMove: handleDragMove, onDragOver: handleDragOver, onDragCancel: handleDragCancel, measuring: measuring, children: _jsxs(SortableContext, { items: flattenedAndRemovedCollapsedItemsWithActive.map((item) => item.id), strategy: verticalListSortingStrategy, children: [_jsx("ol", Object.assign({ className: "ndl-tree-view-list", role: "tree" }, htmlAttributes, { children: flattenedAndRemovedCollapsedItemsWithActive.map((item) => { var _a; return (_jsx(SortableTreeViewItem, { depth: item.id === activeId && projected ? projected.depth : item.depth, indentationWidth: indentationWidth, item: item, isLast: false, parent: (_a = flattenItems.find((i) => i.id === item.parentId)) !== null && _a !== void 0 ? _a : null, id: item.id, trails: trails[item.id], keepGhostInPlace: item.id === ACTIVE_PLACEHOLDER_ID, TreeItemComponent: TreeItemComponent === undefined ? TreeViewTextItem : TreeItemComponent, onCollapse: () => handleToggleCollapse(item.id), onItemsChanged: (newItems, itemChangedReason) => onItemsChanged(buildTree(newItems), itemChangedReason), items: flattenItems, tabIndex: item.id === rowIdWithFocus ? 0 : -1, onFocus: () => setFocusedRowId(item.id), shouldDisableSorting: item.isSortable === false }, item.id)); }) })), createPortal(_jsx(DragOverlay, { dropAnimation: dropAnimationConfig }), document.body)] }) })); }; const TreeViewComponents = { SortableTreeViewItem, TreeItemWrapper, TreeViewTextItem, }; export const TreeView = Object.assign(TreeViewComponent, TreeViewComponents); //# sourceMappingURL=TreeView.js.map