mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
Add ephemeral block support (#1275)
Ephemeral blocks can now be added to the LayoutModel for a tab. Only one ephemeral block can exist at a time. It is placed above all other blocks, including the magnified blocks. Updates how magnified and ephemeral blocks overlay the other blocks. Now, there's a blurred backdrop behind them that will obscure the other blocks. As a result of this, the overlayed blocks are now translucent.
This commit is contained in:
parent
31d0aa114d
commit
3fcf209b52
@ -61,7 +61,7 @@
|
||||
}
|
||||
|
||||
&.block-preview.block-frame-default .block-frame-default-inner .block-frame-default-header {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
background-color: rgb(from var(--block-bg-color) r g b / 70%);
|
||||
}
|
||||
|
||||
&.block-frame-default {
|
||||
@ -254,7 +254,7 @@
|
||||
}
|
||||
|
||||
.block-frame-preview {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
background-color: rgb(from var(--block-bg-color) r g b / 70%);
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
border-bottom-left-radius: var(--block-border-radius);
|
||||
@ -271,6 +271,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.magnified,
|
||||
&.ephemeral {
|
||||
background-color: rgb(from var(--block-bg-color) r g b / 60%);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.connstatus-overlay {
|
||||
position: absolute;
|
||||
top: calc(var(--header-height) + 6px);
|
||||
@ -385,7 +391,7 @@
|
||||
|
||||
&.show-block-mask .block-mask-inner {
|
||||
margin-top: var(--header-height); // TODO fix this magic
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
background-color: rgb(from var(--block-bg-color) r g b / 50%);
|
||||
height: calc(100% - var(--header-height));
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
@ -120,6 +120,7 @@ function computeEndIcons(
|
||||
const endIconsElem: JSX.Element[] = [];
|
||||
const endIconButtons = util.useAtomValueSafe(viewModel?.endIconButtons);
|
||||
const magnified = jotai.useAtomValue(nodeModel.isMagnified);
|
||||
const ephemeral = jotai.useAtomValue(nodeModel.isEphemeral);
|
||||
const numLeafs = jotai.useAtomValue(nodeModel.numLeafs);
|
||||
const magnifyDisabled = numLeafs <= 1;
|
||||
|
||||
@ -133,14 +134,27 @@ function computeEndIcons(
|
||||
click: onContextMenu,
|
||||
};
|
||||
endIconsElem.push(<IconButton key="settings" decl={settingsDecl} className="block-frame-settings" />);
|
||||
endIconsElem.push(
|
||||
<OptMagnifyButton
|
||||
key="unmagnify"
|
||||
magnified={magnified}
|
||||
toggleMagnify={nodeModel.toggleMagnify}
|
||||
disabled={magnifyDisabled}
|
||||
/>
|
||||
);
|
||||
if (ephemeral) {
|
||||
const addToLayoutDecl: IconButtonDecl = {
|
||||
elemtype: "iconbutton",
|
||||
icon: "circle-plus",
|
||||
title: "Add to Layout",
|
||||
click: () => {
|
||||
nodeModel.addEphemeralNodeToLayout();
|
||||
},
|
||||
};
|
||||
endIconsElem.push(<IconButton key="add-to-layout" decl={addToLayoutDecl} />);
|
||||
} else {
|
||||
endIconsElem.push(
|
||||
<OptMagnifyButton
|
||||
key="unmagnify"
|
||||
magnified={magnified}
|
||||
toggleMagnify={nodeModel.toggleMagnify}
|
||||
disabled={magnifyDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const closeDecl: IconButtonDecl = {
|
||||
elemtype: "iconbutton",
|
||||
icon: "xmark-large",
|
||||
@ -166,6 +180,7 @@ const BlockFrame_Header = ({
|
||||
const preIconButton = util.useAtomValueSafe(viewModel?.preIconButton);
|
||||
let headerTextUnion = util.useAtomValueSafe(viewModel?.viewText);
|
||||
const magnified = jotai.useAtomValue(nodeModel.isMagnified);
|
||||
const ephemeral = jotai.useAtomValue(nodeModel.isEphemeral);
|
||||
const manageConnection = util.useAtomValueSafe(viewModel?.manageConnection);
|
||||
const dragHandleRef = preview ? null : nodeModel.dragHandleRef;
|
||||
|
||||
@ -221,7 +236,12 @@ const BlockFrame_Header = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block-frame-default-header" ref={dragHandleRef} onContextMenu={onContextMenu}>
|
||||
<div
|
||||
className="block-frame-default-header"
|
||||
ref={dragHandleRef}
|
||||
onContextMenu={onContextMenu}
|
||||
draggable={!(preview || magnified || ephemeral)}
|
||||
>
|
||||
{preIconButtonElem}
|
||||
<div className="block-frame-default-header-iconview">
|
||||
{viewIconElem}
|
||||
@ -309,7 +329,6 @@ const ConnStatusOverlay = React.memo(
|
||||
const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30);
|
||||
const width = domRect?.width;
|
||||
const [showError, setShowError] = React.useState(false);
|
||||
const blockNum = jotai.useAtomValue(nodeModel.blockNum);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (width) {
|
||||
@ -421,6 +440,8 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
||||
return jotai.atom(false);
|
||||
}) as jotai.PrimitiveAtom<boolean>;
|
||||
const connModalOpen = jotai.useAtomValue(changeConnModalAtom);
|
||||
const isMagnified = jotai.useAtomValue(nodeModel.isMagnified);
|
||||
const isEphemeral = jotai.useAtomValue(nodeModel.isEphemeral);
|
||||
|
||||
const connBtnRef = React.useRef<HTMLDivElement>();
|
||||
React.useEffect(() => {
|
||||
@ -476,6 +497,8 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
|
||||
"block-focused": isFocused || preview,
|
||||
"block-preview": preview,
|
||||
"block-no-highlight": numBlocksInTab === 1,
|
||||
ephemeral: isEphemeral,
|
||||
magnified: isMagnified,
|
||||
})}
|
||||
data-blockid={nodeModel.blockId}
|
||||
onClick={blockModel?.onClick}
|
||||
|
@ -341,17 +341,21 @@ function getApi(): ElectronApi {
|
||||
return (window as any).api;
|
||||
}
|
||||
|
||||
async function createBlock(blockDef: BlockDef, magnified = false): Promise<string> {
|
||||
async function createBlock(blockDef: BlockDef, magnified = false, ephemeral = false): Promise<string> {
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
const layoutModel = getLayoutModelForTabById(tabId);
|
||||
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
|
||||
const blockId = await ObjectService.CreateBlock(blockDef, rtOpts);
|
||||
if (ephemeral) {
|
||||
layoutModel.newEphemeralNode(blockId);
|
||||
return blockId;
|
||||
}
|
||||
const insertNodeAction: LayoutTreeInsertNodeAction = {
|
||||
type: LayoutTreeActionType.InsertNode,
|
||||
node: newLayoutNode(undefined, undefined, undefined, { blockId }),
|
||||
magnified,
|
||||
focused: true,
|
||||
};
|
||||
const tabId = globalStore.get(atoms.staticTabId);
|
||||
const layoutModel = getLayoutModelForTabById(tabId);
|
||||
layoutModel.treeReducer(insertNodeAction);
|
||||
return blockId;
|
||||
}
|
||||
|
@ -56,10 +56,14 @@
|
||||
--zindex-tab-name: 3;
|
||||
--zindex-layout-display-container: 0;
|
||||
--zindex-layout-last-magnified-node: 1;
|
||||
--zindex-layout-resize-handle: 2;
|
||||
--zindex-layout-placeholder-container: 3;
|
||||
--zindex-layout-overlay-container: 4;
|
||||
--zindex-layout-magnified-node: 5;
|
||||
--zindex-layout-last-ephemeral-node: 2;
|
||||
--zindex-layout-resize-handle: 3;
|
||||
--zindex-layout-placeholder-container: 4;
|
||||
--zindex-layout-overlay-container: 5;
|
||||
--zindex-layout-magnified-node-backdrop: 6;
|
||||
--zindex-layout-magnified-node: 7;
|
||||
--zindex-layout-ephemeral-node-backdrop: 8;
|
||||
--zindex-layout-ephemeral-node: 9;
|
||||
--zindex-block-mask-inner: 10;
|
||||
--zindex-flash-error-container: 550;
|
||||
--zindex-app-background: -1;
|
||||
|
@ -240,7 +240,7 @@ export class WaveAiModel implements ViewModel {
|
||||
file: path,
|
||||
},
|
||||
};
|
||||
await createBlock(blockDef, true);
|
||||
await createBlock(blockDef, false, true);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -57,6 +57,7 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
|
||||
const setActiveDrag = useSetAtom(layoutModel.activeDrag);
|
||||
const setReady = useSetAtom(layoutModel.ready);
|
||||
const isResizing = useAtomValue(layoutModel.isResizing);
|
||||
const ephemeralNode = useAtomValue(layoutModel.ephemeralNode);
|
||||
|
||||
const { activeDrag, dragClientOffset } = useDragLayer((monitor) => ({
|
||||
activeDrag: monitor.isDragging(),
|
||||
@ -121,6 +122,7 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
|
||||
<div key="display" ref={layoutModel.displayContainerRef} className="display-container">
|
||||
<ResizeHandleWrapper layoutModel={layoutModel} />
|
||||
<DisplayNodesWrapper layoutModel={layoutModel} />
|
||||
<NodeBackdrops layoutModel={layoutModel} />
|
||||
</div>
|
||||
<Placeholder key="placeholder" layoutModel={layoutModel} style={{ top: 10000, ...overlayTransform }} />
|
||||
<OverlayNodeWrapper layoutModel={layoutModel} />
|
||||
@ -130,6 +132,55 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr
|
||||
}
|
||||
export const TileLayout = memo(TileLayoutComponent) as typeof TileLayoutComponent;
|
||||
|
||||
function NodeBackdrops({ layoutModel }: { layoutModel: LayoutModel }) {
|
||||
const ephemeralNode = useAtomValue(layoutModel.ephemeralNode);
|
||||
const magnifiedNodeId = useAtomValue(layoutModel.treeStateAtom).magnifiedNodeId;
|
||||
|
||||
const [showMagnifiedBackdrop, setShowMagnifiedBackdrop] = useState(!!ephemeralNode);
|
||||
const [showEphemeralBackdrop, setShowEphemeralBackdrop] = useState(!!magnifiedNodeId);
|
||||
|
||||
const debouncedCallback = useCallback(
|
||||
debounce(100, (callback: () => void) => callback()),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (magnifiedNodeId && !showMagnifiedBackdrop) {
|
||||
debouncedCallback(() => setShowMagnifiedBackdrop(true));
|
||||
}
|
||||
if (!magnifiedNodeId) {
|
||||
setShowMagnifiedBackdrop(false);
|
||||
}
|
||||
if (ephemeralNode && !showEphemeralBackdrop) {
|
||||
debouncedCallback(() => setShowEphemeralBackdrop(true));
|
||||
}
|
||||
if (!ephemeralNode) {
|
||||
setShowEphemeralBackdrop(false);
|
||||
}
|
||||
}, [ephemeralNode, magnifiedNodeId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showMagnifiedBackdrop && (
|
||||
<div
|
||||
className="magnified-node-backdrop"
|
||||
onClick={() => {
|
||||
layoutModel.magnifyNodeToggle(magnifiedNodeId);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showEphemeralBackdrop && (
|
||||
<div
|
||||
className="ephemeral-node-backdrop"
|
||||
onClick={() => {
|
||||
layoutModel.closeNode(ephemeralNode?.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface DisplayNodesWrapperProps {
|
||||
/**
|
||||
* The layout tree state.
|
||||
@ -173,7 +224,6 @@ const DisplayNode = ({ layoutModel, node }: DisplayNodeProps) => {
|
||||
() => ({
|
||||
type: dragItemType,
|
||||
item: () => node,
|
||||
canDrag: () => !addlProps?.isMagnifiedNode,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
@ -243,8 +293,6 @@ const DisplayNode = ({ layoutModel, node }: DisplayNodeProps) => {
|
||||
<div
|
||||
className={clsx("tile-node", {
|
||||
dragging: isDragging,
|
||||
magnified: addlProps?.isMagnifiedNode,
|
||||
"last-magnified": addlProps?.isLastMagnifiedNode,
|
||||
})}
|
||||
key={node.id}
|
||||
ref={tileNodeRef}
|
||||
|
@ -171,6 +171,8 @@ export class LayoutModel {
|
||||
* Atom pointing to the currently focused node.
|
||||
*/
|
||||
focusedNode: Atom<LayoutNode>;
|
||||
|
||||
// TODO: Nodes that need to be placed at higher z-indices should probably be handled by an ordered list, rather than individual properties.
|
||||
/**
|
||||
* The currently magnified node.
|
||||
*/
|
||||
@ -179,6 +181,14 @@ export class LayoutModel {
|
||||
* The last node to be magnified, other than the current magnified node, if set. This node should sit at a higher z-index than the others so that it floats above the other nodes as it returns to its original position.
|
||||
*/
|
||||
lastMagnifiedNodeId: string;
|
||||
/**
|
||||
* Atom holding an ephemeral node that is not part of the layout tree. This node displays above all other nodes.
|
||||
*/
|
||||
ephemeralNode: PrimitiveAtom<LayoutNode>;
|
||||
/**
|
||||
* The last node to be an ephemeral node. This node should sit at a higher z-index than the others so that it floats above the other nodes as it returns to its original position.
|
||||
*/
|
||||
lastEphemeralNodeId: string;
|
||||
|
||||
/**
|
||||
* The size of the resize handles, in CSS pixels.
|
||||
@ -267,6 +277,8 @@ export class LayoutModel {
|
||||
}
|
||||
});
|
||||
|
||||
this.ephemeralNode = atom();
|
||||
|
||||
this.focusedNode = atom((get) => {
|
||||
const treeState = get(this.treeStateAtom);
|
||||
if (treeState.focusedNodeId == null) {
|
||||
@ -366,6 +378,7 @@ export class LayoutModel {
|
||||
if (this.lastTreeStateGeneration < this.treeState.generation) {
|
||||
if (this.magnifiedNodeId !== this.treeState.magnifiedNodeId) {
|
||||
this.lastMagnifiedNodeId = this.magnifiedNodeId;
|
||||
this.lastEphemeralNodeId = undefined;
|
||||
this.magnifiedNodeId = this.treeState.magnifiedNodeId;
|
||||
}
|
||||
this.updateTree();
|
||||
@ -486,11 +499,28 @@ export class LayoutModel {
|
||||
? (pendingAction as LayoutTreeResizeNodeAction)
|
||||
: null;
|
||||
const resizeHandleSizePx = this.getter(this.resizeHandleSizePx);
|
||||
|
||||
const boundingRect = this.getBoundingRect();
|
||||
|
||||
const callback = (node: LayoutNode) =>
|
||||
this.updateTreeHelper(node, newAdditionalProps, newLeafs, resizeHandleSizePx, resizeAction);
|
||||
this.updateTreeHelper(
|
||||
node,
|
||||
newAdditionalProps,
|
||||
newLeafs,
|
||||
resizeHandleSizePx,
|
||||
boundingRect,
|
||||
resizeAction
|
||||
);
|
||||
if (balanceTree) this.treeState.rootNode = balanceNode(this.treeState.rootNode, callback);
|
||||
else walkNodes(this.treeState.rootNode, callback);
|
||||
|
||||
// Process ephemeral node, if present.
|
||||
const ephemeralNode = this.getter(this.ephemeralNode);
|
||||
if (ephemeralNode) {
|
||||
console.log("updateTree ephemeralNode", ephemeralNode);
|
||||
this.updateEphemeralNodeProps(ephemeralNode, newAdditionalProps, newLeafs, boundingRect);
|
||||
}
|
||||
|
||||
this.treeState.leafOrder = getLeafOrder(newLeafs, newAdditionalProps);
|
||||
this.validateFocusedNode(this.treeState.leafOrder);
|
||||
this.validateMagnifiedNode(this.treeState.leafOrder, newAdditionalProps);
|
||||
@ -516,23 +546,14 @@ export class LayoutModel {
|
||||
additionalPropsMap: Record<string, LayoutNodeAdditionalProps>,
|
||||
leafs: LayoutNode[],
|
||||
resizeHandleSizePx: number,
|
||||
boundingRect: Dimensions,
|
||||
resizeAction?: LayoutTreeResizeNodeAction
|
||||
) {
|
||||
/**
|
||||
* Gets normalized dimensions for the TileLayout container.
|
||||
* @returns The normalized dimensions for the TileLayout container.
|
||||
*/
|
||||
const getBoundingRect: () => Dimensions = () => {
|
||||
const boundingRect = this.displayContainerRef.current.getBoundingClientRect();
|
||||
return { top: 0, left: 0, width: boundingRect.width, height: boundingRect.height };
|
||||
};
|
||||
|
||||
if (!node.children?.length) {
|
||||
leafs.push(node);
|
||||
const addlProps = additionalPropsMap[node.id];
|
||||
if (addlProps) {
|
||||
if (this.magnifiedNodeId === node.id) {
|
||||
const boundingRect = getBoundingRect();
|
||||
const transform = setTransform(
|
||||
{
|
||||
top: boundingRect.height * 0.05,
|
||||
@ -540,12 +561,17 @@ export class LayoutModel {
|
||||
width: boundingRect.width * 0.9,
|
||||
height: boundingRect.height * 0.9,
|
||||
},
|
||||
true
|
||||
true,
|
||||
true,
|
||||
"var(--zindex-layout-magnified-node)"
|
||||
);
|
||||
addlProps.transform = transform;
|
||||
addlProps.isMagnifiedNode = true;
|
||||
}
|
||||
addlProps.isLastMagnifiedNode = this.lastMagnifiedNodeId === node.id;
|
||||
if (this.lastMagnifiedNodeId === node.id) {
|
||||
addlProps.transform.zIndex = "var(--zindex-layout-last-magnified-node)";
|
||||
} else if (this.lastEphemeralNodeId === node.id) {
|
||||
addlProps.transform.zIndex = "var(--zindex-layout-last-ephemeral-node)";
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -558,7 +584,7 @@ export class LayoutModel {
|
||||
? additionalPropsMap[node.id]
|
||||
: { treeKey: "0" };
|
||||
|
||||
const nodeRect: Dimensions = node.id === this.treeState.rootNode.id ? getBoundingRect() : additionalProps.rect;
|
||||
const nodeRect: Dimensions = node.id === this.treeState.rootNode.id ? boundingRect : additionalProps.rect;
|
||||
const nodeIsRow = node.flexDirection === FlexDirection.Row;
|
||||
const nodePixels = nodeIsRow ? nodeRect.width : nodeRect.height;
|
||||
const totalChildrenSize = node.children.reduce((acc, child) => acc + getNodeSize(child), 0);
|
||||
@ -615,6 +641,15 @@ export class LayoutModel {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets normalized dimensions for the TileLayout container.
|
||||
* @returns The normalized dimensions for the TileLayout container.
|
||||
*/
|
||||
getBoundingRect: () => Dimensions = () => {
|
||||
const boundingRect = this.displayContainerRef.current.getBoundingClientRect();
|
||||
return { top: 0, left: 0, width: boundingRect.width, height: boundingRect.height };
|
||||
};
|
||||
|
||||
/**
|
||||
* The id of the focused node in the layout.
|
||||
*/
|
||||
@ -627,6 +662,7 @@ export class LayoutModel {
|
||||
* @param leafOrder The new leaf order array to use when searching for stale nodes in the stack.
|
||||
*/
|
||||
private validateFocusedNode(leafOrder: LeafOrderEntry[]) {
|
||||
console.log("validateFocusedNode", this.treeState.focusedNodeId, this.focusedNodeId, this.focusedNodeIdStack);
|
||||
if (this.treeState.focusedNodeId !== this.focusedNodeId) {
|
||||
// Remove duplicates and stale entries from focus stack.
|
||||
const newFocusedNodeIdStack: string[] = [];
|
||||
@ -794,6 +830,11 @@ export class LayoutModel {
|
||||
const treeState = get(this.treeStateAtom);
|
||||
return treeState.magnifiedNodeId === nodeid;
|
||||
}),
|
||||
isEphemeral: atom((get) => {
|
||||
const ephemeralNode = get(this.ephemeralNode);
|
||||
return ephemeralNode?.id === nodeid;
|
||||
}),
|
||||
addEphemeralNodeToLayout: () => this.addEphemeralNodeToLayout(),
|
||||
animationTimeS: this.animationTimeS,
|
||||
ready: this.ready,
|
||||
disablePointerEvents: this.activeDrag,
|
||||
@ -907,10 +948,15 @@ export class LayoutModel {
|
||||
*/
|
||||
focusNode(nodeId: string) {
|
||||
if (this.focusedNodeId === nodeId) return;
|
||||
const layoutNode = findNode(this.treeState?.rootNode, nodeId);
|
||||
let layoutNode = findNode(this.treeState?.rootNode, nodeId);
|
||||
if (!layoutNode) {
|
||||
console.error("unable to focus node, cannot find it in tree", nodeId);
|
||||
return;
|
||||
const ephemeralNode = this.getter(this.ephemeralNode);
|
||||
if (ephemeralNode?.id === nodeId) {
|
||||
layoutNode = ephemeralNode;
|
||||
} else {
|
||||
console.error("unable to focus node, cannot find it in tree", nodeId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const action: LayoutTreeFocusNodeAction = {
|
||||
type: LayoutTreeActionType.FocusNode,
|
||||
@ -931,13 +977,16 @@ export class LayoutModel {
|
||||
* Toggle magnification of a given node.
|
||||
* @param nodeId The id of the node that is being magnified.
|
||||
*/
|
||||
magnifyNodeToggle(nodeId: string) {
|
||||
magnifyNodeToggle(nodeId: string, setState = true) {
|
||||
const action: LayoutTreeMagnifyNodeToggleAction = {
|
||||
type: LayoutTreeActionType.MagnifyNodeToggle,
|
||||
nodeId: nodeId,
|
||||
};
|
||||
|
||||
this.treeReducer(action);
|
||||
// Unset the last ephemeral node id to ensure the magnify animation sits on top of the layout.
|
||||
this.lastEphemeralNodeId = undefined;
|
||||
|
||||
this.treeReducer(action, setState);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -947,9 +996,23 @@ export class LayoutModel {
|
||||
async closeNode(nodeId: string) {
|
||||
const nodeToDelete = findNode(this.treeState.rootNode, nodeId);
|
||||
if (!nodeToDelete) {
|
||||
// TODO: clean up the ephemeral node handling
|
||||
// The ephemeral node is not in the tree, so we need to handle it separately.
|
||||
const ephemeralNode = this.getter(this.ephemeralNode);
|
||||
if (ephemeralNode?.id === nodeId) {
|
||||
this.setter(this.ephemeralNode, undefined);
|
||||
this.treeState.focusedNodeId = undefined;
|
||||
this.updateTree(false);
|
||||
this.setTreeStateAtom(true);
|
||||
await this.onNodeDelete?.(ephemeralNode.data);
|
||||
return;
|
||||
}
|
||||
console.error("unable to close node, cannot find it in tree", nodeId);
|
||||
return;
|
||||
}
|
||||
if (nodeId === this.magnifiedNodeId) {
|
||||
this.magnifyNodeToggle(nodeId);
|
||||
}
|
||||
const deleteAction: LayoutTreeDeleteNodeAction = {
|
||||
type: LayoutTreeActionType.DeleteNode,
|
||||
nodeId: nodeId,
|
||||
@ -965,6 +1028,61 @@ export class LayoutModel {
|
||||
await this.closeNode(this.focusedNodeId);
|
||||
}
|
||||
|
||||
newEphemeralNode(blockId: string) {
|
||||
if (this.getter(this.ephemeralNode)) {
|
||||
this.closeNode(this.getter(this.ephemeralNode).id);
|
||||
}
|
||||
|
||||
const ephemeralNode = newLayoutNode(undefined, undefined, undefined, { blockId });
|
||||
this.setter(this.ephemeralNode, ephemeralNode);
|
||||
|
||||
const addlProps = this.getter(this.additionalProps);
|
||||
const leafs = this.getter(this.leafs);
|
||||
const boundingRect = this.getBoundingRect();
|
||||
this.updateEphemeralNodeProps(ephemeralNode, addlProps, leafs, boundingRect);
|
||||
this.setter(this.additionalProps, addlProps);
|
||||
this.focusNode(ephemeralNode.id);
|
||||
}
|
||||
|
||||
addEphemeralNodeToLayout() {
|
||||
const ephemeralNode = this.getter(this.ephemeralNode);
|
||||
this.setter(this.ephemeralNode, undefined);
|
||||
if (this.magnifiedNodeId) {
|
||||
this.magnifyNodeToggle(this.magnifiedNodeId, false);
|
||||
}
|
||||
this.lastEphemeralNodeId = ephemeralNode.id;
|
||||
if (ephemeralNode) {
|
||||
const action: LayoutTreeInsertNodeAction = {
|
||||
type: LayoutTreeActionType.InsertNode,
|
||||
node: ephemeralNode,
|
||||
magnified: false,
|
||||
focused: false,
|
||||
};
|
||||
this.treeReducer(action);
|
||||
}
|
||||
}
|
||||
|
||||
updateEphemeralNodeProps(
|
||||
node: LayoutNode,
|
||||
addlPropsMap: Record<string, LayoutNodeAdditionalProps>,
|
||||
leafs: LayoutNode[],
|
||||
boundingRect: Dimensions
|
||||
) {
|
||||
const transform = setTransform(
|
||||
{
|
||||
top: boundingRect.height * 0.075,
|
||||
left: boundingRect.width * 0.075,
|
||||
width: boundingRect.width * 0.85,
|
||||
height: boundingRect.height * 0.85,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
"var(--zindex-layout-ephemeral-node)"
|
||||
);
|
||||
addlPropsMap[node.id] = { treeKey: "-1", transform };
|
||||
leafs.push(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback that is invoked when a drag operation completes and the pending action should be committed.
|
||||
*/
|
||||
|
@ -84,14 +84,6 @@
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
&.magnified {
|
||||
background-color: var(--block-bg-solid-color);
|
||||
z-index: var(--zindex-layout-magnified-node);
|
||||
}
|
||||
&.last-magnified {
|
||||
z-index: var(--zindex-layout-last-magnified-node);
|
||||
}
|
||||
|
||||
.tile-leaf {
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -109,11 +101,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:only-child, .magnified) .tile-leaf {
|
||||
&:not(:only-child) .tile-leaf {
|
||||
padding: calc(var(--gap-size-px) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.magnified-node-backdrop,
|
||||
.ephemeral-node-backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.magnified-node-backdrop {
|
||||
z-index: var(--zindex-layout-magnified-node-backdrop);
|
||||
}
|
||||
|
||||
.ephemeral-node-backdrop {
|
||||
z-index: var(--zindex-layout-ephemeral-node-backdrop);
|
||||
}
|
||||
|
||||
&.animate {
|
||||
.tile-node,
|
||||
.placeholder {
|
||||
|
@ -332,8 +332,7 @@ export interface LayoutNodeAdditionalProps {
|
||||
rect?: Dimensions;
|
||||
pixelToSizeRatio?: number;
|
||||
resizeHandles?: ResizeHandleProps[];
|
||||
isMagnifiedNode?: boolean;
|
||||
isLastMagnifiedNode?: boolean;
|
||||
isLastEphemeralNode?: boolean;
|
||||
}
|
||||
|
||||
export interface NodeModel {
|
||||
@ -343,10 +342,12 @@ export interface NodeModel {
|
||||
numLeafs: Atom<number>;
|
||||
nodeId: string;
|
||||
blockId: string;
|
||||
addEphemeralNodeToLayout: () => void;
|
||||
animationTimeS: Atom<number>;
|
||||
isResizing: Atom<boolean>;
|
||||
isFocused: Atom<boolean>;
|
||||
isMagnified: Atom<boolean>;
|
||||
isEphemeral: Atom<boolean>;
|
||||
ready: Atom<boolean>;
|
||||
disablePointerEvents: Atom<boolean>;
|
||||
toggleMagnify: () => void;
|
||||
|
@ -61,7 +61,8 @@ export function determineDropDirection(dimensions?: Dimensions, offset?: XYCoord
|
||||
export function setTransform(
|
||||
{ top, left, width, height }: Dimensions,
|
||||
setSize = true,
|
||||
roundVals = true
|
||||
roundVals = true,
|
||||
zIndex?: number | string
|
||||
): CSSProperties {
|
||||
// Replace unitless items with px
|
||||
const topRounded = roundVals ? Math.floor(top) : top;
|
||||
@ -80,6 +81,7 @@ export function setTransform(
|
||||
width: setSize ? `${widthRounded}px` : undefined,
|
||||
height: setSize ? `${heightRounded}px` : undefined,
|
||||
position: "absolute",
|
||||
zIndex: zIndex,
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user