mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
Add action for magnifying a block (#172)
Adds the implementation for the "Magnify Block" context menu item. This will pop a block out of the layout and bring it to the foreground. This also cleans up some block styling to make radii more consistent. <img width="814" alt="image" src="https://github.com/user-attachments/assets/c81521e1-c91f-4bb5-9eec-ff0eda178268">
This commit is contained in:
parent
49072364e9
commit
f3f272a47b
@ -11,7 +11,7 @@
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--block-border-radius);
|
||||
|
||||
.block-frame-icon {
|
||||
margin-right: 0.5em;
|
||||
@ -62,7 +62,7 @@
|
||||
background-color: var(--block-bg-color);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--block-border-radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -75,7 +75,7 @@
|
||||
align-self: stretch;
|
||||
font: var(--header-font);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px 8px 0 0;
|
||||
border-radius: var(--block-border-radius) var(--block-border-radius) 0 0;
|
||||
|
||||
.block-frame-preicon-button {
|
||||
opacity: 0.7;
|
||||
@ -251,8 +251,8 @@
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
border-bottom-left-radius: var(--block-border-radius);
|
||||
border-bottom-right-radius: var(--block-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,7 +265,7 @@
|
||||
border: 2px solid transparent;
|
||||
pointer-events: none;
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--block-border-radius);
|
||||
}
|
||||
|
||||
&.block-focused {
|
||||
|
@ -21,8 +21,9 @@ import * as React from "react";
|
||||
|
||||
import "./block.less";
|
||||
|
||||
interface LayoutComponentModel {
|
||||
export interface LayoutComponentModel {
|
||||
onClose?: () => void;
|
||||
onMagnifyToggle?: () => void;
|
||||
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
@ -163,51 +164,52 @@ function handleHeaderContextMenu(
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
blockData: Block,
|
||||
viewModel: ViewModel,
|
||||
onMagnifyToggle: () => void,
|
||||
onClose: () => void
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let menu: ContextMenuItem[] = [];
|
||||
menu.push({
|
||||
label: "Focus Block",
|
||||
click: () => {
|
||||
alert("Not Implemented");
|
||||
let menu: ContextMenuItem[] = [
|
||||
{
|
||||
label: "Magnify Block",
|
||||
click: () => {
|
||||
onMagnifyToggle();
|
||||
},
|
||||
},
|
||||
});
|
||||
menu.push({
|
||||
label: "Minimize",
|
||||
click: () => {
|
||||
alert("Not Implemented");
|
||||
{
|
||||
label: "Minimize",
|
||||
click: () => {
|
||||
alert("Not Implemented");
|
||||
},
|
||||
},
|
||||
});
|
||||
menu.push({
|
||||
label: "Move to New Window",
|
||||
click: () => {
|
||||
let currentTabId = globalStore.get(atoms.activeTabId);
|
||||
try {
|
||||
services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
|
||||
} catch (e) {
|
||||
console.error("error moving block to new window", e);
|
||||
}
|
||||
{
|
||||
label: "Move to New Window",
|
||||
click: () => {
|
||||
const currentTabId = globalStore.get(atoms.activeTabId);
|
||||
try {
|
||||
services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
|
||||
} catch (e) {
|
||||
console.error("error moving block to new window", e);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
menu.push({ type: "separator" });
|
||||
menu.push({
|
||||
label: "Copy BlockId",
|
||||
click: () => {
|
||||
navigator.clipboard.writeText(blockData.oid);
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Copy BlockId",
|
||||
click: () => {
|
||||
navigator.clipboard.writeText(blockData.oid);
|
||||
},
|
||||
},
|
||||
});
|
||||
];
|
||||
const extraItems = viewModel?.getSettingsMenuItems?.();
|
||||
if (extraItems && extraItems.length > 0) {
|
||||
menu.push({ type: "separator" });
|
||||
menu.push(...extraItems);
|
||||
}
|
||||
menu.push({ type: "separator" });
|
||||
menu.push({
|
||||
label: "Close Block",
|
||||
click: onClose,
|
||||
});
|
||||
if (extraItems && extraItems.length > 0) menu.push({ type: "separator" }, ...extraItems);
|
||||
menu.push(
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Close Block",
|
||||
click: onClose,
|
||||
}
|
||||
);
|
||||
ContextMenuModel.showContextMenu(menu, e);
|
||||
}
|
||||
|
||||
@ -294,7 +296,8 @@ const BlockFrame_Default_Component = ({
|
||||
elemtype: "iconbutton",
|
||||
icon: "cog",
|
||||
title: "Settings",
|
||||
click: (e) => handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onClose),
|
||||
click: (e) =>
|
||||
handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onMagnifyToggle, layoutModel?.onClose),
|
||||
};
|
||||
endIconsElem.push(
|
||||
<IconButton key="settings" decl={settingsDecl} className="block-frame-endicon-button block-frame-settings" />
|
||||
@ -394,7 +397,15 @@ const BlockFrame_Default_Component = ({
|
||||
<div
|
||||
className="block-frame-default-header"
|
||||
ref={layoutModel?.dragHandleRef}
|
||||
onContextMenu={(e) => handleHeaderContextMenu(e, blockData, viewModel, layoutModel?.onClose)}
|
||||
onContextMenu={(e) =>
|
||||
handleHeaderContextMenu(
|
||||
e,
|
||||
blockData,
|
||||
viewModel,
|
||||
layoutModel?.onMagnifyToggle,
|
||||
layoutModel?.onClose
|
||||
)
|
||||
}
|
||||
>
|
||||
{preIconButtonElem}
|
||||
<div className="block-frame-default-header-iconview">
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright 2023, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Block } from "@/app/block/block";
|
||||
import { Block, LayoutComponentModel } from "@/app/block/block";
|
||||
import { getApi } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
@ -26,14 +26,16 @@ const TabContent = React.memo(({ tabId }: { tabId: string }) => {
|
||||
function renderBlock(
|
||||
tabData: TabLayoutData,
|
||||
ready: boolean,
|
||||
onMagnifyToggle: () => void,
|
||||
onClose: () => void,
|
||||
dragHandleRef: React.RefObject<HTMLDivElement>
|
||||
) {
|
||||
if (!tabData.blockId || !ready) {
|
||||
return null;
|
||||
}
|
||||
const layoutModel = {
|
||||
const layoutModel: LayoutComponentModel = {
|
||||
onClose: onClose,
|
||||
onMagnifyToggle: onMagnifyToggle,
|
||||
dragHandleRef: dragHandleRef,
|
||||
};
|
||||
return <Block key={tabData.blockId} blockId={tabData.blockId} layoutModel={layoutModel} preview={false} />;
|
||||
|
@ -20,6 +20,8 @@
|
||||
--success-color: rgb(78, 154, 6);
|
||||
--hover-bg-color: rgba(255, 255, 255, 0.1);
|
||||
--block-bg-color: rgba(0, 0, 0, 0.5);
|
||||
--block-bg-solid-color: rgb(0, 0, 0);
|
||||
--block-border-radius: 8px;
|
||||
|
||||
/* scrollbar colors */
|
||||
--scrollbar-background-color: transparent;
|
||||
|
@ -40,7 +40,7 @@ import {
|
||||
} from "./model";
|
||||
import { NodeRefMap } from "./nodeRefMap";
|
||||
import "./tilelayout.less";
|
||||
import { Dimensions, FlexDirection, setTransform as createTransform, determineDropDirection } from "./utils";
|
||||
import { Dimensions, FlexDirection, determineDropDirection, setTransform } from "./utils";
|
||||
|
||||
/**
|
||||
* contains callbacks and information about the contents (or styling) of of the TileLayout
|
||||
@ -181,14 +181,28 @@ function TileLayoutComponent<T>({ layoutTreeStateAtom, contents, getCursorPoint
|
||||
const leafRef = nodeRefs.get(leaf.id);
|
||||
// console.log("current leafRef", leafRef.current);
|
||||
if (leafRef?.current) {
|
||||
if (leaf.id === layoutTreeState.magnifiedNodeId) {
|
||||
const transform = setTransform(
|
||||
{
|
||||
top: displayBoundingRect.height * 0.05,
|
||||
left: displayBoundingRect.width * 0.05,
|
||||
width: displayBoundingRect.width * 0.9,
|
||||
height: displayBoundingRect.height * 0.9,
|
||||
},
|
||||
true
|
||||
);
|
||||
newLayoutLeafTransforms[leaf.id] = transform;
|
||||
continue;
|
||||
}
|
||||
|
||||
const leafBounding = leafRef.current.getBoundingClientRect();
|
||||
const transform = createTransform({
|
||||
const transform = setTransform({
|
||||
top: leafBounding.top - overlayBoundingRect.top,
|
||||
left: leafBounding.left - overlayBoundingRect.left,
|
||||
width: leafBounding.width,
|
||||
height: leafBounding.height,
|
||||
});
|
||||
newLayoutLeafTransforms[leafRef.current.id] = transform;
|
||||
newLayoutLeafTransforms[leaf.id] = transform;
|
||||
} else {
|
||||
console.warn("missing leaf", leaf.id);
|
||||
}
|
||||
@ -199,7 +213,7 @@ function TileLayoutComponent<T>({ layoutTreeStateAtom, contents, getCursorPoint
|
||||
const newOverlayOffset = displayBoundingRect.top + 2 * displayBoundingRect.height;
|
||||
// console.log("overlayOffset", newOverlayOffset);
|
||||
setOverlayTransform(
|
||||
createTransform(
|
||||
setTransform(
|
||||
{
|
||||
top: activeDrag || showOverlay ? 0 : newOverlayOffset,
|
||||
left: 0,
|
||||
@ -246,6 +260,18 @@ function TileLayoutComponent<T>({ layoutTreeStateAtom, contents, getCursorPoint
|
||||
[contents.onNodeDelete, dispatch]
|
||||
);
|
||||
|
||||
const onLeafMagnifyToggle = useCallback(
|
||||
(node: LayoutNode<T>) => {
|
||||
const action = {
|
||||
type: LayoutTreeActionType.MagnifyNodeToggle,
|
||||
nodeId: node.id,
|
||||
};
|
||||
|
||||
dispatch(action);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
<div className={clsx("tile-layout", contents.className, { animate })} onPointerOver={onPointerOver}>
|
||||
@ -253,6 +279,7 @@ function TileLayoutComponent<T>({ layoutTreeStateAtom, contents, getCursorPoint
|
||||
<DisplayNodesWrapper
|
||||
contents={contents}
|
||||
ready={animate}
|
||||
onLeafMagnifyToggle={onLeafMagnifyToggle}
|
||||
onLeafClose={onLeafClose}
|
||||
layoutTreeState={layoutTreeState}
|
||||
layoutLeafTransforms={layoutLeafTransforms}
|
||||
@ -303,6 +330,13 @@ interface DisplayNodesWrapperProps<T> {
|
||||
* @param node The node that is closed.
|
||||
*/
|
||||
onLeafClose: (node: LayoutNode<T>) => void;
|
||||
|
||||
/**
|
||||
* A callback that is called when a leaf's magnification is being toggled.
|
||||
* @param node The node that is being magnified or un-magnified.
|
||||
*/
|
||||
onLeafMagnifyToggle: (node: LayoutNode<T>) => void;
|
||||
|
||||
/**
|
||||
* A series of CSS properties used to display a leaf node with the correct dimensions and position, as determined from its corresponding OverlayNode.
|
||||
*/
|
||||
@ -314,18 +348,27 @@ interface DisplayNodesWrapperProps<T> {
|
||||
}
|
||||
|
||||
const DisplayNodesWrapper = memo(
|
||||
<T,>({ layoutTreeState, contents, onLeafClose, layoutLeafTransforms, ready }: DisplayNodesWrapperProps<T>) => {
|
||||
<T,>({
|
||||
layoutTreeState,
|
||||
contents,
|
||||
onLeafClose,
|
||||
onLeafMagnifyToggle,
|
||||
layoutLeafTransforms,
|
||||
ready,
|
||||
}: DisplayNodesWrapperProps<T>) => {
|
||||
if (!layoutLeafTransforms) {
|
||||
return null;
|
||||
}
|
||||
return layoutTreeState.leafs.map((leaf) => {
|
||||
return (
|
||||
<DisplayNode
|
||||
className={clsx({ magnified: layoutTreeState.magnifiedNodeId === leaf.id })}
|
||||
key={leaf.id}
|
||||
layoutNode={leaf}
|
||||
contents={contents}
|
||||
transform={layoutLeafTransforms[leaf.id]}
|
||||
onLeafClose={onLeafClose}
|
||||
onLeafMagnifyToggle={onLeafMagnifyToggle}
|
||||
ready={ready}
|
||||
/>
|
||||
);
|
||||
@ -344,6 +387,12 @@ interface DisplayNodeProps<T> {
|
||||
*/
|
||||
contents: TileLayoutContents<T>;
|
||||
|
||||
/**
|
||||
* A callback that is called when a leaf's magnification is being toggled.
|
||||
* @param node The node that is being magnified or unmagnified.
|
||||
*/
|
||||
onLeafMagnifyToggle: (node: LayoutNode<T>) => void;
|
||||
|
||||
/**
|
||||
* A callback that is called when a leaf node gets closed.
|
||||
* @param node The node that is closed.
|
||||
@ -353,6 +402,12 @@ interface DisplayNodeProps<T> {
|
||||
* Determines whether a leaf's contents should be displayed to the user.
|
||||
*/
|
||||
ready: boolean;
|
||||
|
||||
/**
|
||||
* Any class names to add to the component.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* A series of CSS properties used to display a leaf node with the correct dimensions and position, as determined from its corresponding OverlayNode.
|
||||
*/
|
||||
@ -364,104 +419,118 @@ const dragItemType = "TILE_ITEM";
|
||||
/**
|
||||
* The draggable and displayable portion of a leaf node in a layout tree.
|
||||
*/
|
||||
const DisplayNode = memo(<T,>({ layoutNode, contents, transform, onLeafClose, ready }: DisplayNodeProps<T>) => {
|
||||
const tileNodeRef = useRef<HTMLDivElement>(null);
|
||||
const dragHandleRef = useRef<HTMLDivElement>(null);
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
const DisplayNode = memo(
|
||||
<T,>({
|
||||
layoutNode,
|
||||
contents,
|
||||
transform,
|
||||
onLeafMagnifyToggle,
|
||||
onLeafClose,
|
||||
ready,
|
||||
className,
|
||||
}: DisplayNodeProps<T>) => {
|
||||
const tileNodeRef = useRef<HTMLDivElement>(null);
|
||||
const dragHandleRef = useRef<HTMLDivElement>(null);
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const devicePixelRatio = useDevicePixelRatio();
|
||||
const devicePixelRatio = useDevicePixelRatio();
|
||||
|
||||
const [{ isDragging }, drag, dragPreview] = useDrag(
|
||||
() => ({
|
||||
type: dragItemType,
|
||||
item: () => layoutNode,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
const [{ isDragging }, drag, dragPreview] = useDrag(
|
||||
() => ({
|
||||
type: dragItemType,
|
||||
item: () => layoutNode,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
[layoutNode]
|
||||
);
|
||||
[layoutNode]
|
||||
);
|
||||
|
||||
const [previewElementGeneration, setPreviewElementGeneration] = useState(0);
|
||||
const previewElement = useMemo(() => {
|
||||
setPreviewElementGeneration(previewElementGeneration + 1);
|
||||
return (
|
||||
<div key="preview" className="tile-preview-container">
|
||||
<div
|
||||
className="tile-preview"
|
||||
ref={previewRef}
|
||||
style={{
|
||||
width: DragPreviewWidth,
|
||||
height: DragPreviewHeight,
|
||||
transform: `scale(${1 / devicePixelRatio})`,
|
||||
}}
|
||||
>
|
||||
{contents.renderPreview?.(layoutNode.data)}
|
||||
const [previewElementGeneration, setPreviewElementGeneration] = useState(0);
|
||||
const previewElement = useMemo(() => {
|
||||
setPreviewElementGeneration(previewElementGeneration + 1);
|
||||
return (
|
||||
<div key="preview" className="tile-preview-container">
|
||||
<div
|
||||
className="tile-preview"
|
||||
ref={previewRef}
|
||||
style={{
|
||||
width: DragPreviewWidth,
|
||||
height: DragPreviewHeight,
|
||||
transform: `scale(${1 / devicePixelRatio})`,
|
||||
}}
|
||||
>
|
||||
{contents.renderPreview?.(layoutNode.data)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [contents.renderPreview, devicePixelRatio]);
|
||||
|
||||
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null);
|
||||
const [previewImageGeneration, setPreviewImageGeneration] = useState(0);
|
||||
const generatePreviewImage = useCallback(() => {
|
||||
const offsetX = (DragPreviewWidth * devicePixelRatio - DragPreviewWidth) / 2 + 10;
|
||||
const offsetY = (DragPreviewHeight * devicePixelRatio - DragPreviewHeight) / 2 + 10;
|
||||
if (previewImage !== null && previewElementGeneration === previewImageGeneration) {
|
||||
dragPreview(previewImage, { offsetY, offsetX });
|
||||
} else if (previewRef.current) {
|
||||
setPreviewImageGeneration(previewElementGeneration);
|
||||
toPng(previewRef.current).then((url) => {
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
setPreviewImage(img);
|
||||
dragPreview(img, { offsetY, offsetX });
|
||||
});
|
||||
}
|
||||
}, [
|
||||
dragPreview,
|
||||
previewRef.current,
|
||||
previewElementGeneration,
|
||||
previewImageGeneration,
|
||||
previewImage,
|
||||
devicePixelRatio,
|
||||
]);
|
||||
|
||||
// Register the tile item as a draggable component
|
||||
useEffect(() => {
|
||||
drag(dragHandleRef);
|
||||
}, [drag, dragHandleRef.current]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
onLeafClose(layoutNode);
|
||||
}, [layoutNode, onLeafClose]);
|
||||
|
||||
const onMagnifyToggle = useCallback(() => {
|
||||
onLeafMagnifyToggle(layoutNode);
|
||||
}, [layoutNode, onLeafMagnifyToggle]);
|
||||
|
||||
const leafContent = useMemo(() => {
|
||||
return (
|
||||
layoutNode.data && (
|
||||
<div key="leaf" className="tile-leaf">
|
||||
{contents.renderContent(layoutNode.data, ready, onMagnifyToggle, onClose, dragHandleRef)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}, [layoutNode.data, ready, onClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("tile-node", className, { dragging: isDragging })}
|
||||
ref={tileNodeRef}
|
||||
id={layoutNode.id}
|
||||
style={{
|
||||
...transform,
|
||||
}}
|
||||
onPointerEnter={generatePreviewImage}
|
||||
onPointerOver={(event) => event.stopPropagation()}
|
||||
>
|
||||
{leafContent}
|
||||
{previewElement}
|
||||
</div>
|
||||
);
|
||||
}, [contents.renderPreview, devicePixelRatio]);
|
||||
|
||||
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null);
|
||||
const [previewImageGeneration, setPreviewImageGeneration] = useState(0);
|
||||
const generatePreviewImage = useCallback(() => {
|
||||
const offsetX = (DragPreviewWidth * devicePixelRatio - DragPreviewWidth) / 2 + 10;
|
||||
const offsetY = (DragPreviewHeight * devicePixelRatio - DragPreviewHeight) / 2 + 10;
|
||||
if (previewImage !== null && previewElementGeneration === previewImageGeneration) {
|
||||
dragPreview(previewImage, { offsetY, offsetX });
|
||||
} else if (previewRef.current) {
|
||||
setPreviewImageGeneration(previewElementGeneration);
|
||||
toPng(previewRef.current).then((url) => {
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
setPreviewImage(img);
|
||||
dragPreview(img, { offsetY, offsetX });
|
||||
});
|
||||
}
|
||||
}, [
|
||||
dragPreview,
|
||||
previewRef.current,
|
||||
previewElementGeneration,
|
||||
previewImageGeneration,
|
||||
previewImage,
|
||||
devicePixelRatio,
|
||||
]);
|
||||
|
||||
// Register the tile item as a draggable component
|
||||
useEffect(() => {
|
||||
drag(dragHandleRef);
|
||||
}, [drag, dragHandleRef.current]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
onLeafClose(layoutNode);
|
||||
}, [layoutNode, onLeafClose]);
|
||||
|
||||
const leafContent = useMemo(() => {
|
||||
return (
|
||||
layoutNode.data && (
|
||||
<div key="leaf" className="tile-leaf">
|
||||
{contents.renderContent(layoutNode.data, ready, onClose, dragHandleRef)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}, [layoutNode.data, ready, onClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("tile-node", { dragging: isDragging })}
|
||||
ref={tileNodeRef}
|
||||
id={layoutNode.id}
|
||||
style={{
|
||||
...transform,
|
||||
}}
|
||||
onPointerEnter={generatePreviewImage}
|
||||
onPointerOver={(event) => event.stopPropagation()}
|
||||
>
|
||||
{leafContent}
|
||||
{previewElement}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
interface OverlayNodeProps<T> {
|
||||
/**
|
||||
@ -886,7 +955,7 @@ const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefsAtom, s
|
||||
}
|
||||
}
|
||||
|
||||
const placeholderTransform = createTransform(placeholderDimensions);
|
||||
const placeholderTransform = setTransform(placeholderDimensions);
|
||||
newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />;
|
||||
}
|
||||
}
|
||||
@ -907,7 +976,7 @@ const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefsAtom, s
|
||||
width: targetBoundingRect.width,
|
||||
};
|
||||
|
||||
const placeholderTransform = createTransform(placeholderDimensions);
|
||||
const placeholderTransform = setTransform(placeholderDimensions);
|
||||
newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />;
|
||||
}
|
||||
break;
|
||||
|
@ -78,7 +78,7 @@ function getLayoutNodeWaveObjAtomFromTab<T>(
|
||||
const tabValue = get(tabAtom);
|
||||
// console.log("getLayoutNodeWaveObjAtomFromTab tabValue", tabValue);
|
||||
if (!tabValue) return;
|
||||
const layoutNodeOref = WOS.makeORef("layout", tabValue.layoutNode);
|
||||
const layoutNodeOref = WOS.makeORef("layout", tabValue.layoutnode);
|
||||
// console.log("getLayoutNodeWaveObjAtomFromTab oref", layoutNodeOref);
|
||||
return WOS.getWaveObjectAtom<LayoutNodeWaveObj<T>>(layoutNodeOref);
|
||||
}
|
||||
@ -86,13 +86,16 @@ function getLayoutNodeWaveObjAtomFromTab<T>(
|
||||
export function withLayoutStateAtomFromTab<T>(tabAtom: Atom<Tab>): WritableLayoutTreeStateAtom<T> {
|
||||
const pendingActionAtom = atom<LayoutTreeAction>(null) as PrimitiveAtom<LayoutTreeAction>;
|
||||
const generationAtom = atom(0) as PrimitiveAtom<number>;
|
||||
|
||||
return atom(
|
||||
(get) => {
|
||||
const waveObjAtom = getLayoutNodeWaveObjAtomFromTab<T>(tabAtom, get);
|
||||
if (!waveObjAtom) return null;
|
||||
const layoutState = newLayoutTreeState(get(waveObjAtom)?.node);
|
||||
const waveObj = get(waveObjAtom);
|
||||
const layoutState = newLayoutTreeState(waveObj?.node);
|
||||
layoutState.pendingAction = get(pendingActionAtom);
|
||||
layoutState.generation = get(generationAtom);
|
||||
layoutState.magnifiedNodeId = waveObj?.magnifiednodeid;
|
||||
return layoutState;
|
||||
},
|
||||
(get, set, value) => {
|
||||
@ -100,7 +103,11 @@ export function withLayoutStateAtomFromTab<T>(tabAtom: Atom<Tab>): WritableLayou
|
||||
if (get(generationAtom) !== value.generation) {
|
||||
const waveObjAtom = getLayoutNodeWaveObjAtomFromTab<T>(tabAtom, get);
|
||||
if (!waveObjAtom) return;
|
||||
const newWaveObj = { ...get(waveObjAtom), node: value.rootNode };
|
||||
const newWaveObj = {
|
||||
...get(waveObjAtom),
|
||||
node: value.rootNode,
|
||||
magnifiednodeid: value.magnifiedNodeId,
|
||||
};
|
||||
set(generationAtom, value.generation);
|
||||
set(waveObjAtom, newWaveObj);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
LayoutTreeComputeMoveNodeAction,
|
||||
LayoutTreeDeleteNodeAction,
|
||||
LayoutTreeInsertNodeAction,
|
||||
LayoutTreeMagnifyNodeToggleAction,
|
||||
LayoutTreeMoveNodeAction,
|
||||
LayoutTreeResizeNodeAction,
|
||||
LayoutTreeSetPendingAction,
|
||||
@ -108,6 +109,10 @@ function layoutTreeStateReducerInner<T>(layoutTreeState: LayoutTreeState<T>, act
|
||||
resizeNode(layoutTreeState, action as LayoutTreeResizeNodeAction);
|
||||
layoutTreeState.generation++;
|
||||
break;
|
||||
case LayoutTreeActionType.MagnifyNodeToggle:
|
||||
magnifyNodeToggle(layoutTreeState, action as LayoutTreeMagnifyNodeToggleAction);
|
||||
layoutTreeState.generation++;
|
||||
break;
|
||||
default: {
|
||||
console.error("Invalid reducer action", layoutTreeState, action);
|
||||
}
|
||||
@ -460,3 +465,20 @@ function resizeNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeRe
|
||||
node.size = resize.size;
|
||||
}
|
||||
}
|
||||
|
||||
function magnifyNodeToggle<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeMagnifyNodeToggleAction) {
|
||||
console.log("magnifyNodeToggle", layoutTreeState, action);
|
||||
if (!action.nodeId) {
|
||||
console.error("invalid magnifyNodeToggle operation. nodeId must be defined.");
|
||||
return;
|
||||
}
|
||||
if (layoutTreeState.rootNode.id === action.nodeId) {
|
||||
console.warn(`cannot toggle magnification of node ${action.nodeId} because it is the root node.`);
|
||||
return;
|
||||
}
|
||||
if (layoutTreeState.magnifiedNodeId === action.nodeId) {
|
||||
layoutTreeState.magnifiedNodeId = undefined;
|
||||
} else {
|
||||
layoutTreeState.magnifiedNodeId = action.nodeId;
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ export enum LayoutTreeActionType {
|
||||
ResizeNode = "resize",
|
||||
InsertNode = "insert",
|
||||
DeleteNode = "delete",
|
||||
MagnifyNodeToggle = "magnify",
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,6 +164,18 @@ export interface LayoutTreeResizeNodeAction extends LayoutTreeAction {
|
||||
resizeOperations: ResizeNodeOperation[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Action for toggling magnification of a node from the layout tree.
|
||||
*/
|
||||
export interface LayoutTreeMagnifyNodeToggleAction extends LayoutTreeAction {
|
||||
type: LayoutTreeActionType.MagnifyNodeToggle;
|
||||
|
||||
/**
|
||||
* The id of the node to maximize;
|
||||
*/
|
||||
nodeId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the state of a layout tree.
|
||||
*
|
||||
@ -173,6 +186,7 @@ export type LayoutTreeState<T> = {
|
||||
leafs: LayoutNode<T>[];
|
||||
pendingAction: LayoutTreeAction;
|
||||
generation: number;
|
||||
magnifiedNodeId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -200,6 +214,7 @@ export type WritableLayoutTreeStateAtom<T> = WritableAtom<LayoutTreeState<T>, [v
|
||||
export type ContentRenderer<T> = (
|
||||
data: T,
|
||||
ready: boolean,
|
||||
onMagnifyToggle: () => void,
|
||||
onClose: () => void,
|
||||
dragHandleRef: React.RefObject<HTMLDivElement>
|
||||
) => React.ReactNode;
|
||||
@ -208,6 +223,7 @@ export type PreviewRenderer<T> = (data: T) => React.ReactElement;
|
||||
|
||||
export interface LayoutNodeWaveObj<T> extends WaveObj {
|
||||
node: LayoutNode<T>;
|
||||
magnifiednodeid: string;
|
||||
}
|
||||
|
||||
export const DefaultNodeSize = 10;
|
||||
|
@ -66,10 +66,18 @@
|
||||
}
|
||||
|
||||
.tile-node {
|
||||
border-radius: var(--block-border-radius);
|
||||
overflow: hidden;
|
||||
|
||||
&.dragging {
|
||||
filter: blur(8px);
|
||||
}
|
||||
|
||||
&.magnified {
|
||||
background-color: var(--block-bg-solid-color);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tile-preview-container {
|
||||
position: absolute;
|
||||
top: 10000px;
|
||||
@ -89,7 +97,7 @@
|
||||
.placeholder {
|
||||
transition-duration: 0.15s;
|
||||
transition-timing-function: ease-in;
|
||||
transition-property: transform, width, height;
|
||||
transition-property: transform, width, height, background-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,6 +114,6 @@
|
||||
.placeholder {
|
||||
background-color: var(--accent-color);
|
||||
opacity: 0.5;
|
||||
border-radius: 5px;
|
||||
border-radius: calc(var(--block-border-radius) + 2px);
|
||||
}
|
||||
}
|
||||
|
3
frontend/types/gotypes.d.ts
vendored
3
frontend/types/gotypes.d.ts
vendored
@ -176,6 +176,7 @@ declare global {
|
||||
// wstore.LayoutNode
|
||||
type LayoutNode = WaveObj & {
|
||||
node?: any;
|
||||
magnifiednodeid?: string;
|
||||
meta?: MetaType;
|
||||
};
|
||||
|
||||
@ -315,7 +316,7 @@ declare global {
|
||||
// wstore.Tab
|
||||
type Tab = WaveObj & {
|
||||
name: string;
|
||||
layoutNode: string;
|
||||
layoutnode: string;
|
||||
blockids: string[];
|
||||
meta: MetaType;
|
||||
};
|
||||
|
@ -207,7 +207,7 @@ type Tab struct {
|
||||
OID string `json:"oid"`
|
||||
Version int `json:"version"`
|
||||
Name string `json:"name"`
|
||||
LayoutNode string `json:"layoutNode"`
|
||||
LayoutNode string `json:"layoutnode"`
|
||||
BlockIds []string `json:"blockids"`
|
||||
Meta map[string]any `json:"meta"`
|
||||
}
|
||||
@ -225,10 +225,11 @@ func (t *Tab) GetBlockORefs() []waveobj.ORef {
|
||||
}
|
||||
|
||||
type LayoutNode struct {
|
||||
OID string `json:"oid"`
|
||||
Version int `json:"version"`
|
||||
Node any `json:"node,omitempty"`
|
||||
Meta map[string]any `json:"meta,omitempty"`
|
||||
OID string `json:"oid"`
|
||||
Version int `json:"version"`
|
||||
Node any `json:"node,omitempty"`
|
||||
MagnifiedNodeId string `json:"magnifiednodeid,omitempty"`
|
||||
Meta map[string]any `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
func (*LayoutNode) GetOType() string {
|
||||
|
Loading…
Reference in New Issue
Block a user