@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
259 lines • 12.8 kB
JavaScript
/**
*
* 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 { useCallback, useEffect, useMemo } from 'react';
import { isMac } from './utils';
const isMultiSelectKeyPressed = (evt) => (!isMac && evt.ctrlKey) || (isMac && evt.metaKey);
const isClickingOnTextInput = (e) => {
if (!(e.target instanceof HTMLElement)) {
return false;
}
return (e.target.isContentEditable ||
['INPUT', 'TEXTAREA'].includes(e.target.tagName));
};
export function useManagedNodeState({ selected, setSelected, gesture, interactionMode, setInteractionMode, mouseEventCallbacks, nvlGraph, highlightedNodeIds, highlightedRelationshipIds, }) {
const panOnSpace = useCallback((evt) => {
// We could check the gesture here
// but it doesn't really matter if we switch to panning mode even in single select
if (interactionMode === 'select' && evt.key === ' ') {
setInteractionMode('pan');
}
}, [interactionMode, setInteractionMode]);
const stopPanningReleaseSpace = useCallback((evt) => {
if (interactionMode === 'pan' && evt.key === ' ') {
setInteractionMode('select');
}
}, [interactionMode, setInteractionMode]);
useEffect(() => {
document.addEventListener('keydown', panOnSpace);
document.addEventListener('keyup', stopPanningReleaseSpace);
return () => {
document.removeEventListener('keydown', panOnSpace);
document.removeEventListener('keyup', stopPanningReleaseSpace);
};
}, [panOnSpace, stopPanningReleaseSpace]);
const { onBoxSelect: originalBoxSelect, onLassoSelect: originalOnLassoSelect, onLassoStarted, onBoxStarted, onPan = true, onHover, onHoverNodeMargin, onNodeClick: originalOnNodeClick, onRelationshipClick: originalOnRelationshipClick, onDragStart: originalOnDragStart, onDragEnd: originalOnDragEnd, onDrawEnded: originalOnDrawEnded, onDrawStarted: originalOnDrawStarted, onCanvasClick: originalOnCanvasClick, onNodeDoubleClick: originalOnNodeDoubleClick, onRelationshipDoubleClick: originalOnRelationshipDoubleClick, } = mouseEventCallbacks;
const onCanvasClick = useCallback((evt) => {
if (isClickingOnTextInput(evt)) {
return;
}
// Clear selection
setSelected({ nodeIds: [], relationshipIds: [] });
if (typeof originalOnCanvasClick === 'function') {
originalOnCanvasClick(evt);
}
}, [originalOnCanvasClick, setSelected]);
const onDragStart = useCallback((draggedNodes, evt) => {
setInteractionMode('drag');
// Update Selection
const draggedNodeIds = draggedNodes.map((n) => n.id);
if (selected.nodeIds.length === 0 || isMultiSelectKeyPressed(evt)) {
setSelected({
nodeIds: draggedNodeIds,
relationshipIds: selected.relationshipIds,
});
return;
}
setSelected({
nodeIds: draggedNodeIds,
relationshipIds: selected.relationshipIds,
});
if (typeof originalOnDragStart === 'function') {
originalOnDragStart(draggedNodes, evt);
}
}, [setSelected, originalOnDragStart, selected, setInteractionMode]);
const onDragEnd = useCallback((draggedNodes, evt) => {
if (typeof originalOnDragEnd === 'function') {
originalOnDragEnd(draggedNodes, evt);
}
setInteractionMode('select');
}, [originalOnDragEnd, setInteractionMode]);
// wrapping so that there's a function passed, even if the original function is undefined
const onDrawStarted = useCallback((evt) => {
if (typeof originalOnDrawStarted === 'function') {
originalOnDrawStarted(evt);
}
}, [originalOnDrawStarted]);
const onDrawEnded = useCallback((newRelationshipToAdd, newTargetNodeToAdd, event) => {
if (typeof originalOnDrawEnded === 'function') {
originalOnDrawEnded(newRelationshipToAdd, newTargetNodeToAdd, event);
}
}, [originalOnDrawEnded]);
const onNodeClick = useCallback((node, hitElements, evt) => {
if (isClickingOnTextInput(evt)) {
return;
}
if (isMultiSelectKeyPressed(evt)) {
// if not selected, add to selection, otherwise deselect
const isNodeSelected = selected.nodeIds.includes(node.id);
if (isNodeSelected) {
const newSelectedNodeIds = selected.nodeIds.filter((id) => id !== node.id);
setSelected({
nodeIds: newSelectedNodeIds,
relationshipIds: selected.relationshipIds,
});
}
else {
const newSelectedNodeIds = [...selected.nodeIds, node.id];
setSelected({
nodeIds: newSelectedNodeIds,
relationshipIds: selected.relationshipIds,
});
}
}
else {
setSelected({ nodeIds: [node.id], relationshipIds: [] });
}
if (typeof originalOnNodeClick === 'function') {
originalOnNodeClick(node, hitElements, evt);
}
}, [setSelected, selected, originalOnNodeClick]);
const onRelationshipClick = useCallback((relationship, hitElements, evt) => {
if (isClickingOnTextInput(evt)) {
return;
}
if (isMultiSelectKeyPressed(evt)) {
// if not selected, add to selection, otherwise deselect
const isRelationshipSelected = selected.relationshipIds.includes(relationship.id);
if (isRelationshipSelected) {
const newSelectedRelIds = selected.relationshipIds.filter((id) => id !== relationship.id);
setSelected({
nodeIds: selected.nodeIds,
relationshipIds: newSelectedRelIds,
});
}
else {
const newSelectedRelIds = [
...selected.relationshipIds,
relationship.id,
];
setSelected({
nodeIds: selected.nodeIds,
relationshipIds: newSelectedRelIds,
});
}
}
else {
setSelected({ nodeIds: [], relationshipIds: [relationship.id] });
}
if (typeof originalOnRelationshipClick === 'function') {
originalOnRelationshipClick(relationship, hitElements, evt);
}
}, [setSelected, selected, originalOnRelationshipClick]);
const onNodeDoubleClick = useCallback((node, hitElements, evt) => {
if (isClickingOnTextInput(evt)) {
return;
}
if (typeof originalOnNodeDoubleClick === 'function') {
originalOnNodeDoubleClick(node, hitElements, evt);
}
}, [originalOnNodeDoubleClick]);
const onRelationshipDoubleClick = useCallback((relationship, hitElements, evt) => {
if (isClickingOnTextInput(evt)) {
return;
}
if (typeof originalOnRelationshipDoubleClick === 'function') {
originalOnRelationshipDoubleClick(relationship, hitElements, evt);
}
}, [originalOnRelationshipDoubleClick]);
const onMultiSelect = useCallback((nodes, rels, evt) => {
const nodeIds = nodes.map((n) => n.id);
const relationshipIds = rels.map((r) => r.id);
if (isMultiSelectKeyPressed(evt)) {
const currentlySelectedNodeIds = selected.nodeIds;
const currentlySelectedRelIds = selected.relationshipIds;
// Toggle selection: items that are already selected should be removed from selection,
// those that are not should be added
const xor = (a, b) => [
...new Set([...a, ...b].filter((id) => !a.includes(id) || !b.includes(id))),
];
const newSelectedNodeIds = xor(currentlySelectedNodeIds, nodeIds);
const newSelectedRelIds = xor(currentlySelectedRelIds, relationshipIds);
setSelected({
nodeIds: newSelectedNodeIds,
relationshipIds: newSelectedRelIds,
});
}
else {
// REPLACE selection
setSelected({ nodeIds: nodeIds, relationshipIds: relationshipIds });
}
}, [setSelected, selected]);
const onLassoSelect = useCallback(({ nodes, rels }, evt) => {
onMultiSelect(nodes, rels, evt);
if (typeof originalOnLassoSelect === 'function') {
originalOnLassoSelect({ nodes, rels }, evt);
}
}, [onMultiSelect, originalOnLassoSelect]);
const onBoxSelect = useCallback(({ nodes, rels }, evt) => {
onMultiSelect(nodes, rels, evt);
if (typeof originalBoxSelect === 'function') {
originalBoxSelect({ nodes, rels }, evt);
}
}, [onMultiSelect, originalBoxSelect]);
const isDrawMode = interactionMode === 'draw';
const isSelectMode = interactionMode === 'select';
const shouldEnableBox = isSelectMode && gesture === 'box';
const shouldEnableLasso = isSelectMode && gesture === 'lasso';
const shouldEnablePan = interactionMode === 'pan' || (isSelectMode && gesture === 'single');
const canDrag = interactionMode === 'drag' || interactionMode === 'select';
const wrappedMouseEventCallbacks = useMemo(() => {
var _a;
return (Object.assign(Object.assign({}, mouseEventCallbacks), { onBoxSelect: shouldEnableBox ? onBoxSelect : false, onBoxStarted: shouldEnableBox ? onBoxStarted : false, onCanvasClick: isSelectMode ? onCanvasClick : false, onDragEnd: canDrag ? onDragEnd : false, onDragStart: canDrag ? onDragStart : false, onDrawEnded: isDrawMode ? onDrawEnded : false, onDrawStarted: isDrawMode ? onDrawStarted : false, onHover: isSelectMode ? onHover : false, onHoverNodeMargin: isDrawMode ? onHoverNodeMargin : false, onLassoSelect: shouldEnableLasso ? onLassoSelect : false, onLassoStarted: shouldEnableLasso ? onLassoStarted : false, onNodeClick: isSelectMode ? onNodeClick : false, onNodeDoubleClick: isSelectMode ? onNodeDoubleClick : false, onPan: shouldEnablePan ? onPan : false, onRelationshipClick: isSelectMode ? onRelationshipClick : false, onRelationshipDoubleClick: isSelectMode
? onRelationshipDoubleClick
: false, onZoom: (_a = mouseEventCallbacks.onZoom) !== null && _a !== void 0 ? _a : true }));
}, [
canDrag,
shouldEnableBox,
shouldEnableLasso,
shouldEnablePan,
isDrawMode,
isSelectMode,
mouseEventCallbacks,
onBoxSelect,
onBoxStarted,
onCanvasClick,
onDragEnd,
onDragStart,
onDrawEnded,
onDrawStarted,
onHover,
onHoverNodeMargin,
onLassoSelect,
onLassoStarted,
onNodeClick,
onNodeDoubleClick,
onPan,
onRelationshipClick,
onRelationshipDoubleClick,
]);
const selectionSet = useMemo(() => ({
nodeIds: new Set(selected.nodeIds),
relIds: new Set(selected.relationshipIds),
}), [selected]);
const highlightedNodeSet = useMemo(() => highlightedNodeIds !== undefined ? new Set(highlightedNodeIds) : null, [highlightedNodeIds]);
const highlightedRelSet = useMemo(() => highlightedRelationshipIds !== undefined
? new Set(highlightedRelationshipIds)
: null, [highlightedRelationshipIds]);
const nodesWithState = useMemo(() => nvlGraph.nodes.map((n) => (Object.assign(Object.assign({}, n), { disabled: highlightedNodeSet ? !highlightedNodeSet.has(n.id) : false, selected: selectionSet.nodeIds.has(n.id) }))), [nvlGraph.nodes, selectionSet, highlightedNodeSet]);
const relsWithState = useMemo(() => nvlGraph.rels.map((r) => (Object.assign(Object.assign({}, r), { disabled: highlightedRelSet ? !highlightedRelSet.has(r.id) : false, selected: selectionSet.relIds.has(r.id) }))), [nvlGraph.rels, selectionSet, highlightedRelSet]);
return { nodesWithState, relsWithState, wrappedMouseEventCallbacks };
}
//# sourceMappingURL=use-managed-node-state.js.map