// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { BlockComponentModel2, BlockNodeModel, BlockProps, FullBlockProps, FullSubBlockProps, SubBlockProps, } from "@/app/block/blocktypes"; import { PlotView } from "@/app/view/plotview/plotview"; import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview"; import { SysinfoView, SysinfoViewModel, makeSysinfoViewModel } from "@/app/view/sysinfo/sysinfo"; import { VDomView, makeVDomModel } from "@/app/view/vdom/vdom"; 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 { HelpView, HelpViewModel, makeHelpViewModel } from "@/view/helpview/helpview"; import { QuickTipsView, QuickTipsViewModel } from "@/view/quicktipsview/quicktipsview"; import { TermViewModel, TerminalView, makeTerminalModel } from "@/view/term/term"; import { WaveAi, WaveAiModel, makeWaveAiViewModel } from "@/view/waveai/waveai"; import { WebView, WebViewModel, makeWebViewModel } from "@/view/webview/webview"; import clsx from "clsx"; import { atom, useAtomValue } from "jotai"; import { Suspense, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import "./block.scss"; import { BlockFrame } from "./blockframe"; import { blockViewToIcon, blockViewToName } from "./blockutil"; function makeViewModel(blockId: string, blockView: string, nodeModel: BlockNodeModel): ViewModel { if (blockView === "term") { return makeTerminalModel(blockId, nodeModel); } if (blockView === "preview") { return makePreviewModel(blockId, nodeModel); } if (blockView === "web") { return makeWebViewModel(blockId, nodeModel); } if (blockView === "waveai") { return makeWaveAiViewModel(blockId); } if (blockView === "cpuplot" || blockView == "sysinfo") { // "cpuplot" is for backwards compatibility with already-opened widgets return makeSysinfoViewModel(blockId, blockView); } if (blockView == "vdom") { return makeVDomModel(blockId, nodeModel); } if (blockView === "help") { return makeHelpViewModel(blockId, nodeModel); } return makeDefaultViewModel(blockId, blockView); } function getViewElem( blockId: string, blockRef: React.RefObject, contentRef: React.RefObject, blockView: string, viewModel: ViewModel ): JSX.Element { if (isBlank(blockView)) { return No View; } if (blockView === "term") { return ; } if (blockView === "preview") { return ( ); } if (blockView === "plot") { return ; } if (blockView === "web") { return ; } if (blockView === "waveai") { return ; } if (blockView === "cpuplot" || blockView === "sysinfo") { // "cpuplot" is for backwards compatibility with already opened widgets return ; } if (blockView == "help") { return ; } if (blockView == "tips") { return ; } if (blockView == "vdom") { return ; } return Invalid View "{blockView}"; } function makeDefaultViewModel(blockId: string, viewType: string): ViewModel { const blockDataAtom = getWaveObjectAtom(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), }; return viewModel; } const BlockPreview = memo(({ nodeModel, viewModel }: FullBlockProps) => { const [blockData] = useWaveObjectValue(makeORef("block", nodeModel.blockId)); if (!blockData) { return null; } return ( ); }); const BlockSubBlock = memo(({ nodeModel, viewModel }: FullSubBlockProps) => { const [blockData] = useWaveObjectValue(makeORef("block", nodeModel.blockId)); const blockRef = useRef(null); const contentRef = useRef(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 (
Loading...}>{viewElem}
); }); const BlockFull = memo(({ nodeModel, viewModel }: FullBlockProps) => { counterInc("render-BlockFull"); const focusElemRef = useRef(null); const blockRef = useRef(null); const contentRef = useRef(null); const [blockClicked, setBlockClicked] = useState(false); const [blockData] = useWaveObjectValue(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(); 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(() => { 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) => { 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 (
{}} />
Loading...}>{viewElem}
); }); const Block = memo((props: BlockProps) => { counterInc("render-Block"); counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8)); const [blockData, loading] = useWaveObjectValue(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 ; } return ; }); const SubBlock = memo((props: SubBlockProps) => { counterInc("render-Block"); counterInc("render-Block-" + props.nodeModel?.blockId?.substring(0, 8)); const [blockData, loading] = useWaveObjectValue(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 ; }); export { Block, SubBlock };