mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
Disable block pointer events during layout drag (#183)
This will ensure that the webview cannot capture the pointer events and disrupt the drag functionality. This also fixes an issue where greedy and imprecise bounds calculations could result in thrashing of the layoutState.pendingAction field, which could cause the placeholder to flicker. This also fixes issues where some React Effects that were supposed to be debounced or throttled were being invoked too much. This is because useEffect regenerates the callback when it is run, resulting in the debounce or throttle never taking effect. Moving the throttled or debounced logic to a separate callback solves this.
This commit is contained in:
parent
3ff03f7b34
commit
2157df85de
@ -22,6 +22,7 @@ import * as React from "react";
|
||||
import "./block.less";
|
||||
|
||||
export interface LayoutComponentModel {
|
||||
disablePointerEvents: boolean;
|
||||
onClose?: () => void;
|
||||
onMagnifyToggle?: () => void;
|
||||
dragHandleRef?: React.RefObject<HTMLDivElement>;
|
||||
@ -268,7 +269,7 @@ const BlockFrame_Default_Component = ({
|
||||
if (preview) {
|
||||
isFocused = true;
|
||||
}
|
||||
let style: React.CSSProperties = {};
|
||||
const style: React.CSSProperties = {};
|
||||
if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
|
||||
style.borderColor = blockData.meta["frame:bordercolor"];
|
||||
}
|
||||
@ -286,12 +287,13 @@ const BlockFrame_Default_Component = ({
|
||||
if (preIconButton) {
|
||||
preIconButtonElem = <IconButton decl={preIconButton} className="block-frame-preicon-button" />;
|
||||
}
|
||||
let endIconsElem: JSX.Element[] = [];
|
||||
const endIconsElem: JSX.Element[] = [];
|
||||
if (endIconButtons && endIconButtons.length > 0) {
|
||||
for (let idx = 0; idx < endIconButtons.length; idx++) {
|
||||
const button = endIconButtons[idx];
|
||||
endIconsElem.push(<IconButton key={idx} decl={button} className="block-frame-endicon-button" />);
|
||||
}
|
||||
endIconsElem.push(
|
||||
...endIconButtons.map((button, idx) => (
|
||||
<IconButton key={idx} decl={button} className="block-frame-endicon-button" />
|
||||
))
|
||||
);
|
||||
}
|
||||
const settingsDecl: HeaderIconButton = {
|
||||
elemtype: "iconbutton",
|
||||
@ -549,7 +551,7 @@ function makeDefaultViewModel(blockId: string): ViewModel {
|
||||
}
|
||||
|
||||
const BlockPreview = React.memo(({ blockId, layoutModel }: BlockProps) => {
|
||||
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
|
||||
if (!blockData) {
|
||||
return null;
|
||||
}
|
||||
@ -578,7 +580,7 @@ const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => {
|
||||
return winData.activeblockid === blockId;
|
||||
});
|
||||
});
|
||||
let isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||
const isFocused = jotai.useAtomValue(isFocusedAtom);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
setBlockClicked(isFocused);
|
||||
@ -651,7 +653,11 @@ const BlockFull = React.memo(({ blockId, layoutModel }: BlockProps) => {
|
||||
<div key="focuselem" className="block-focuselem">
|
||||
<input type="text" value="" ref={focusElemRef} id={`${blockId}-dummy-focus`} onChange={() => {}} />
|
||||
</div>
|
||||
<div key="content" className="block-content">
|
||||
<div
|
||||
key="content"
|
||||
className="block-content"
|
||||
style={{ pointerEvents: layoutModel?.disablePointerEvents ? "none" : undefined }}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
|
@ -8,6 +8,7 @@ import * as WOS from "@/store/wos";
|
||||
import * as React from "react";
|
||||
|
||||
import { CenteredDiv } from "@/element/quickelems";
|
||||
import { ContentRenderer } from "@/layout/lib/model";
|
||||
import { TileLayout } from "frontend/layout/index";
|
||||
import { getLayoutStateAtomForTab } from "frontend/layout/lib/layoutAtom";
|
||||
import { useAtomValue } from "jotai";
|
||||
@ -23,23 +24,25 @@ const TabContent = React.memo(({ tabId }: { tabId: string }) => {
|
||||
const tabData = useAtomValue(tabAtom);
|
||||
|
||||
const tileLayoutContents = useMemo(() => {
|
||||
function renderBlock(
|
||||
const renderBlock: ContentRenderer<TabLayoutData> = (
|
||||
tabData: TabLayoutData,
|
||||
ready: boolean,
|
||||
disablePointerEvents: boolean,
|
||||
onMagnifyToggle: () => void,
|
||||
onClose: () => void,
|
||||
dragHandleRef: React.RefObject<HTMLDivElement>
|
||||
) {
|
||||
) => {
|
||||
if (!tabData.blockId || !ready) {
|
||||
return null;
|
||||
}
|
||||
const layoutModel: LayoutComponentModel = {
|
||||
onClose: onClose,
|
||||
onMagnifyToggle: onMagnifyToggle,
|
||||
dragHandleRef: dragHandleRef,
|
||||
disablePointerEvents,
|
||||
onClose,
|
||||
onMagnifyToggle,
|
||||
dragHandleRef,
|
||||
};
|
||||
return <Block key={tabData.blockId} blockId={tabData.blockId} layoutModel={layoutModel} preview={false} />;
|
||||
}
|
||||
};
|
||||
|
||||
function renderPreview(tabData: TabLayoutData) {
|
||||
return <Block key={tabData.blockId} blockId={tabData.blockId} layoutModel={null} preview={true} />;
|
||||
|
@ -17,7 +17,7 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { DropTargetMonitor, useDrag, useDragLayer, useDrop } from "react-dnd";
|
||||
import { DropTargetMonitor, XYCoord, useDrag, useDragLayer, useDrop } from "react-dnd";
|
||||
import { debounce, throttle } from "throttle-debounce";
|
||||
import { useDevicePixelRatio } from "use-device-pixel-ratio";
|
||||
import { globalLayoutTransformsMap } from "./layoutAtom";
|
||||
@ -132,11 +132,9 @@ function TileLayoutComponent<T>({ layoutTreeStateAtom, contents, getCursorPoint
|
||||
dragClientOffset: monitor.getClientOffset(),
|
||||
}));
|
||||
|
||||
// Effect to detect when the cursor leaves the TileLayout hit trap so we can remove any placeholders. This cannot be done using pointer capture
|
||||
// because that conflicts with the DnD layer.
|
||||
useEffect(
|
||||
debounce(100, () => {
|
||||
const cursorPoint = getCursorPoint?.() ?? dragClientOffset;
|
||||
const checkForCursorBounds = useCallback(
|
||||
debounce(100, (dragClientOffset: XYCoord) => {
|
||||
const cursorPoint = dragClientOffset ?? getCursorPoint?.();
|
||||
if (cursorPoint && displayContainerRef.current) {
|
||||
const displayContainerRect = displayContainerRef.current.getBoundingClientRect();
|
||||
const normalizedX = cursorPoint.x - displayContainerRect.x;
|
||||
@ -151,9 +149,13 @@ function TileLayoutComponent<T>({ layoutTreeStateAtom, contents, getCursorPoint
|
||||
}
|
||||
}
|
||||
}),
|
||||
[dragClientOffset]
|
||||
[getCursorPoint, displayContainerRef, dispatch]
|
||||
);
|
||||
|
||||
// Effect to detect when the cursor leaves the TileLayout hit trap so we can remove any placeholders. This cannot be done using pointer capture
|
||||
// because that conflicts with the DnD layer.
|
||||
useEffect(() => checkForCursorBounds(dragClientOffset), [dragClientOffset]);
|
||||
|
||||
/**
|
||||
* Callback to update the transforms on the displayed leafs and move the overlay over the display layer when dragging.
|
||||
*/
|
||||
@ -279,6 +281,7 @@ function TileLayoutComponent<T>({ layoutTreeStateAtom, contents, getCursorPoint
|
||||
<DisplayNodesWrapper
|
||||
contents={contents}
|
||||
ready={animate}
|
||||
activeDrag={activeDrag}
|
||||
onLeafMagnifyToggle={onLeafMagnifyToggle}
|
||||
onLeafClose={onLeafClose}
|
||||
layoutTreeState={layoutTreeState}
|
||||
@ -345,6 +348,11 @@ interface DisplayNodesWrapperProps<T> {
|
||||
* Determines whether the leaf nodes are ready to be displayed to the user.
|
||||
*/
|
||||
ready: boolean;
|
||||
|
||||
/**
|
||||
* Determines if a drag operation is in progress.
|
||||
*/
|
||||
activeDrag: boolean;
|
||||
}
|
||||
|
||||
const DisplayNodesWrapper = memo(
|
||||
@ -355,6 +363,7 @@ const DisplayNodesWrapper = memo(
|
||||
onLeafMagnifyToggle,
|
||||
layoutLeafTransforms,
|
||||
ready,
|
||||
activeDrag,
|
||||
}: DisplayNodesWrapperProps<T>) => {
|
||||
if (!layoutLeafTransforms) {
|
||||
return null;
|
||||
@ -366,6 +375,7 @@ const DisplayNodesWrapper = memo(
|
||||
key={leaf.id}
|
||||
layoutNode={leaf}
|
||||
contents={contents}
|
||||
activeDrag={activeDrag}
|
||||
transform={layoutLeafTransforms[leaf.id]}
|
||||
onLeafClose={onLeafClose}
|
||||
onLeafMagnifyToggle={onLeafMagnifyToggle}
|
||||
@ -398,11 +408,17 @@ interface DisplayNodeProps<T> {
|
||||
* @param node The node that is closed.
|
||||
*/
|
||||
onLeafClose: (node: LayoutNode<T>) => void;
|
||||
|
||||
/**
|
||||
* Determines whether a leaf's contents should be displayed to the user.
|
||||
*/
|
||||
ready: boolean;
|
||||
|
||||
/**
|
||||
* Determines if a drag operation is in progress.
|
||||
*/
|
||||
activeDrag: boolean;
|
||||
|
||||
/**
|
||||
* Any class names to add to the component.
|
||||
*/
|
||||
@ -427,6 +443,7 @@ const DisplayNode = memo(
|
||||
onLeafMagnifyToggle,
|
||||
onLeafClose,
|
||||
ready,
|
||||
activeDrag,
|
||||
className,
|
||||
}: DisplayNodeProps<T>) => {
|
||||
const tileNodeRef = useRef<HTMLDivElement>(null);
|
||||
@ -508,11 +525,18 @@ const DisplayNode = memo(
|
||||
return (
|
||||
layoutNode.data && (
|
||||
<div key="leaf" className="tile-leaf">
|
||||
{contents.renderContent(layoutNode.data, ready, onMagnifyToggle, onClose, dragHandleRef)}
|
||||
{contents.renderContent(
|
||||
layoutNode.data,
|
||||
ready,
|
||||
activeDrag,
|
||||
onMagnifyToggle,
|
||||
onClose,
|
||||
dragHandleRef
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}, [layoutNode.data, ready, onClose]);
|
||||
}, [layoutNode.data, ready, activeDrag, onClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -885,17 +909,18 @@ interface PlaceholderProps<T> {
|
||||
/**
|
||||
* An overlay to preview pending actions on the layout tree.
|
||||
*/
|
||||
const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefsAtom, style }: PlaceholderProps<T>) => {
|
||||
const Placeholder = memo(<T,>({ layoutTreeState, overlayContainerRef, nodeRefsAtom, style }: PlaceholderProps<T>) => {
|
||||
const [placeholderOverlay, setPlaceholderOverlay] = useState<ReactNode>(null);
|
||||
|
||||
const nodeRefs = useAtomValue(nodeRefsAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const updatePlaceholder = useCallback(
|
||||
throttle(10, (pendingAction: LayoutTreeAction) => {
|
||||
let newPlaceholderOverlay: ReactNode;
|
||||
if (overlayContainerRef?.current) {
|
||||
switch (layoutTreeState?.pendingAction?.type) {
|
||||
switch (pendingAction?.type) {
|
||||
case LayoutTreeActionType.Move: {
|
||||
const action = layoutTreeState.pendingAction as LayoutTreeMoveNodeAction<T>;
|
||||
const action = pendingAction as LayoutTreeMoveNodeAction<T>;
|
||||
let parentId: string;
|
||||
if (action.insertAtRoot) {
|
||||
parentId = layoutTreeState.rootNode.id;
|
||||
@ -943,7 +968,8 @@ const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefsAtom, s
|
||||
parentNode.flexDirection === FlexDirection.Column &&
|
||||
targetBoundingRect.height / 2;
|
||||
placeholderDimensions.left +=
|
||||
parentNode.flexDirection === FlexDirection.Row && targetBoundingRect.width / 2;
|
||||
parentNode.flexDirection === FlexDirection.Row &&
|
||||
targetBoundingRect.width / 2;
|
||||
} else {
|
||||
// Otherwise, place the placeholder between the target node (the one after which it will be inserted) and the next node
|
||||
placeholderDimensions.top +=
|
||||
@ -956,13 +982,15 @@ const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefsAtom, s
|
||||
}
|
||||
|
||||
const placeholderTransform = setTransform(placeholderDimensions);
|
||||
newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />;
|
||||
newPlaceholderOverlay = (
|
||||
<div className="placeholder" style={{ ...placeholderTransform }} />
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LayoutTreeActionType.Swap: {
|
||||
const action = layoutTreeState.pendingAction as LayoutTreeSwapNodeAction;
|
||||
const action = pendingAction as LayoutTreeSwapNodeAction;
|
||||
// console.log("placeholder for swap", action);
|
||||
const targetNodeId = action.node1Id;
|
||||
const targetRef = nodeRefs.get(targetNodeId);
|
||||
@ -987,11 +1015,17 @@ const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefsAtom, s
|
||||
}
|
||||
}
|
||||
setPlaceholderOverlay(newPlaceholderOverlay);
|
||||
}, [layoutTreeState.pendingAction, nodeRefs, overlayContainerRef]);
|
||||
}),
|
||||
[nodeRefs, overlayContainerRef, layoutTreeState.rootNode]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updatePlaceholder(layoutTreeState.pendingAction);
|
||||
}, [layoutTreeState.pendingAction, updatePlaceholder]);
|
||||
|
||||
return (
|
||||
<div className="placeholder-container" style={style}>
|
||||
{placeholderOverlay}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -214,6 +214,7 @@ export type WritableLayoutTreeStateAtom<T> = WritableAtom<LayoutTreeState<T>, [v
|
||||
export type ContentRenderer<T> = (
|
||||
data: T,
|
||||
ready: boolean,
|
||||
disablePointerEvents: boolean,
|
||||
onMagnifyToggle: () => void,
|
||||
onClose: () => void,
|
||||
dragHandleRef: React.RefObject<HTMLDivElement>
|
||||
|
Loading…
Reference in New Issue
Block a user