@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
304 lines • 15.5 kB
JavaScript
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