mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-24 17:08:26 +01:00
239 lines
8.0 KiB
TypeScript
239 lines
8.0 KiB
TypeScript
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import { CodeEdit } from "@/app/view/codeedit";
|
|
import { PlotView } from "@/app/view/plotview";
|
|
import { PreviewView } from "@/app/view/preview";
|
|
import { TerminalView } from "@/app/view/term/term";
|
|
import { ErrorBoundary } from "@/element/errorboundary";
|
|
import { CenteredDiv } from "@/element/quickelems";
|
|
import { atoms, globalStore, useBlockAtom } from "@/store/global";
|
|
import * as WOS from "@/store/wos";
|
|
import clsx from "clsx";
|
|
import { produce } from "immer";
|
|
import * as jotai from "jotai";
|
|
import * as React from "react";
|
|
|
|
import "./block.less";
|
|
|
|
const HoverPixels = 15;
|
|
const HoverTimeoutMs = 100;
|
|
|
|
interface BlockProps {
|
|
blockId: string;
|
|
onClose?: () => void;
|
|
}
|
|
|
|
function getBlockHeaderText(blockData: Block): string {
|
|
if (!blockData) {
|
|
return "no block data";
|
|
}
|
|
return `${blockData?.view} [${blockData.oid.substring(0, 8)}]`;
|
|
}
|
|
|
|
const FramelessBlockHeader = ({ blockId, onClose }: BlockProps) => {
|
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
|
|
|
return (
|
|
<div key="header" className="block-header">
|
|
<div className="block-header-text text-fixed">{getBlockHeaderText(blockData)}</div>
|
|
{onClose && (
|
|
<div className="close-button" onClick={onClose}>
|
|
<i className="fa fa-solid fa-xmark-large" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const hoverStateOff = "off";
|
|
const hoverStatePending = "pending";
|
|
const hoverStateOn = "on";
|
|
|
|
interface BlockFrameProps {
|
|
blockId: string;
|
|
onClose?: () => void;
|
|
onClick?: () => void;
|
|
preview: boolean;
|
|
children?: React.ReactNode;
|
|
blockRef?: React.RefObject<HTMLDivElement>;
|
|
}
|
|
|
|
const BlockFrame_Tech = ({ blockId, onClose, onClick, preview, blockRef, children }: BlockFrameProps) => {
|
|
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
|
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
|
|
return jotai.atom((get) => {
|
|
const winData = get(atoms.waveWindow);
|
|
return winData.activeblockid === blockId;
|
|
});
|
|
});
|
|
let isFocused = jotai.useAtomValue(isFocusedAtom);
|
|
if (preview) {
|
|
isFocused = true;
|
|
}
|
|
return (
|
|
<div
|
|
className={clsx(
|
|
"block",
|
|
"block-frame-tech",
|
|
isFocused ? "block-focused" : null,
|
|
preview ? "block-preview" : null
|
|
)}
|
|
onClick={onClick}
|
|
ref={blockRef}
|
|
>
|
|
<div className="block-frame-tech-header">{getBlockHeaderText(blockData)}</div>
|
|
<div className="block-frame-tech-close" onClick={onClose}>
|
|
<i className="fa fa-solid fa-xmark fa-fw " />
|
|
</div>
|
|
{preview ? <div className="block-frame-preview" /> : children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const BlockFrame_Frameless = ({ blockId, onClose, onClick, preview, blockRef, children }: BlockFrameProps) => {
|
|
const localBlockRef = React.useRef<HTMLDivElement>(null);
|
|
const [showHeader, setShowHeader] = React.useState(preview ? true : false);
|
|
const hoverState = React.useRef(hoverStateOff);
|
|
|
|
// this forward the lcal ref to the blockRef
|
|
React.useImperativeHandle(blockRef, () => localBlockRef.current);
|
|
|
|
React.useEffect(() => {
|
|
if (preview) {
|
|
return;
|
|
}
|
|
const block = localBlockRef.current;
|
|
let hoverTimeout: NodeJS.Timeout = null;
|
|
const handleMouseMove = (event) => {
|
|
const rect = block.getBoundingClientRect();
|
|
if (event.clientY - rect.top <= HoverPixels) {
|
|
if (hoverState.current == hoverStateOff) {
|
|
hoverTimeout = setTimeout(() => {
|
|
if (hoverState.current == hoverStatePending) {
|
|
hoverState.current = hoverStateOn;
|
|
setShowHeader(true);
|
|
}
|
|
}, HoverTimeoutMs);
|
|
hoverState.current = hoverStatePending;
|
|
}
|
|
} else {
|
|
if (hoverTimeout) {
|
|
if (hoverState.current == hoverStatePending) {
|
|
hoverState.current = hoverStateOff;
|
|
}
|
|
clearTimeout(hoverTimeout);
|
|
hoverTimeout = null;
|
|
}
|
|
}
|
|
};
|
|
block.addEventListener("mousemove", handleMouseMove);
|
|
return () => {
|
|
block.removeEventListener("mousemove", handleMouseMove);
|
|
};
|
|
});
|
|
let mouseLeaveHandler = () => {
|
|
if (preview) {
|
|
return;
|
|
}
|
|
setShowHeader(false);
|
|
hoverState.current = hoverStateOff;
|
|
};
|
|
return (
|
|
<div
|
|
className="block block-frame-frameless"
|
|
ref={localBlockRef}
|
|
onMouseLeave={mouseLeaveHandler}
|
|
onClick={onClick}
|
|
>
|
|
<div
|
|
className={clsx("block-header-animation-wrap", showHeader ? "is-showing" : null)}
|
|
onMouseLeave={mouseLeaveHandler}
|
|
>
|
|
<FramelessBlockHeader blockId={blockId} onClose={onClose} />
|
|
</div>
|
|
{preview ? <div className="block-frame-preview" /> : children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const BlockFrame = (props: BlockFrameProps) => {
|
|
const blockId = props.blockId;
|
|
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
|
const tabData = jotai.useAtomValue(atoms.tabAtom);
|
|
|
|
if (!blockId || !blockData) {
|
|
return null;
|
|
}
|
|
// if 0 or 1 blocks, use frameless, otherwise use tech
|
|
const numBlocks = tabData?.blockids?.length ?? 0;
|
|
if (numBlocks <= 1) {
|
|
return <BlockFrame_Frameless {...props} />;
|
|
}
|
|
return <BlockFrame_Tech {...props} />;
|
|
};
|
|
|
|
function setBlockFocus(blockId: string) {
|
|
let winData = globalStore.get(atoms.waveWindow);
|
|
if (winData.activeblockid === blockId) {
|
|
return;
|
|
}
|
|
winData = produce(winData, (draft) => {
|
|
draft.activeblockid = blockId;
|
|
});
|
|
WOS.setObjectValue(winData, globalStore.set, true);
|
|
}
|
|
|
|
const Block = ({ blockId, onClose }: BlockProps) => {
|
|
let blockElem: JSX.Element = null;
|
|
const focusElemRef = React.useRef<HTMLInputElement>(null);
|
|
const blockRef = React.useRef<HTMLDivElement>(null);
|
|
const [blockClicked, setBlockClicked] = React.useState(false);
|
|
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
|
|
|
React.useLayoutEffect(() => {
|
|
if (!blockClicked) {
|
|
return;
|
|
}
|
|
setBlockClicked(false);
|
|
const focusWithin = blockRef.current?.contains(document.activeElement);
|
|
if (!focusWithin) {
|
|
focusElemRef.current?.focus();
|
|
}
|
|
setBlockFocus(blockId);
|
|
}, [blockClicked]);
|
|
|
|
if (!blockId || !blockData) return null;
|
|
if (blockDataLoading) {
|
|
blockElem = <CenteredDiv>Loading...</CenteredDiv>;
|
|
} else if (blockData.view === "term") {
|
|
blockElem = <TerminalView blockId={blockId} />;
|
|
} else if (blockData.view === "preview") {
|
|
blockElem = <PreviewView blockId={blockId} />;
|
|
} else if (blockData.view === "plot") {
|
|
blockElem = <PlotView />;
|
|
} else if (blockData.view === "codeedit") {
|
|
blockElem = <CodeEdit text={null} filename={null} />;
|
|
}
|
|
return (
|
|
<BlockFrame
|
|
blockId={blockId}
|
|
onClose={onClose}
|
|
preview={false}
|
|
onClick={() => setBlockClicked(true)}
|
|
blockRef={blockRef}
|
|
>
|
|
<div key="focuselem" className="block-focuselem">
|
|
<input type="text" value="" ref={focusElemRef} onChange={() => {}} />
|
|
</div>
|
|
<div key="content" className="block-content">
|
|
<ErrorBoundary>
|
|
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{blockElem}</React.Suspense>
|
|
</ErrorBoundary>
|
|
</div>
|
|
</BlockFrame>
|
|
);
|
|
};
|
|
|
|
export { Block, BlockFrame };
|