mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-24 09:18:27 +00:00
A recent refactor introduced the BlockRegistry to add different types of widgets. This was missing the tips widget, so it has been added to correct that.
302 lines
11 KiB
TypeScript
302 lines
11 KiB
TypeScript
// Copyright 2025, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import {
|
|
BlockComponentModel2,
|
|
BlockNodeModel,
|
|
BlockProps,
|
|
FullBlockProps,
|
|
FullSubBlockProps,
|
|
SubBlockProps,
|
|
} from "@/app/block/blocktypes";
|
|
import { LauncherViewModel } from "@/app/view/launcher/launcher";
|
|
import { PreviewModel } from "@/app/view/preview/preview";
|
|
import { SysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
|
|
import { VDomModel } from "@/app/view/vdom/vdom-model";
|
|
import { ErrorBoundary } from "@/element/errorboundary";
|
|
import { CenteredDiv } from "@/element/quickelems";
|
|
import { useDebouncedNodeInnerRect } from "@/layout/index";
|
|
import {
|
|
counterInc,
|
|
getBlockComponentModel,
|
|
registerBlockComponentModel,
|
|
unregisterBlockComponentModel,
|
|
} from "@/store/global";
|
|
import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos";
|
|
import { focusedBlockId, getElemAsStr } from "@/util/focusutil";
|
|
import { isBlank, useAtomValueSafe } from "@/util/util";
|
|
import { HelpViewModel } from "@/view/helpview/helpview";
|
|
import { TermViewModel } from "@/view/term/term";
|
|
import { WaveAiModel } from "@/view/waveai/waveai";
|
|
import { WebViewModel } from "@/view/webview/webview";
|
|
import clsx from "clsx";
|
|
import { atom, useAtomValue } from "jotai";
|
|
import { memo, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
import { QuickTipsViewModel } from "../view/quicktipsview/quicktipsview";
|
|
import "./block.scss";
|
|
import { BlockFrame } from "./blockframe";
|
|
import { blockViewToIcon, blockViewToName } from "./blockutil";
|
|
|
|
const BlockRegistry: Map<string, ViewModelClass> = new Map();
|
|
BlockRegistry.set("term", TermViewModel);
|
|
BlockRegistry.set("preview", PreviewModel);
|
|
BlockRegistry.set("web", WebViewModel);
|
|
BlockRegistry.set("waveai", WaveAiModel);
|
|
BlockRegistry.set("cpuplot", SysinfoViewModel);
|
|
BlockRegistry.set("sysinfo", SysinfoViewModel);
|
|
BlockRegistry.set("vdom", VDomModel);
|
|
BlockRegistry.set("tips", QuickTipsViewModel);
|
|
BlockRegistry.set("help", HelpViewModel);
|
|
BlockRegistry.set("launcher", LauncherViewModel);
|
|
|
|
function makeViewModel(blockId: string, blockView: string, nodeModel: BlockNodeModel): ViewModel {
|
|
const ctor = BlockRegistry.get(blockView);
|
|
if (ctor != null) {
|
|
return new ctor(blockId, nodeModel);
|
|
}
|
|
return makeDefaultViewModel(blockId, blockView);
|
|
}
|
|
|
|
function getViewElem(
|
|
blockId: string,
|
|
blockRef: React.RefObject<HTMLDivElement>,
|
|
contentRef: React.RefObject<HTMLDivElement>,
|
|
blockView: string,
|
|
viewModel: ViewModel
|
|
): JSX.Element {
|
|
if (isBlank(blockView)) {
|
|
return <CenteredDiv>No View</CenteredDiv>;
|
|
}
|
|
if (viewModel.viewComponent == null) {
|
|
return <CenteredDiv>No View Component</CenteredDiv>;
|
|
}
|
|
const VC = viewModel.viewComponent;
|
|
return <VC key={blockId} blockId={blockId} blockRef={blockRef} contentRef={contentRef} model={viewModel} />;
|
|
}
|
|
|
|
function makeDefaultViewModel(blockId: string, viewType: string): ViewModel {
|
|
const blockDataAtom = getWaveObjectAtom<Block>(makeORef("block", blockId));
|
|
let viewModel: ViewModel = {
|
|
viewType: viewType,
|
|
viewIcon: atom((get) => {
|
|
const blockData = get(blockDataAtom);
|
|
return blockViewToIcon(blockData?.meta?.view);
|
|
}),
|
|
viewName: atom((get) => {
|
|
const blockData = get(blockDataAtom);
|
|
return blockViewToName(blockData?.meta?.view);
|
|
}),
|
|
preIconButton: atom(null),
|
|
endIconButtons: atom(null),
|
|
viewComponent: null,
|
|
};
|
|
return viewModel;
|
|
}
|
|
|
|
const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => {
|
|
const [blockData] = useWaveObjectValue<Block>(makeORef("block", nodeModel.blockId));
|
|
if (!blockData) {
|
|
return null;
|
|
}
|
|
return (
|
|
<BlockFrame
|
|
key={nodeModel.blockId}
|
|
nodeModel={nodeModel}
|
|
preview={true}
|
|
blockModel={null}
|
|
viewModel={viewModel}
|
|
/>
|
|
);
|
|
});
|
|
|
|
const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => {
|
|
const [blockData] = useWaveObjectValue<Block>(makeORef("block", nodeModel.blockId));
|
|
const blockRef = useRef<HTMLDivElement>(null);
|
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
const viewElem = useMemo(
|
|
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel),
|
|
[nodeModel.blockId, blockData?.meta?.view, viewModel]
|
|
);
|
|
const noPadding = useAtomValueSafe(viewModel.noPadding);
|
|
if (!blockData) {
|
|
return null;
|
|
}
|
|
return (
|
|
<div key="content" className={clsx("block-content", { "block-no-padding": noPadding })} ref={contentRef}>
|
|
<ErrorBoundary>
|
|
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
|
|
</ErrorBoundary>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => {
|
|
counterInc("render-BlockFull");
|
|
const focusElemRef = useRef<HTMLInputElement>(null);
|
|
const blockRef = useRef<HTMLDivElement>(null);
|
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
const [blockClicked, setBlockClicked] = useState(false);
|
|
const [blockData] = useWaveObjectValue<Block>(makeORef("block", nodeModel.blockId));
|
|
const isFocused = useAtomValue(nodeModel.isFocused);
|
|
const disablePointerEvents = useAtomValue(nodeModel.disablePointerEvents);
|
|
const innerRect = useDebouncedNodeInnerRect(nodeModel);
|
|
const noPadding = useAtomValueSafe(viewModel.noPadding);
|
|
|
|
useLayoutEffect(() => {
|
|
setBlockClicked(isFocused);
|
|
}, [isFocused]);
|
|
|
|
useLayoutEffect(() => {
|
|
if (!blockClicked) {
|
|
return;
|
|
}
|
|
setBlockClicked(false);
|
|
const focusWithin = focusedBlockId() == nodeModel.blockId;
|
|
if (!focusWithin) {
|
|
setFocusTarget();
|
|
}
|
|
if (!isFocused) {
|
|
nodeModel.focusNode();
|
|
}
|
|
}, [blockClicked, isFocused]);
|
|
|
|
const setBlockClickedTrue = useCallback(() => {
|
|
setBlockClicked(true);
|
|
}, []);
|
|
|
|
const [blockContentOffset, setBlockContentOffset] = useState<Dimensions>();
|
|
|
|
useEffect(() => {
|
|
if (blockRef.current && contentRef.current) {
|
|
const blockRect = blockRef.current.getBoundingClientRect();
|
|
const contentRect = contentRef.current.getBoundingClientRect();
|
|
setBlockContentOffset({
|
|
top: 0,
|
|
left: 0,
|
|
width: blockRect.width - contentRect.width,
|
|
height: blockRect.height - contentRect.height,
|
|
});
|
|
}
|
|
}, [blockRef, contentRef]);
|
|
|
|
const blockContentStyle = useMemo<React.CSSProperties>(() => {
|
|
const retVal: React.CSSProperties = {
|
|
pointerEvents: disablePointerEvents ? "none" : undefined,
|
|
};
|
|
if (innerRect?.width && innerRect.height && blockContentOffset) {
|
|
retVal.width = `calc(${innerRect?.width} - ${blockContentOffset.width}px)`;
|
|
retVal.height = `calc(${innerRect?.height} - ${blockContentOffset.height}px)`;
|
|
}
|
|
return retVal;
|
|
}, [innerRect, disablePointerEvents, blockContentOffset]);
|
|
|
|
const viewElem = useMemo(
|
|
() => getViewElem(nodeModel.blockId, blockRef, contentRef, blockData?.meta?.view, viewModel),
|
|
[nodeModel.blockId, blockData?.meta?.view, viewModel]
|
|
);
|
|
|
|
const handleChildFocus = useCallback(
|
|
(event: React.FocusEvent<HTMLDivElement, Element>) => {
|
|
console.log("setFocusedChild", nodeModel.blockId, getElemAsStr(event.target));
|
|
if (!isFocused) {
|
|
console.log("focusedChild focus", nodeModel.blockId);
|
|
nodeModel.focusNode();
|
|
}
|
|
},
|
|
[isFocused]
|
|
);
|
|
|
|
const setFocusTarget = useCallback(() => {
|
|
const ok = viewModel?.giveFocus?.();
|
|
if (ok) {
|
|
return;
|
|
}
|
|
focusElemRef.current?.focus({ preventScroll: true });
|
|
}, []);
|
|
|
|
const blockModel: BlockComponentModel2 = {
|
|
onClick: setBlockClickedTrue,
|
|
onFocusCapture: handleChildFocus,
|
|
blockRef: blockRef,
|
|
};
|
|
|
|
return (
|
|
<BlockFrame
|
|
key={nodeModel.blockId}
|
|
nodeModel={nodeModel}
|
|
preview={false}
|
|
blockModel={blockModel}
|
|
viewModel={viewModel}
|
|
>
|
|
<div key="focuselem" className="block-focuselem">
|
|
<input
|
|
type="text"
|
|
value=""
|
|
ref={focusElemRef}
|
|
id={`${nodeModel.blockId}-dummy-focus`} // don't change this name (used in refocusNode)
|
|
className="dummy-focus"
|
|
onChange={() => {}}
|
|
/>
|
|
</div>
|
|
<div
|
|
key="content"
|
|
className={clsx("block-content", { "block-no-padding": noPadding })}
|
|
ref={contentRef}
|
|
style={blockContentStyle}
|
|
>
|
|
<ErrorBoundary>
|
|
<Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</Suspense>
|
|
</ErrorBoundary>
|
|
</div>
|
|
</BlockFrame>
|
|
);
|
|
});
|
|
|
|
const Block = memo((props: BlockProps) => {
|
|
counterInc("render-Block");
|
|
counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8));
|
|
const [blockData, loading] = useWaveObjectValue<Block>(makeORef("block", props.nodeModel.blockId));
|
|
const bcm = getBlockComponentModel(props.nodeModel.blockId);
|
|
let viewModel = bcm?.viewModel;
|
|
if (viewModel == null || viewModel.viewType != blockData?.meta?.view) {
|
|
viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel);
|
|
registerBlockComponentModel(props.nodeModel.blockId, { viewModel });
|
|
}
|
|
useEffect(() => {
|
|
return () => {
|
|
unregisterBlockComponentModel(props.nodeModel.blockId);
|
|
viewModel?.dispose?.();
|
|
};
|
|
}, []);
|
|
if (loading || isBlank(props.nodeModel.blockId) || blockData == null) {
|
|
return null;
|
|
}
|
|
if (props.preview) {
|
|
return <BlockPreview {...props} viewModel={viewModel} />;
|
|
}
|
|
return <BlockFull {...props} viewModel={viewModel} />;
|
|
});
|
|
|
|
const SubBlock = memo((props: SubBlockProps) => {
|
|
counterInc("render-Block");
|
|
counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8));
|
|
const [blockData, loading] = useWaveObjectValue<Block>(makeORef("block", props.nodeModel.blockId));
|
|
const bcm = getBlockComponentModel(props.nodeModel.blockId);
|
|
let viewModel = bcm?.viewModel;
|
|
if (viewModel == null || viewModel.viewType != blockData?.meta?.view) {
|
|
viewModel = makeViewModel(props.nodeModel.blockId, blockData?.meta?.view, props.nodeModel);
|
|
registerBlockComponentModel(props.nodeModel.blockId, { viewModel });
|
|
}
|
|
useEffect(() => {
|
|
return () => {
|
|
unregisterBlockComponentModel(props.nodeModel.blockId);
|
|
viewModel?.dispose?.();
|
|
};
|
|
}, []);
|
|
if (loading || isBlank(props.nodeModel.blockId) || blockData == null) {
|
|
return null;
|
|
}
|
|
return <BlockSubBlock {...props} viewModel={viewModel} />;
|
|
});
|
|
|
|
export { Block, SubBlock };
|