Skip to content
Snippets Groups Projects
Commit 8dbcceac authored by Ben Mullins's avatar Ben Mullins Committed by Jesse Baker
Browse files

Issue #3458535 by balintbrews, jessebaker: Middle click + drag doesn't work...

Issue #3458535 by balintbrews, jessebaker: Middle click + drag doesn't work correctly if middle click is inside preview iframe
parent e86c7d86
No related branches found
No related tags found
1 merge request!145Fixed bug where scroll pos would jump wildly up to the top left corner. Fixed...
Pipeline #246483 passed
......@@ -34,9 +34,3 @@
display: flex;
gap: 2em;
}
.modifierKeyPressed .canvas > *,
.isPanning .canvas > * {
user-select: none;
pointer-events: none;
}
......@@ -10,6 +10,9 @@ import {
canvasViewPortZoomIn,
canvasViewPortZoomOut,
setCanvasViewPort,
selectPanning,
setPanningIFrame,
setPanningParent,
} from '@/features/ui/uiSlice';
const Canvas = () => {
......@@ -18,9 +21,10 @@ const Canvas = () => {
const canvasPaneRef = useRef<HTMLDivElement | null>(null);
const animFrameIdRef = useRef<number | null>(null);
const previewsContainerRef = useRef<HTMLDivElement | null>(null);
const [isPanning, setIsPanning] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const canvasViewPort = useAppSelector(selectCanvasViewPort);
const { isPanning, isPanningIFrame, isPanningParent } =
useAppSelector(selectPanning);
const [modifierKeyPressed, setModifierKeyPressed] = useState(false);
const modifierKeyPressedRef = useRef(false);
useHotkeys(['NumpadAdd', 'Equal'], () => dispatch(canvasViewPortZoomIn()));
......@@ -35,6 +39,16 @@ const Canvas = () => {
keydown: false,
keyup: true,
});
const isPanningParentRef = useRef(isPanningParent);
const isPanningIFrameRef = useRef(isPanningIFrame);
useEffect(() => {
isPanningParentRef.current = isPanningParent;
}, [isPanningParent]);
useEffect(() => {
isPanningIFrameRef.current = isPanningIFrame;
}, [isPanningIFrame]);
useEffect(() => {
modifierKeyPressedRef.current = modifierKeyPressed;
......@@ -59,12 +73,16 @@ const Canvas = () => {
setModifierKeyPressed(false);
break;
case 'dispatchMiddleMouseDown':
setIsPanning(true);
// @todo the coordinates of where the iframe is clicked should be added probably to the top left position of the iframe on the canvas.
dispatch(setPanningIFrame(true));
setStartPos(event.data.coordinates);
break;
case 'dispatchMiddleMouseUp':
setIsPanning(false);
dispatch(setPanningIFrame(false));
dispatch(setPanningParent(false));
break;
case 'dispatchMouseMove':
isPanningIFrameRef.current &&
handlePreviewMouseMove(event.data.coordinates);
break;
}
}
......@@ -106,18 +124,19 @@ const Canvas = () => {
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.button === 1) {
const { clientX, clientY } = e;
setIsPanning(true);
dispatch(setPanningParent(true));
if (canvasPaneRef.current) {
setStartPos({
x: clientX + canvasPaneRef.current.scrollLeft,
y: clientY + canvasPaneRef.current.scrollTop,
});
}
e.preventDefault();
}
};
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (isPanning) {
const handleCanvasMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (isPanningParentRef.current) {
const { clientX, clientY } = e;
const translationX = startPos.x - clientX;
const translationY = startPos.y - clientY;
......@@ -144,9 +163,28 @@ const Canvas = () => {
}
};
const handlePreviewMouseMove = ({ x, y }: { x: number; y: number }) => {
if (isPanningIFrameRef.current) {
const translationX = startPos.x - x;
const translationY = startPos.y - y;
if (animFrameIdRef.current) {
cancelAnimationFrame(animFrameIdRef.current);
}
animFrameIdRef.current = requestAnimationFrame(() => {
if (canvasPaneRef.current) {
canvasPaneRef.current.scrollLeft += translationX;
canvasPaneRef.current.scrollTop += translationY;
}
});
}
};
const handleMouseUp = useCallback(() => {
setIsPanning(false);
}, []);
dispatch(setPanningParent(false));
dispatch(setPanningIFrame(false));
}, [dispatch]);
const handleWheel = useCallback(
(e: WheelEvent) => {
......@@ -166,16 +204,6 @@ const Canvas = () => {
[dispatch],
);
useEffect(() => {
if (previewsContainerRef.current) {
if (isPanning) {
previewsContainerRef.current.style.pointerEvents = 'none';
} else {
previewsContainerRef.current.style.pointerEvents = 'all';
}
}
}, [isPanning]);
useEffect(() => {
if (animFrameIdRef.current) {
cancelAnimationFrame(animFrameIdRef.current);
......@@ -209,7 +237,7 @@ const Canvas = () => {
[styles.isPanning]: isPanning,
})}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseMove={handleCanvasMouseMove}
onScroll={handlePaneScroll}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
......
......@@ -12,6 +12,7 @@ import { useAppDispatch, useAppSelector } from '@/app/hooks';
import { Card, Progress } from '@radix-ui/themes';
import {
selectDragging,
selectPanning,
selectHoveredComponent,
selectSelectedComponent,
setHoveredComponent,
......@@ -45,6 +46,7 @@ const Viewport: React.FC<ViewportProps> = (props) => {
const hoveredComponent = useAppSelector(selectHoveredComponent);
const iframeDocumentRef = useRef<Document | null>(null);
const { isDragging } = useAppSelector(selectDragging);
const { isPanning } = useAppSelector(selectPanning);
const dispatch = useAppDispatch();
useIframeKeyHandlers(iframeRef);
useSyncIframeHeightToContent(iframeRef, height, width);
......@@ -169,6 +171,10 @@ const Viewport: React.FC<ViewportProps> = (props) => {
const initComponentClick = (listItemEl: HTMLElement) => {
listItemEl.addEventListener('click', function (event: MouseEvent) {
// In safari middle mouse click fires the click event with button === 1
if (event.button === 1) {
return;
}
event.stopPropagation();
if (event.target) {
const target = event.currentTarget as HTMLElement;
......@@ -262,7 +268,7 @@ const Viewport: React.FC<ViewportProps> = (props) => {
data-xb-preview={previewId}
title="Preview"
></iframe>
{!isDragging && (
{!isDragging && !isPanning && (
<>
<Outline
elementId={selectedComponent}
......
......@@ -8,6 +8,17 @@ export interface DraggingStatus {
previewDragging: boolean;
}
export interface PanningStatus {
isPanning: boolean;
isPanningIFrame: boolean;
isPanningParent: boolean;
}
export interface PrimaryMenuState {
activeMenu: string;
isHidden: boolean;
}
export interface CanvasViewPort {
x: number;
y: number;
......@@ -17,6 +28,7 @@ export interface CanvasViewPort {
export interface uiSliceState {
pending: boolean;
dragging: DraggingStatus;
panning: PanningStatus;
selectedComponent: string | undefined; //uuid of component
hoveredComponent: string | undefined; //uuid of component
contextualPanelOpen: boolean;
......@@ -37,6 +49,11 @@ export const initialState: uiSliceState = {
listDragging: false,
previewDragging: false,
},
panning: {
isPanning: false,
isPanningIFrame: false,
isPanningParent: false,
},
selectedComponent: undefined,
hoveredComponent: undefined,
contextualPanelOpen: false,
......@@ -96,6 +113,18 @@ export const uiSlice = createAppSlice({
state.dragging.isDragging = action.payload;
state.dragging.listDragging = action.payload;
}),
setPanningIFrame: create.reducer(
(state, action: PayloadAction<boolean>) => {
state.panning.isPanning = action.payload;
state.panning.isPanningIFrame = action.payload;
},
),
setPanningParent: create.reducer(
(state, action: PayloadAction<boolean>) => {
state.panning.isPanning = action.payload;
state.panning.isPanningParent = action.payload;
},
),
setSelectedComponent: create.reducer(
(state, action: PayloadAction<string>) => {
state.selectedComponent = action.payload;
......@@ -144,6 +173,9 @@ export const uiSlice = createAppSlice({
// You can define your selectors here. These selectors receive the slice
// state as their first argument.
selectors: {
selectPanning: (ui): PanningStatus => {
return ui.panning;
},
selectDragging: (ui): DraggingStatus => {
return ui.dragging;
},
......@@ -168,6 +200,8 @@ export const {
setTreeDragging,
setPreviewDragging,
setListDragging,
setPanningIFrame,
setPanningParent,
setSelectedComponent,
setHoveredComponent,
unsetSelectedComponent,
......@@ -181,6 +215,7 @@ export const {
// Selectors returned by `slice.selectors` take the root state as their first argument.
export const {
selectDragging,
selectPanning,
selectSelectedComponent,
selectHoveredComponent,
selectContextualPanelOpen,
......
......@@ -43,24 +43,36 @@ function useIframeKeyHandlers(iframeRef: React.RefObject<HTMLIFrameElement>) {
}
function notifyParentDocumentMouse(event: MouseEvent) {
if (event.button !== 1) {
return;
}
if (event.type === 'mousedown') {
window.parent.postMessage(
{
type: 'dispatchMiddleMouseDown',
coordinates: { x: event.clientX, y: event.clientY },
},
'*',
);
return;
}
if (event.type === 'mouseup') {
window.parent.postMessage('dispatchMiddleMouseUp', '*');
return;
switch (event.type) {
case 'mousemove':
window.parent.postMessage(
{
type: 'dispatchMouseMove',
coordinates: { x: event.clientX, y: event.clientY },
},
'*',
);
break;
case 'mousedown':
if (event.button === 1) {
window.parent.postMessage(
{
type: 'dispatchMiddleMouseDown',
coordinates: { x: event.clientX, y: event.clientY },
},
'*',
);
event.preventDefault();
}
break;
case 'mouseup':
if (event.button === 1) {
window.parent.postMessage('dispatchMiddleMouseUp', '*');
}
break;
}
}
const handleLoad = useCallback((event: Event) => {
const iframe = event.currentTarget as HTMLIFrameElement | null;
......@@ -79,14 +91,14 @@ function useIframeKeyHandlers(iframeRef: React.RefObject<HTMLIFrameElement>) {
},
);
(['mousedown', 'mouseup'] as Array<keyof HTMLElementEventMap>).forEach(
(eventType) => {
iframeContentDoc?.body.addEventListener(
eventType,
notifyParentDocumentMouse as EventListener,
);
},
);
(
['mousedown', 'mouseup', 'mousemove'] as Array<keyof HTMLElementEventMap>
).forEach((eventType) => {
iframeContentDoc?.body.addEventListener(
eventType,
notifyParentDocumentMouse as EventListener,
);
});
}, []);
useEffect(() => {
......
......@@ -15,10 +15,13 @@ function useSyncIframeHeightToContent(
const iframe = iframeRef.current;
if (iframe && iframe.contentDocument) {
const iframeHTML = iframe.contentDocument.documentElement;
const iframeBody = iframe.contentDocument.body;
window.requestAnimationFrame(() => {
iframe.style.height = iframeHTML.offsetHeight + 'px';
iframe.style.width = width + 'px';
iframe.style.minHeight = height + 'px';
iframeHTML.style.minHeight = height + 'px';
iframeBody.style.minHeight = height + 'px';
});
}
}, [iframeRef, height, width]);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment