mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
Unified node model to pass data from layout to blocks (#259)
This adds a new NodeModel, which can be passed from the TileLayout to contained blocks. It contains all the layout data that the block should care about, including focus status, whether a drag operation is underway, whether the node is magnified, etc. This also adds a focus stack for the layout, which will let the focus switch to the last-focused node when the currently-focused one is closed. This also addresses a regression in the resize handles that caused them to be offset from the cursor when dragged. --------- Co-authored-by: sawka <mike.sawka@gmail.com>
This commit is contained in:
@@ -5,10 +5,10 @@ import { TileLayout } from "./lib/TileLayout";
|
||||
import { LayoutModel } from "./lib/layoutModel";
|
||||
import {
|
||||
deleteLayoutModelForTab,
|
||||
getLayoutModelForActiveTab,
|
||||
getLayoutModelForTab,
|
||||
getLayoutModelForTabById,
|
||||
useLayoutModel,
|
||||
useLayoutNode,
|
||||
} from "./lib/layoutModelHooks";
|
||||
import { newLayoutNode } from "./lib/layoutNode";
|
||||
import type {
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
LayoutTreeCommitPendingAction,
|
||||
LayoutTreeComputeMoveNodeAction,
|
||||
LayoutTreeDeleteNodeAction,
|
||||
LayoutTreeFocusNodeAction,
|
||||
LayoutTreeInsertNodeAction,
|
||||
LayoutTreeInsertNodeAtIndexAction,
|
||||
LayoutTreeMagnifyNodeToggleAction,
|
||||
@@ -27,12 +28,15 @@ import type {
|
||||
LayoutTreeSetPendingAction,
|
||||
LayoutTreeStateSetter,
|
||||
LayoutTreeSwapNodeAction,
|
||||
NodeModel,
|
||||
PreviewRenderer,
|
||||
} from "./lib/types";
|
||||
import { DropDirection, LayoutTreeActionType, NavigateDirection } from "./lib/types";
|
||||
|
||||
export {
|
||||
deleteLayoutModelForTab,
|
||||
DropDirection,
|
||||
getLayoutModelForActiveTab,
|
||||
getLayoutModelForTab,
|
||||
getLayoutModelForTabById,
|
||||
LayoutModel,
|
||||
@@ -41,7 +45,6 @@ export {
|
||||
newLayoutNode,
|
||||
TileLayout,
|
||||
useLayoutModel,
|
||||
useLayoutNode,
|
||||
};
|
||||
export type {
|
||||
ContentRenderer,
|
||||
@@ -51,6 +54,7 @@ export type {
|
||||
LayoutTreeCommitPendingAction,
|
||||
LayoutTreeComputeMoveNodeAction,
|
||||
LayoutTreeDeleteNodeAction,
|
||||
LayoutTreeFocusNodeAction,
|
||||
LayoutTreeInsertNodeAction,
|
||||
LayoutTreeInsertNodeAtIndexAction,
|
||||
LayoutTreeMagnifyNodeToggleAction,
|
||||
@@ -59,4 +63,6 @@ export type {
|
||||
LayoutTreeSetPendingAction,
|
||||
LayoutTreeStateSetter,
|
||||
LayoutTreeSwapNodeAction,
|
||||
NodeModel,
|
||||
PreviewRenderer,
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ import { DropTargetMonitor, XYCoord, useDrag, useDragLayer, useDrop } from "reac
|
||||
import { debounce, throttle } from "throttle-debounce";
|
||||
import { useDevicePixelRatio } from "use-device-pixel-ratio";
|
||||
import { LayoutModel } from "./layoutModel";
|
||||
import { useLayoutNode, useTileLayout } from "./layoutModelHooks";
|
||||
import { useNodeModel, useTileLayout } from "./layoutModelHooks";
|
||||
import "./tilelayout.less";
|
||||
import {
|
||||
LayoutNode,
|
||||
@@ -53,7 +53,6 @@ const DragPreviewHeight = 300;
|
||||
|
||||
function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutProps) {
|
||||
const layoutModel = useTileLayout(tabAtom, contents);
|
||||
const generation = useAtomValue(layoutModel.generationAtom);
|
||||
const overlayTransform = useAtomValue(layoutModel.overlayTransform);
|
||||
const setActiveDrag = useSetAtom(layoutModel.activeDrag);
|
||||
const setReady = useSetAtom(layoutModel.ready);
|
||||
@@ -85,7 +84,7 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
|
||||
}
|
||||
}
|
||||
}),
|
||||
[getCursorPoint, generation]
|
||||
[getCursorPoint]
|
||||
);
|
||||
|
||||
// Effect to detect when the cursor leaves the TileLayout hit trap so we can remove any placeholders. This cannot be done using pointer capture
|
||||
@@ -115,7 +114,7 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
|
||||
>
|
||||
<div key="display" ref={layoutModel.displayContainerRef} className="display-container">
|
||||
<ResizeHandleWrapper layoutModel={layoutModel} />
|
||||
<DisplayNodesWrapper contents={contents} layoutModel={layoutModel} />
|
||||
<DisplayNodesWrapper layoutModel={layoutModel} />
|
||||
</div>
|
||||
<Placeholder key="placeholder" layoutModel={layoutModel} style={{ top: 10000, ...overlayTransform }} />
|
||||
<OverlayNodeWrapper layoutModel={layoutModel} />
|
||||
@@ -131,19 +130,15 @@ interface DisplayNodesWrapperProps {
|
||||
* The layout tree state.
|
||||
*/
|
||||
layoutModel: LayoutModel;
|
||||
/**
|
||||
* contains callbacks and information about the contents (or styling) of of the TileLayout
|
||||
*/
|
||||
contents: TileLayoutContents;
|
||||
}
|
||||
|
||||
const DisplayNodesWrapper = ({ layoutModel, contents }: DisplayNodesWrapperProps) => {
|
||||
const DisplayNodesWrapper = ({ layoutModel }: DisplayNodesWrapperProps) => {
|
||||
const leafs = useAtomValue(layoutModel.leafs);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
leafs.map((leaf) => {
|
||||
return <DisplayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} contents={contents} />;
|
||||
leafs.map((node) => {
|
||||
return <DisplayNode key={node.id} layoutModel={layoutModel} node={node} />;
|
||||
}),
|
||||
[leafs]
|
||||
);
|
||||
@@ -154,12 +149,7 @@ interface DisplayNodeProps {
|
||||
/**
|
||||
* The leaf node object, containing the data needed to display the leaf contents to the user.
|
||||
*/
|
||||
layoutNode: LayoutNode;
|
||||
|
||||
/**
|
||||
* contains callbacks and information about the contents (or styling) of of the TileLayout
|
||||
*/
|
||||
contents: TileLayoutContents;
|
||||
node: LayoutNode;
|
||||
}
|
||||
|
||||
const dragItemType = "TILE_ITEM";
|
||||
@@ -167,26 +157,23 @@ const dragItemType = "TILE_ITEM";
|
||||
/**
|
||||
* The draggable and displayable portion of a leaf node in a layout tree.
|
||||
*/
|
||||
const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) => {
|
||||
const DisplayNode = ({ layoutModel, node }: DisplayNodeProps) => {
|
||||
const nodeModel = useNodeModel(layoutModel, node);
|
||||
const tileNodeRef = useRef<HTMLDivElement>(null);
|
||||
const dragHandleRef = useRef<HTMLDivElement>(null);
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
const addlProps = useLayoutNode(layoutModel, layoutNode);
|
||||
const activeDrag = useAtomValue(layoutModel.activeDrag);
|
||||
const globalReady = useAtomValue(layoutModel.ready);
|
||||
|
||||
const addlProps = useAtomValue(nodeModel.additionalProps);
|
||||
const devicePixelRatio = useDevicePixelRatio();
|
||||
|
||||
const [{ isDragging }, drag, dragPreview] = useDrag(
|
||||
() => ({
|
||||
type: dragItemType,
|
||||
item: () => layoutNode,
|
||||
item: () => node,
|
||||
canDrag: () => !addlProps?.isMagnifiedNode,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
}),
|
||||
[layoutNode, addlProps]
|
||||
[node, addlProps]
|
||||
);
|
||||
|
||||
const [previewElementGeneration, setPreviewElementGeneration] = useState(0);
|
||||
@@ -203,11 +190,11 @@ const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) =>
|
||||
transform: `scale(${1 / devicePixelRatio})`,
|
||||
}}
|
||||
>
|
||||
{contents.renderPreview?.(layoutNode.data)}
|
||||
{layoutModel.renderPreview?.(nodeModel)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [contents.renderPreview, devicePixelRatio, layoutNode.data]);
|
||||
}, [devicePixelRatio, nodeModel]);
|
||||
|
||||
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null);
|
||||
const [previewImageGeneration, setPreviewImageGeneration] = useState(0);
|
||||
@@ -232,31 +219,20 @@ const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) =>
|
||||
previewImageGeneration,
|
||||
previewImage,
|
||||
devicePixelRatio,
|
||||
layoutNode.data,
|
||||
]);
|
||||
|
||||
// Register the display node as a draggable item
|
||||
useEffect(() => {
|
||||
drag(dragHandleRef);
|
||||
}, [drag, dragHandleRef.current]);
|
||||
|
||||
const leafContent = useMemo(() => {
|
||||
return (
|
||||
layoutNode.data && (
|
||||
<div key="leaf" className="tile-leaf">
|
||||
{contents.renderContent(
|
||||
layoutNode.data,
|
||||
globalReady,
|
||||
addlProps?.isMagnifiedNode ?? false,
|
||||
activeDrag,
|
||||
() => layoutModel.magnifyNodeToggle(layoutNode),
|
||||
() => layoutModel.closeNode(layoutNode),
|
||||
dragHandleRef
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
<div key="leaf" className="tile-leaf">
|
||||
{layoutModel.renderContent(nodeModel)}
|
||||
</div>
|
||||
);
|
||||
}, [layoutNode, globalReady, activeDrag, addlProps]);
|
||||
}, [nodeModel]);
|
||||
|
||||
// Register the display node as a draggable item
|
||||
useEffect(() => {
|
||||
drag(nodeModel.dragHandleRef);
|
||||
}, [drag, nodeModel.dragHandleRef.current]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -266,7 +242,7 @@ const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) =>
|
||||
"last-magnified": addlProps?.isLastMagnifiedNode,
|
||||
})}
|
||||
ref={tileNodeRef}
|
||||
id={layoutNode.id}
|
||||
id={node.id}
|
||||
style={addlProps?.transform}
|
||||
onPointerEnter={generatePreviewImage}
|
||||
onPointerOver={(event) => event.stopPropagation()}
|
||||
@@ -281,14 +257,14 @@ interface OverlayNodeWrapperProps {
|
||||
layoutModel: LayoutModel;
|
||||
}
|
||||
|
||||
const OverlayNodeWrapper = ({ layoutModel }: OverlayNodeWrapperProps) => {
|
||||
const OverlayNodeWrapper = memo(({ layoutModel }: OverlayNodeWrapperProps) => {
|
||||
const leafs = useAtomValue(layoutModel.leafs);
|
||||
const overlayTransform = useAtomValue(layoutModel.overlayTransform);
|
||||
|
||||
const overlayNodes = useMemo(
|
||||
() =>
|
||||
leafs.map((leaf) => {
|
||||
return <OverlayNode key={leaf.id} layoutModel={layoutModel} layoutNode={leaf} />;
|
||||
leafs.map((node) => {
|
||||
return <OverlayNode key={node.id} layoutModel={layoutModel} node={node} />;
|
||||
}),
|
||||
[leafs]
|
||||
);
|
||||
@@ -298,34 +274,31 @@ const OverlayNodeWrapper = ({ layoutModel }: OverlayNodeWrapperProps) => {
|
||||
{overlayNodes}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
interface OverlayNodeProps {
|
||||
/**
|
||||
* The layout node object corresponding to the OverlayNode.
|
||||
*/
|
||||
layoutNode: LayoutNode;
|
||||
/**
|
||||
* The layout tree state.
|
||||
*/
|
||||
layoutModel: LayoutModel;
|
||||
node: LayoutNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* An overlay representing the true flexbox layout of the LayoutTreeState. This holds the drop targets for moving around nodes and is used to calculate the
|
||||
* dimensions of the corresponding DisplayNode for each LayoutTreeState leaf.
|
||||
*/
|
||||
const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
|
||||
const additionalProps = useLayoutNode(layoutModel, layoutNode);
|
||||
const OverlayNode = memo(({ node, layoutModel }: OverlayNodeProps) => {
|
||||
const nodeModel = useNodeModel(layoutModel, node);
|
||||
const additionalProps = useAtomValue(nodeModel.additionalProps);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const generation = useAtomValue(layoutModel.generationAtom);
|
||||
|
||||
const [, drop] = useDrop(
|
||||
() => ({
|
||||
accept: dragItemType,
|
||||
canDrop: (_, monitor) => {
|
||||
const dragItem = monitor.getItem<LayoutNode>();
|
||||
if (monitor.isOver({ shallow: true }) && dragItem?.id !== layoutNode.id) {
|
||||
if (monitor.isOver({ shallow: true }) && dragItem?.id !== node.id) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -346,7 +319,7 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
|
||||
offset.y -= containerRect.y;
|
||||
layoutModel.treeReducer({
|
||||
type: LayoutTreeActionType.ComputeMove,
|
||||
node: layoutNode,
|
||||
node: node,
|
||||
nodeToMove: dragItem,
|
||||
direction: determineDropDirection(additionalProps.rect, offset),
|
||||
} as LayoutTreeComputeMoveNodeAction);
|
||||
@@ -358,7 +331,7 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
|
||||
}
|
||||
}),
|
||||
}),
|
||||
[layoutNode, generation, additionalProps, layoutModel.displayContainerRef]
|
||||
[node, additionalProps, layoutModel.displayContainerRef]
|
||||
);
|
||||
|
||||
// Register the overlay node as a drop target
|
||||
@@ -366,27 +339,27 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
|
||||
drop(overlayRef);
|
||||
}, []);
|
||||
|
||||
return <div ref={overlayRef} className="overlay-node" id={layoutNode.id} style={additionalProps?.transform} />;
|
||||
};
|
||||
return <div ref={overlayRef} className="overlay-node" id={node.id} style={additionalProps?.transform} />;
|
||||
});
|
||||
|
||||
interface ResizeHandleWrapperProps {
|
||||
layoutModel: LayoutModel;
|
||||
}
|
||||
|
||||
const ResizeHandleWrapper = ({ layoutModel }: ResizeHandleWrapperProps) => {
|
||||
const ResizeHandleWrapper = memo(({ layoutModel }: ResizeHandleWrapperProps) => {
|
||||
const resizeHandles = useAtomValue(layoutModel.resizeHandles) as Atom<ResizeHandleProps>[];
|
||||
|
||||
return resizeHandles.map((resizeHandleAtom, i) => (
|
||||
<ResizeHandle key={`resize-handle-${i}`} layoutModel={layoutModel} resizeHandleAtom={resizeHandleAtom} />
|
||||
));
|
||||
};
|
||||
});
|
||||
|
||||
interface ResizeHandleComponentProps {
|
||||
resizeHandleAtom: Atom<ResizeHandleProps>;
|
||||
layoutModel: LayoutModel;
|
||||
}
|
||||
|
||||
const ResizeHandle = ({ resizeHandleAtom, layoutModel }: ResizeHandleComponentProps) => {
|
||||
const ResizeHandle = memo(({ resizeHandleAtom, layoutModel }: ResizeHandleComponentProps) => {
|
||||
const resizeHandleProps = useAtomValue(resizeHandleAtom);
|
||||
const resizeHandleRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -436,7 +409,7 @@ const ResizeHandle = ({ resizeHandleAtom, layoutModel }: ResizeHandleComponentPr
|
||||
<div className="line" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
interface PlaceholderProps {
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,6 @@ function getLayoutStateAtomFromTab(tabAtom: Atom<Tab>, get: Getter): WritableWav
|
||||
|
||||
export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayoutTreeStateAtom {
|
||||
if (layoutStateAtomMap.has(tabAtom)) {
|
||||
// console.log("found atom");
|
||||
return layoutStateAtomMap.get(tabAtom);
|
||||
}
|
||||
const generationAtom = atom(1);
|
||||
@@ -26,9 +25,9 @@ export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayo
|
||||
const stateAtom = getLayoutStateAtomFromTab(tabAtom, get);
|
||||
if (!stateAtom) return;
|
||||
const layoutStateData = get(stateAtom);
|
||||
// console.log("layoutStateData", layoutStateData);
|
||||
const layoutTreeState: LayoutTreeState = {
|
||||
rootNode: layoutStateData?.rootnode,
|
||||
focusedNodeId: layoutStateData?.focusednodeid,
|
||||
magnifiedNodeId: layoutStateData?.magnifiednodeid,
|
||||
generation: get(generationAtom),
|
||||
};
|
||||
@@ -37,12 +36,11 @@ export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayo
|
||||
(get, set, value) => {
|
||||
if (get(generationAtom) < value.generation) {
|
||||
const stateAtom = getLayoutStateAtomFromTab(tabAtom, get);
|
||||
// console.log("setting new atom val", value);
|
||||
if (!stateAtom) return;
|
||||
const waveObjVal = get(stateAtom);
|
||||
// console.log("waveObjVal", waveObjVal);
|
||||
waveObjVal.rootnode = value.rootNode;
|
||||
waveObjVal.magnifiednodeid = value.magnifiedNodeId;
|
||||
waveObjVal.focusednodeid = value.focusedNodeId;
|
||||
set(generationAtom, value.generation);
|
||||
set(stateAtom, waveObjVal);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { globalStore, WOS } from "@/app/store/global";
|
||||
import { atoms, globalStore, WOS } from "@/app/store/global";
|
||||
import useResizeObserver from "@react-hook/resize-observer";
|
||||
import { Atom, useAtomValue } from "jotai";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { withLayoutTreeStateAtomFromTab } from "./layoutAtom";
|
||||
import { LayoutModel } from "./layoutModel";
|
||||
import { LayoutNode, LayoutNodeAdditionalProps, TileLayoutContents } from "./types";
|
||||
import { LayoutNode, NodeModel, TileLayoutContents } from "./types";
|
||||
|
||||
const layoutModelMap: Map<string, LayoutModel> = new Map();
|
||||
|
||||
@@ -34,6 +34,11 @@ export function getLayoutModelForTabById(tabId: string) {
|
||||
return getLayoutModelForTab(tabAtom);
|
||||
}
|
||||
|
||||
export function getLayoutModelForActiveTab() {
|
||||
const tabId = globalStore.get(atoms.activeTabId);
|
||||
return getLayoutModelForTabById(tabId);
|
||||
}
|
||||
|
||||
export function deleteLayoutModelForTab(tabId: string) {
|
||||
if (layoutModelMap.has(tabId)) layoutModelMap.delete(tabId);
|
||||
}
|
||||
@@ -51,8 +56,6 @@ export function useTileLayout(tabAtom: Atom<Tab>, tileContent: TileLayoutContent
|
||||
return layoutModel;
|
||||
}
|
||||
|
||||
export function useLayoutNode(layoutModel: LayoutModel, layoutNode: LayoutNode): LayoutNodeAdditionalProps {
|
||||
const [addlPropsAtom] = useState(layoutModel.getNodeAdditionalPropertiesAtom(layoutNode.id));
|
||||
const addlProps = useAtomValue(addlPropsAtom);
|
||||
return addlProps;
|
||||
export function useNodeModel(layoutModel: LayoutModel, layoutNode: LayoutNode): NodeModel {
|
||||
return layoutModel.getNodeModel(layoutNode);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
LayoutTreeActionType,
|
||||
LayoutTreeComputeMoveNodeAction,
|
||||
LayoutTreeDeleteNodeAction,
|
||||
LayoutTreeFocusNodeAction,
|
||||
LayoutTreeInsertNodeAction,
|
||||
LayoutTreeInsertNodeAtIndexAction,
|
||||
LayoutTreeMagnifyNodeToggleAction,
|
||||
@@ -37,7 +38,6 @@ import {
|
||||
export function computeMoveNode(layoutState: LayoutTreeState, computeInsertAction: LayoutTreeComputeMoveNodeAction) {
|
||||
const rootNode = layoutState.rootNode;
|
||||
const { node, nodeToMove, direction } = computeInsertAction;
|
||||
// console.log("computeInsertOperation start", layoutState.rootNode, node, nodeToMove, direction);
|
||||
if (direction === undefined) {
|
||||
console.warn("No direction provided for insertItemInDirection");
|
||||
return;
|
||||
@@ -179,14 +179,12 @@ export function computeMoveNode(layoutState: LayoutTreeState, computeInsertActio
|
||||
}
|
||||
break;
|
||||
case DropDirection.Center:
|
||||
// console.log("center drop", rootNode, node, nodeToMove);
|
||||
if (node.id !== rootNode.id && nodeToMove.id !== rootNode.id) {
|
||||
const swapAction: LayoutTreeSwapNodeAction = {
|
||||
type: LayoutTreeActionType.Swap,
|
||||
node1Id: node.id,
|
||||
node2Id: nodeToMove.id,
|
||||
};
|
||||
// console.log("swapAction", swapAction);
|
||||
return swapAction;
|
||||
} else {
|
||||
console.warn("cannot swap");
|
||||
@@ -209,7 +207,6 @@ export function computeMoveNode(layoutState: LayoutTreeState, computeInsertActio
|
||||
|
||||
export function moveNode(layoutState: LayoutTreeState, action: LayoutTreeMoveNodeAction) {
|
||||
const rootNode = layoutState.rootNode;
|
||||
// console.log("moveNode", action, layoutState.rootNode);
|
||||
if (!action) {
|
||||
console.error("no move node action provided");
|
||||
return;
|
||||
@@ -223,8 +220,6 @@ export function moveNode(layoutState: LayoutTreeState, action: LayoutTreeMoveNod
|
||||
const parent = findNode(rootNode, action.parentId);
|
||||
const oldParent = findParent(rootNode, action.node.id);
|
||||
|
||||
// console.log(node, parent, oldParent);
|
||||
|
||||
let startingIndex = 0;
|
||||
|
||||
// If moving under the same parent, we need to make sure that we are removing the child from its old position, not its new one.
|
||||
@@ -256,6 +251,7 @@ export function moveNode(layoutState: LayoutTreeState, action: LayoutTreeMoveNod
|
||||
if (oldParent) {
|
||||
removeChild(oldParent, node, startingIndex);
|
||||
}
|
||||
layoutState.generation++;
|
||||
}
|
||||
|
||||
export function insertNode(layoutState: LayoutTreeState, action: LayoutTreeInsertNodeAction) {
|
||||
@@ -265,13 +261,15 @@ export function insertNode(layoutState: LayoutTreeState, action: LayoutTreeInser
|
||||
}
|
||||
if (!layoutState.rootNode) {
|
||||
layoutState.rootNode = action.node;
|
||||
return;
|
||||
}
|
||||
const insertLoc = findNextInsertLocation(layoutState.rootNode, 5);
|
||||
addChildAt(insertLoc.node, insertLoc.index, action.node);
|
||||
if (action.magnified) {
|
||||
layoutState.magnifiedNodeId = action.node.id;
|
||||
} else {
|
||||
const insertLoc = findNextInsertLocation(layoutState.rootNode, 5);
|
||||
addChildAt(insertLoc.node, insertLoc.index, action.node);
|
||||
if (action.magnified) {
|
||||
layoutState.magnifiedNodeId = action.node.id;
|
||||
}
|
||||
layoutState.focusedNodeId = action.node.id;
|
||||
}
|
||||
layoutState.generation++;
|
||||
}
|
||||
|
||||
export function insertNodeAtIndex(layoutState: LayoutTreeState, action: LayoutTreeInsertNodeAtIndexAction) {
|
||||
@@ -281,22 +279,22 @@ export function insertNodeAtIndex(layoutState: LayoutTreeState, action: LayoutTr
|
||||
}
|
||||
if (!layoutState.rootNode) {
|
||||
layoutState.rootNode = action.node;
|
||||
return;
|
||||
}
|
||||
const insertLoc = findInsertLocationFromIndexArr(layoutState.rootNode, action.indexArr);
|
||||
if (!insertLoc) {
|
||||
console.error("insertNodeAtIndex unable to find insert location");
|
||||
return;
|
||||
}
|
||||
addChildAt(insertLoc.node, insertLoc.index + 1, action.node);
|
||||
if (action.magnified) {
|
||||
layoutState.magnifiedNodeId = action.node.id;
|
||||
} else {
|
||||
const insertLoc = findInsertLocationFromIndexArr(layoutState.rootNode, action.indexArr);
|
||||
if (!insertLoc) {
|
||||
console.error("insertNodeAtIndex unable to find insert location");
|
||||
return;
|
||||
}
|
||||
addChildAt(insertLoc.node, insertLoc.index + 1, action.node);
|
||||
if (action.magnified) {
|
||||
layoutState.magnifiedNodeId = action.node.id;
|
||||
}
|
||||
layoutState.focusedNodeId = action.node.id;
|
||||
}
|
||||
layoutState.generation++;
|
||||
}
|
||||
|
||||
export function swapNode(layoutState: LayoutTreeState, action: LayoutTreeSwapNodeAction) {
|
||||
// console.log("swapNode", layoutState, action);
|
||||
|
||||
if (!action.node1Id || !action.node2Id) {
|
||||
console.error("invalid swapNode action, both node1 and node2 must be defined");
|
||||
return;
|
||||
@@ -325,10 +323,10 @@ export function swapNode(layoutState: LayoutTreeState, action: LayoutTreeSwapNod
|
||||
|
||||
parentNode1.children[parentNode1Index] = node2;
|
||||
parentNode2.children[parentNode2Index] = node1;
|
||||
layoutState.generation++;
|
||||
}
|
||||
|
||||
export function deleteNode(layoutState: LayoutTreeState, action: LayoutTreeDeleteNodeAction) {
|
||||
// console.log("deleteNode", layoutState, action);
|
||||
if (!action?.nodeId) {
|
||||
console.error("no delete node action provided");
|
||||
return;
|
||||
@@ -339,20 +337,23 @@ export function deleteNode(layoutState: LayoutTreeState, action: LayoutTreeDelet
|
||||
}
|
||||
if (layoutState.rootNode.id === action.nodeId) {
|
||||
layoutState.rootNode = undefined;
|
||||
return;
|
||||
}
|
||||
const parent = findParent(layoutState.rootNode, action.nodeId);
|
||||
if (parent) {
|
||||
const node = parent.children.find((child) => child.id === action.nodeId);
|
||||
removeChild(parent, node);
|
||||
// console.log("node deleted", parent, node);
|
||||
} else {
|
||||
console.error("unable to delete node, not found in tree");
|
||||
const parent = findParent(layoutState.rootNode, action.nodeId);
|
||||
if (parent) {
|
||||
const node = parent.children.find((child) => child.id === action.nodeId);
|
||||
removeChild(parent, node);
|
||||
if (layoutState.focusedNodeId === node.id) {
|
||||
layoutState.focusedNodeId = undefined;
|
||||
}
|
||||
} else {
|
||||
console.error("unable to delete node, not found in tree");
|
||||
}
|
||||
}
|
||||
|
||||
layoutState.generation++;
|
||||
}
|
||||
|
||||
export function resizeNode(layoutState: LayoutTreeState, action: LayoutTreeResizeNodeAction) {
|
||||
// console.log("resizeNode", layoutState, action);
|
||||
if (!action.resizeOperations) {
|
||||
console.error("invalid resizeNode operation. nodeSizes array must be defined.");
|
||||
}
|
||||
@@ -364,10 +365,20 @@ export function resizeNode(layoutState: LayoutTreeState, action: LayoutTreeResiz
|
||||
const node = findNode(layoutState.rootNode, resize.nodeId);
|
||||
node.size = resize.size;
|
||||
}
|
||||
layoutState.generation++;
|
||||
}
|
||||
|
||||
export function focusNode(layoutState: LayoutTreeState, action: LayoutTreeFocusNodeAction) {
|
||||
if (!action.nodeId) {
|
||||
console.error("invalid focusNode operation, nodeId must be defined.");
|
||||
return;
|
||||
}
|
||||
|
||||
layoutState.focusedNodeId = action.nodeId;
|
||||
layoutState.generation++;
|
||||
}
|
||||
|
||||
export function magnifyNodeToggle(layoutState: LayoutTreeState, action: LayoutTreeMagnifyNodeToggleAction) {
|
||||
// console.log("magnifyNodeToggle", layoutState, action);
|
||||
if (!action.nodeId) {
|
||||
console.error("invalid magnifyNodeToggle operation. nodeId must be defined.");
|
||||
return;
|
||||
@@ -380,5 +391,7 @@ export function magnifyNodeToggle(layoutState: LayoutTreeState, action: LayoutTr
|
||||
layoutState.magnifiedNodeId = undefined;
|
||||
} else {
|
||||
layoutState.magnifiedNodeId = action.nodeId;
|
||||
layoutState.focusedNodeId = action.nodeId;
|
||||
}
|
||||
layoutState.generation++;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WritableAtom } from "jotai";
|
||||
import { Atom, WritableAtom } from "jotai";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export enum NavigateDirection {
|
||||
Top = 0,
|
||||
Up = 0,
|
||||
Right = 1,
|
||||
Bottom = 2,
|
||||
Down = 2,
|
||||
Left = 3,
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ export enum LayoutTreeActionType {
|
||||
InsertNode = "insert",
|
||||
InsertNodeAtIndex = "insertatindex",
|
||||
DeleteNode = "delete",
|
||||
FocusNode = "focus",
|
||||
MagnifyNodeToggle = "magnify",
|
||||
}
|
||||
|
||||
@@ -207,6 +208,18 @@ export interface LayoutTreeResizeNodeAction extends LayoutTreeAction {
|
||||
resizeOperations: ResizeNodeOperation[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Action for focusing a node from the layout tree.
|
||||
*/
|
||||
export interface LayoutTreeFocusNodeAction extends LayoutTreeAction {
|
||||
type: LayoutTreeActionType.FocusNode;
|
||||
|
||||
/**
|
||||
* The id of the node to focus;
|
||||
*/
|
||||
nodeId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action for toggling magnification of a node from the layout tree.
|
||||
*/
|
||||
@@ -234,23 +247,16 @@ export type LayoutTreeStateSetter = (value: LayoutState) => void;
|
||||
|
||||
export type LayoutTreeState = {
|
||||
rootNode: LayoutNode;
|
||||
focusedNodeId?: string;
|
||||
magnifiedNodeId?: string;
|
||||
generation: number;
|
||||
};
|
||||
|
||||
export type WritableLayoutTreeStateAtom = WritableAtom<LayoutTreeState, [value: LayoutTreeState], void>;
|
||||
|
||||
export type ContentRenderer = (
|
||||
data: TabLayoutData,
|
||||
ready: boolean,
|
||||
isMagnified: boolean,
|
||||
disablePointerEvents: boolean,
|
||||
onMagnifyToggle: () => void,
|
||||
onClose: () => void,
|
||||
dragHandleRef: React.RefObject<HTMLDivElement>
|
||||
) => React.ReactNode;
|
||||
export type ContentRenderer = (nodeModel: NodeModel) => React.ReactNode;
|
||||
|
||||
export type PreviewRenderer = (data: TabLayoutData) => React.ReactElement;
|
||||
export type PreviewRenderer = (nodeModel: NodeModel) => React.ReactElement;
|
||||
|
||||
export const DefaultNodeSize = 10;
|
||||
|
||||
@@ -307,3 +313,18 @@ export interface LayoutNodeAdditionalProps {
|
||||
isMagnifiedNode?: boolean;
|
||||
isLastMagnifiedNode?: boolean;
|
||||
}
|
||||
|
||||
export interface NodeModel {
|
||||
additionalProps: Atom<LayoutNodeAdditionalProps>;
|
||||
blockNum: Atom<number>;
|
||||
nodeId: string;
|
||||
blockId: string;
|
||||
isFocused: Atom<boolean>;
|
||||
isMagnified: Atom<boolean>;
|
||||
ready: Atom<boolean>;
|
||||
disablePointerEvents: Atom<boolean>;
|
||||
toggleMagnify: () => void;
|
||||
focusNode: () => void;
|
||||
onClose: () => void;
|
||||
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { CSSProperties } from "react";
|
||||
import { XYCoord } from "react-dnd";
|
||||
import { DropDirection, FlexDirection } from "./types";
|
||||
import { DropDirection, FlexDirection, NavigateDirection } from "./types";
|
||||
|
||||
export function reverseFlexDirection(flexDirection: FlexDirection): FlexDirection {
|
||||
return flexDirection === FlexDirection.Row ? FlexDirection.Column : FlexDirection.Row;
|
||||
@@ -82,3 +82,23 @@ export function setTransform(
|
||||
position: "absolute",
|
||||
};
|
||||
}
|
||||
|
||||
export function getCenter(dimensions: Dimensions): Point {
|
||||
return {
|
||||
x: dimensions.left + dimensions.width / 2,
|
||||
y: dimensions.top + dimensions.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function navigateDirectionToOffset(direction: NavigateDirection): Point {
|
||||
switch (direction) {
|
||||
case NavigateDirection.Up:
|
||||
return { x: 0, y: -1 };
|
||||
case NavigateDirection.Down:
|
||||
return { x: 0, y: 1 };
|
||||
case NavigateDirection.Left:
|
||||
return { x: -1, y: 0 };
|
||||
case NavigateDirection.Right:
|
||||
return { x: 1, y: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user