Compare commits

...
8 Commits
5 changed files with 425 additions and 130 deletions
@@ -20,6 +20,7 @@ export interface NodeProps {
scaleY?: number;
offsetX?: number;
offsetY?: number;
zIndex?: number;
}
export class Node {
@@ -42,6 +43,7 @@ export class Node {
scaleY: number;
offsetX: number;
offsetY: number;
zIndex: number;
constructor({
id,
@@ -62,7 +64,8 @@ export class Node {
scaleX = 1,
scaleY = 1,
offsetX = 0,
offsetY = 0
offsetY = 0,
zIndex = 0
}: NodeProps) {
this.id = id;
this.x = x;
@@ -83,6 +86,7 @@ export class Node {
this.scaleY = scaleY;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.zIndex = zIndex;
}
setPosition(x: number, y: number): void {
@@ -14,8 +14,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClock, faImage } from "@fortawesome/pro-solid-svg-icons";
import Konva from "konva"; // just for types
import { setCanvasRenderBitmap } from "../../signals/canvasRenderBitmap";
import { captureStageImageBitmap } from "./hooks/useUpdateSnapshot";
import { setCanvasRenderBitmap } from "../../signals/canvasRenderBitmap"
import { captureStageImageBitmap } from "./hooks/useUpdateSnapshot"
import { ContextMenuContainer } from "./components/ui/ContextMenu";
const PageDraw = () => {
//useStateSceneLoader();
@@ -271,23 +271,58 @@ const PageDraw = () => {
activeToolId={store.activeTool}
/>
<div className="relative z-0">
<ContextMenuContainer>
<PaintSurface
nodes={store.nodes}
selectedNodeIds={store.selectedNodeIds}
onCanvasSizeChange={(width: number, height: number): void => {
canvasWidth.current = width;
canvasHeight.current = height;
}}
fillColor={store.fillColor}
activeTool={store.activeTool}
brushColor={store.brushColor}
brushSize={store.brushSize}
onSelectionChange={setIsSelecting}
stageRef={stageRef}
transformerRefs={transformerRefs}
/>
</ContextMenuContainer>
<ContextMenuContainer
onAction={(e, action) => {
if (action === "contextMenu") {
const hasSelection = store.selectedNodeIds.length > 0;
if (hasSelection) {
console.log("An item is selected.");
// You can add additional actions here based on the selection
return true
} else {
console.log("No item is selected.");
return false
}
}
}}
onMenuAction={(action) => {
switch (action) {
case 'BRING_TO_FRONT':
store.bringToFront(store.selectedNodeIds);
break;
case 'BRING_FORWARD':
store.bringForward(store.selectedNodeIds);
break;
case 'SEND_BACKWARD':
store.sendBackward(store.selectedNodeIds);
break;
case 'SEND_TO_BACK':
store.sendToBack(store.selectedNodeIds);
break;
default:
// No action needed for unhandled cases
}
}}
>
<PaintSurface
nodes={store.nodes}
selectedNodeIds={store.selectedNodeIds}
onCanvasSizeChange={(width: number, height: number): void => {
canvasWidth.current = width;
canvasHeight.current = height;
}}
fillColor={store.fillColor}
activeTool={store.activeTool}
brushColor={store.brushColor}
brushSize={store.brushSize}
onSelectionChange={setIsSelecting}
stageRef={stageRef}
transformerRefs={transformerRefs}
/>
</ContextMenuContainer>
</div>
<div className="absolute bottom-6 left-6 z-20 flex items-center gap-2">
<PopoverMenu
@@ -774,6 +774,7 @@ export const PaintSurface = ({
rotation={lineNode.rotation || 0}
offsetX={lineNode.offsetX || 0}
offsetY={lineNode.offsetY || 0}
zIndex={lineNode.zIndex}
/>
{renderTransformer()}
</React.Fragment>
@@ -791,12 +792,13 @@ export const PaintSurface = ({
height={node.height}
fill={node.fill}
stroke={node.stroke}
strokeWidth={node.strokeWidth}
strokeWidth={0}
rotation={node.rotation || 0}
scaleX={node.scaleX || 1}
scaleY={node.scaleY || 1}
offsetX={node.offsetX || 0}
offsetY={node.offsetY || 0}
zIndex={node.zIndex}
draggable={draggableIfToolsNotActive(activeTool, node.draggable)}
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
onTap={(e) => handleNodeMouseDown(e, node.id)}
@@ -822,12 +824,13 @@ export const PaintSurface = ({
height={node.height}
fill={node.fill}
stroke={node.stroke}
strokeWidth={node.strokeWidth}
strokeWidth={0}
rotation={node.rotation || 0}
scaleX={node.scaleX || 1}
scaleY={node.scaleY || 1}
offsetX={node.offsetX || 0}
offsetY={node.offsetY || 0}
zIndex={node.zIndex}
draggable={draggableIfToolsNotActive(activeTool, node.draggable)}
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
onTap={(e) => handleNodeMouseDown(e, node.id)}
@@ -853,12 +856,13 @@ export const PaintSurface = ({
radius={Math.min(node.width, node.height) / 2}
fill={node.fill}
stroke={node.stroke}
strokeWidth={node.strokeWidth}
strokeWidth={0}
rotation={node.rotation || 0}
scaleX={node.scaleX || 1}
scaleY={node.scaleY || 1}
offsetX={node.offsetX || 0}
offsetY={node.offsetY || 0}
zIndex={node.zIndex}
draggable={draggableIfToolsNotActive(activeTool, node.draggable)}
onMouseDown={(e) => handleNodeMouseDown(e, node.id)}
onTap={(e) => handleNodeMouseDown(e, node.id)}
@@ -892,6 +896,7 @@ export const PaintSurface = ({
offsetX={node.offsetX || 0}
offsetY={node.offsetY || 0}
listening={false}
zIndex={node.zIndex}
/>
)}
<Image
@@ -999,7 +1004,12 @@ export const PaintSurface = ({
/>
{/* Render all nodes including line nodes */}
{[...nodes, ...store.lineNodes].map((node) => renderNode(node))}
{[...nodes, ...store.lineNodes]
.sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0))
.map((node, index) => {
// console.log(`Node ${index}:`, node);
return renderNode(node);
})}
{/* Render selection rectangle */}
{selectionRect && (
@@ -1,24 +1,25 @@
import React, { useEffect, useRef } from 'react';
interface MenuPosition {
export interface MenuPosition {
x: number;
y: number;
}
interface MenuItem {
export interface MenuItem {
icon: React.ReactNode;
label: string;
onClick: () => void;
action: string; // Instead of onClick, we use an action identifier
divider?: boolean;
}
interface ContextMenuProps {
items: MenuItem[];
onAction: (action: string) => void; // Single callback for all menu items
onClose: () => void;
position: MenuPosition;
}
export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onClose, position }) => {
export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onAction, onClose, position }) => {
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -45,7 +46,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onClose, positi
<React.Fragment key={index}>
<button
onClick={() => {
item.onClick();
onAction(item.action); // Pass the action identifier up
onClose();
}}
className="w-full px-4 py-2.5 flex items-center gap-3 hover:bg-[#333333] text-[#ECECEC] text-[13px] transition-colors duration-75 ease-in-out"
@@ -62,109 +63,56 @@ export const ContextMenu: React.FC<ContextMenuProps> = ({ items, onClose, positi
);
};
// Example usage with icons
const menuItems: MenuItem[] = [
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M12 3L4 7.5L12 12L20 7.5L12 3Z" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M4 16.5L12 21L20 16.5" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M4 12L12 16.5L20 12" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>,
label: 'Convert to 3D',
onClick: () => console.log('Convert to 3D')
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M4 4v16h16M4 20l16-16" strokeWidth="2" strokeLinecap="round"/>
</svg>,
label: 'Remove background',
onClick: () => console.log('Remove background')
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M12 10V14M8 6H16C17.1046 6 18 6.89543 18 8V16C18 17.1046 17.1046 18 16 18H8C6.89543 18 6 17.1046 6 16V8C6 6.89543 6.89543 6 8 6Z" strokeWidth="2" strokeLinecap="round"/>
</svg>,
label: 'Lock',
onClick: () => console.log('Lock'),
divider: true
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M5 15L12 8L19 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>,
label: 'Bring to front',
onClick: () => console.log('Bring to front')
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M8 14L12 10L16 14" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>,
label: 'Bring forward',
onClick: () => console.log('Bring forward')
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M8 10L12 14L16 10" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>,
label: 'Send backward',
onClick: () => console.log('Send backward')
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M5 9L12 16L19 9" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>,
label: 'Send to back',
onClick: () => console.log('Send to back'),
divider: true
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M16 8V6M16 8H8M16 8V16M8 8V6M8 8V16M8 16H6M8 16H16M16 16H18M6 6H8M8 6H16M16 6H18" strokeWidth="2" strokeLinecap="round"/>
</svg>,
label: 'Duplicate',
onClick: () => console.log('Duplicate')
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M7 16L17 8M7 8L17 16" strokeWidth="2" strokeLinecap="round"/>
</svg>,
label: 'Flip Horizontal',
onClick: () => console.log('Flip Horizontal')
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M8 7L16 17M8 17L16 7" strokeWidth="2" strokeLinecap="round"/>
</svg>,
label: 'Flip Vertical',
onClick: () => console.log('Flip Vertical')
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M4 12V6C4 4.89543 4.89543 4 6 4H18C19.1046 4 20 4.89543 20 6V18C20 19.1046 19.1046 20 18 20H12" strokeWidth="2" strokeLinecap="round"/>
<path d="M9 15H4V20" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>,
label: 'Fit to screen',
onClick: () => console.log('Fit to screen')
}
];
interface ContextMenuContainerProps {
children: React.ReactNode;
items?: MenuItem[];
onAction?: (e: React.MouseEvent, actionName: string) => boolean | void;
onMenuAction?: (action: string) => void; // New prop for menu item actions
className?: string;
}
// Container component that handles the right-click
export const ContextMenuContainer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
export const ContextMenuContainer: React.FC<ContextMenuContainerProps> = ({
children,
items = defaultMenuItems,
onAction,
onMenuAction,
className = ''
}) => {
const [showMenu, setShowMenu] = React.useState(false);
const [position, setPosition] = React.useState<MenuPosition>({ x: 0, y: 0 });
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
setPosition({ x: e.clientX, y: e.clientY });
setShowMenu(true);
const newPosition = { x: e.clientX, y: e.clientY };
setPosition(newPosition);
const shouldShowMenu = onAction?.(e, 'contextMenu');
if (shouldShowMenu !== undefined) {
setShowMenu(shouldShowMenu);
} else {
setShowMenu(true);
}
};
const handleClick = (e: React.MouseEvent) => {
onAction?.(e, 'click');
};
const handleMenuAction = (action: string) => {
onMenuAction?.(action);
};
return (
<div onContextMenu={handleContextMenu} className="w-full h-full">
<div
onClick={handleClick}
onContextMenu={handleContextMenu}
className={`w-full h-full ${className}`}
>
{children}
{showMenu && (
<ContextMenu
items={menuItems}
items={items}
onAction={handleMenuAction}
position={position}
onClose={() => setShowMenu(false)}
/>
@@ -172,3 +120,59 @@ export const ContextMenuContainer: React.FC<{ children: React.ReactNode }> = ({
</div>
);
};
// Default menu items now use action identifiers instead of callbacks
const defaultMenuItems: MenuItem[] = [
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M4 4v16h16M4 20l16-16" strokeWidth="2" strokeLinecap="round"/>
</svg>,
label: 'Remove background',
action: 'REMOVE_BACKGROUND'
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M12 10V14M8 6H16C17.1046 6 18 6.89543 18 8V16C18 17.1046 17.1046 18 16 18H8C6.89543 18 6 17.1046 6 16V8C6 6.89543 6.89543 6 8 6Z" strokeWidth="2" strokeLinecap="round"/>
</svg>,
label: 'Lock',
action: 'LOCK',
divider: true
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M5 15L12 8L19 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>,
label: 'Bring to front',
action: 'BRING_TO_FRONT'
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M8 14L12 10L16 14" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>,
label: 'Bring forward',
action: 'BRING_FORWARD'
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M8 10L12 14L16 10" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>,
label: 'Send backward',
action: 'SEND_BACKWARD'
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M5 9L12 16L19 9" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>,
label: 'Send to back',
action: 'SEND_TO_BACK',
divider: true
},
{
icon: <svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M16 8V6M16 8H8M16 8V16M8 8V6M8 8V16M8 16H6M8 16H16M16 16H18M6 6H8M8 6H16M16 6H18" strokeWidth="2" strokeLinecap="round"/>
</svg>,
label: 'Duplicate',
action: 'DUPLICATE'
},
];
@@ -17,6 +17,7 @@ export type LineNode = {
scaleY?: number;
offsetX?: number;
offsetY?: number;
zIndex: number;
};
export type ActiveTool = 'select' | 'draw' | 'eraser' | 'backgroundColor' | 'shape';
@@ -103,6 +104,12 @@ interface SceneState {
// Add these two properties
serializeSceneToString: () => Promise<string>;
loadSceneFromString: (jsonString: string) => boolean;
// New functions
bringToFront: (nodeIds: string[]) => void;
sendToBack: (nodeIds: string[]) => void;
bringForward: (nodeIds: string[]) => void;
sendBackward: (nodeIds: string[]) => void;
}
const generateId = (): string => {
@@ -112,6 +119,22 @@ const generateId = (): string => {
// Add a flag to prevent saving state during restoration
let isRestoring = false;
// Add this helper function at the top of the store (before create<SceneState>)
const getNextZIndex = (nodes: Node[], lineNodes: LineNode[]): number => {
const allZIndices = [
...nodes.map((n, index) => {
//console.log(`Node: ${JSON.stringify(n)}, Index: ${index}`);
return n.zIndex || 0;
}),
...lineNodes.map((n, index) => {
//console.log(`LineNode: ${JSON.stringify(n)}, Index: ${index}`);
return n.zIndex || 0;
})
];
return allZIndices.length > 0 ? Math.max(...allZIndices) + 1 : 0;
};
export const useSceneStore = create<SceneState>((set, get) => ({
// Initial state
nodes: [],
@@ -135,8 +158,17 @@ export const useSceneStore = create<SceneState>((set, get) => ({
// Actions
addNode: (node: Node) => {
set((state) => {
const newNodes = [...state.nodes, node];
return { nodes: newNodes };
const nextZ = getNextZIndex(state.nodes, state.lineNodes);
console.log(`NextZ for node ID ${node.id}: ${nextZ}`);
const newNode = new Node({
...node,
zIndex: nextZ
});
// console.log("New Node with ID:", newNode.id);
let nodes = [...state.nodes, newNode]
console.log("Nodes after update")
console.log(nodes)
return { nodes: nodes };
});
get().saveState();
},
@@ -161,9 +193,17 @@ export const useSceneStore = create<SceneState>((set, get) => ({
updateNode: (id: string, updates: Partial<Node>, shouldSaveState: boolean = true) => {
set((state) => {
const newNodes = state.nodes.map(node => {
let newZIndex = updates.zIndex !== undefined ? updates.zIndex : node.zIndex
if (node.id === id) {
return new Node({ ...node, ...updates });
return new Node({
...node,
...updates,
zIndex: newZIndex
});
}
return node;
});
@@ -451,8 +491,13 @@ export const useSceneStore = create<SceneState>((set, get) => ({
// Add new line node actions
addLineNode: (lineNode: LineNode) => {
set((state) => {
const newLineNodes = [...state.lineNodes, lineNode];
return { lineNodes: newLineNodes };
const nextZ = getNextZIndex(state.nodes, state.lineNodes);
const newLineNode = {
...lineNode,
zIndex: nextZ
};
return { lineNodes: [...state.lineNodes, newLineNode] };
});
get().saveState();
},
@@ -479,7 +524,11 @@ export const useSceneStore = create<SceneState>((set, get) => ({
set((state) => {
const newLineNodes = state.lineNodes.map(node => {
if (node.id === id) {
return { ...node, ...updates };
return {
...node,
...updates,
zIndex: updates.zIndex !== undefined ? updates.zIndex : node.zIndex
};
}
return node;
});
@@ -585,11 +634,11 @@ export const useSceneStore = create<SceneState>((set, get) => ({
// Load the image asynchronously
node.setImage(source).then(() => {
// Update the node in the store after image loads
console.log('Image loaded for node:', node.id);
get().updateNode(node.id, node, false);
get().addNode(node);
//get().updateNode(node.id, node, false);
}).catch((error)=>console.error('Error loading image:', error));
get().addNode(node);
},
// Action for deleting selected items
@@ -888,5 +937,198 @@ export const useSceneStore = create<SceneState>((set, get) => ({
},
loadSceneFromString: (jsonString: string): boolean => {
return get().importSceneFromJson(jsonString);
}
},
// New functions
bringToFront: (nodeIds: string[]) => {
set(state => {
// Get highest zIndex from all nodes and line nodes
const allZIndices = [
...state.nodes.map(n => n.zIndex || 0),
...state.lineNodes.map(n => n.zIndex || 0)
];
const maxZ = Math.max(...allZIndices, 0) + 1;
return {
nodes: state.nodes.map(node =>
nodeIds.includes(node.id)
? new Node({ ...node, zIndex: maxZ })
: node
),
lineNodes: state.lineNodes.map(node =>
nodeIds.includes(node.id)
? { ...node, zIndex: maxZ }
: node
)
};
});
get().saveState();
},
sendToBack: (nodeIds: string[]) => {
set(state => {
// Get lowest zIndex from all nodes and line nodes
const allZIndices = [
...state.nodes.map(n => n.zIndex || 0),
...state.lineNodes.map(n => n.zIndex || 0)
];
const minZ = Math.min(...allZIndices, 0) - 1;
return {
nodes: state.nodes.map(node =>
nodeIds.includes(node.id)
? new Node({ ...node, zIndex: minZ })
: node
),
lineNodes: state.lineNodes.map(node =>
nodeIds.includes(node.id)
? { ...node, zIndex: minZ }
: node
)
};
});
get().saveState();
},
bringForward: (nodeIds: string[]) => {
set(state => {
// Combine all items with their current zIndex
const allItems = [
...state.nodes.map(n => ({ id: n.id, z: Math.max(0, n.zIndex || 0), selected: nodeIds.includes(n.id) })),
...state.lineNodes.map(n => ({ id: n.id, z: Math.max(0, n.zIndex || 0), selected: nodeIds.includes(n.id) }))
].sort((a, b) => a.z - b.z);
// Find the highest z-index among selected items
const selectedItems = allItems.filter(item => item.selected);
if (selectedItems.length === 0) return state;
const highestSelectedZ = Math.max(...selectedItems.map(item => item.z));
// Find the closest item above our selection
const nextItem = allItems.find(item => !item.selected && item.z > highestSelectedZ);
// If no item above, swap with the highest non-selected item
if (!nextItem) {
const highestNonSelected = allItems.filter(item => !item.selected)
.sort((a, b) => b.z - a.z)[0];
if (!highestNonSelected) return state;
// Swap positions
return {
nodes: state.nodes.map(node => {
if (nodeIds.includes(node.id)) {
return new Node({ ...node, zIndex: highestNonSelected.z });
}
if (node.id === highestNonSelected.id) {
return new Node({ ...node, zIndex: highestSelectedZ });
}
return node;
}),
lineNodes: state.lineNodes.map(node => {
if (nodeIds.includes(node.id)) {
return { ...node, zIndex: highestNonSelected.z };
}
if (node.id === highestNonSelected.id) {
return { ...node, zIndex: highestSelectedZ };
}
return node;
})
};
}
const newZ = nextItem.z;
return {
nodes: state.nodes.map(node => {
if (nodeIds.includes(node.id)) {
return new Node({ ...node, zIndex: newZ });
}
if (node.id === nextItem.id) {
return new Node({ ...node, zIndex: highestSelectedZ });
}
return node;
}),
lineNodes: state.lineNodes.map(node => {
if (nodeIds.includes(node.id)) {
return { ...node, zIndex: newZ };
}
if (node.id === nextItem.id) {
return { ...node, zIndex: highestSelectedZ };
}
return node;
})
};
});
get().saveState();
},
sendBackward: (nodeIds: string[]) => {
set(state => {
// Combine all items with their current zIndex
const allItems = [
...state.nodes.map(n => ({ id: n.id, z: Math.max(0, n.zIndex || 0), selected: nodeIds.includes(n.id) })),
...state.lineNodes.map(n => ({ id: n.id, z: Math.max(0, n.zIndex || 0), selected: nodeIds.includes(n.id) }))
].sort((a, b) => b.z - a.z);
// Find the lowest z-index among selected items
const selectedItems = allItems.filter(item => item.selected);
if (selectedItems.length === 0) return state;
const lowestSelectedZ = Math.min(...selectedItems.map(item => item.z));
// Find the closest item below our selection
const nextItem = allItems.find(item => !item.selected && item.z < lowestSelectedZ);
// If no item below, swap with the lowest non-selected item
if (!nextItem) {
const lowestNonSelected = allItems.filter(item => !item.selected)
.sort((a, b) => a.z - b.z)[0];
if (!lowestNonSelected) return state;
// Swap positions
return {
nodes: state.nodes.map(node => {
if (nodeIds.includes(node.id)) {
return new Node({ ...node, zIndex: lowestNonSelected.z });
}
if (node.id === lowestNonSelected.id) {
return new Node({ ...node, zIndex: lowestSelectedZ });
}
return node;
}),
lineNodes: state.lineNodes.map(node => {
if (nodeIds.includes(node.id)) {
return { ...node, zIndex: lowestNonSelected.z };
}
if (node.id === lowestNonSelected.id) {
return { ...node, zIndex: lowestSelectedZ };
}
return node;
})
};
}
const newZ = nextItem.z;
return {
nodes: state.nodes.map(node => {
if (nodeIds.includes(node.id)) {
return new Node({ ...node, zIndex: newZ });
}
if (node.id === nextItem.id) {
return new Node({ ...node, zIndex: lowestSelectedZ });
}
return node;
}),
lineNodes: state.lineNodes.map(node => {
if (nodeIds.includes(node.id)) {
return { ...node, zIndex: newZ };
}
if (node.id === nextItem.id) {
return { ...node, zIndex: lowestSelectedZ };
}
return node;
})
};
});
get().saveState();
},
}));