mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-02-18 13:29:36 +01:00
scroll the closest element to center (#1670)
Co-authored-by: Sanghyeon Lee <yongdamsh@gmail.com>
This commit is contained in:
parent
0db7ac78c4
commit
fa359034c5
@ -4,7 +4,7 @@ import { getDefaultAppState } from "../appState";
|
||||
import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { t } from "../i18n";
|
||||
import { getNormalizedZoom, calculateScrollCenter } from "../scene";
|
||||
import { getNormalizedZoom, normalizeScroll } from "../scene";
|
||||
import { KEYS } from "../keys";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import useIsMobile from "../is-mobile";
|
||||
@ -202,15 +202,22 @@ export const actionZoomToFit = register({
|
||||
name: "zoomToFit",
|
||||
perform: (elements, appState) => {
|
||||
const nonDeletedElements = elements.filter((element) => !element.isDeleted);
|
||||
const scrollCenter = calculateScrollCenter(nonDeletedElements);
|
||||
const commonBounds = getCommonBounds(nonDeletedElements);
|
||||
const zoom = calculateZoom(commonBounds, appState.zoom, scrollCenter);
|
||||
const [x1, y1, x2, y2] = commonBounds;
|
||||
const centerX = (x1 + x2) / 2;
|
||||
const centerY = (y1 + y2) / 2;
|
||||
const scrollX = normalizeScroll(window.innerWidth / 2 - centerX);
|
||||
const scrollY = normalizeScroll(window.innerHeight / 2 - centerY);
|
||||
const zoom = calculateZoom(commonBounds, appState.zoom, {
|
||||
scrollX,
|
||||
scrollY,
|
||||
});
|
||||
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
scrollX: scrollCenter.scrollX,
|
||||
scrollY: scrollCenter.scrollY,
|
||||
scrollX,
|
||||
scrollY,
|
||||
zoom,
|
||||
},
|
||||
commitToHistory: false,
|
||||
|
@ -847,6 +847,8 @@ class App extends React.Component<any, AppState> {
|
||||
remoteElements.filter((element: { isDeleted: boolean }) => {
|
||||
return !element.isDeleted;
|
||||
}),
|
||||
this.state,
|
||||
this.canvas,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -256,7 +256,9 @@ const LayerUI = ({
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({ ...calculateScrollCenter(elements) });
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
@ -276,6 +278,7 @@ const LayerUI = ({
|
||||
onRoomCreate={onRoomCreate}
|
||||
onRoomDestroy={onRoomDestroy}
|
||||
onLockToggle={onLockToggle}
|
||||
canvas={canvas}
|
||||
/>
|
||||
) : (
|
||||
<div className="layer-ui__wrapper">
|
||||
|
@ -27,6 +27,7 @@ type MobileMenuProps = {
|
||||
onUsernameChange: (username: string) => void;
|
||||
onRoomDestroy: () => void;
|
||||
onLockToggle: () => void;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
};
|
||||
|
||||
export const MobileMenu = ({
|
||||
@ -39,6 +40,7 @@ export const MobileMenu = ({
|
||||
onUsernameChange,
|
||||
onRoomDestroy,
|
||||
onLockToggle,
|
||||
canvas,
|
||||
}: MobileMenuProps) => (
|
||||
<>
|
||||
{appState.isLoading && <LoadingMessage />}
|
||||
@ -131,7 +133,9 @@ export const MobileMenu = ({
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({ ...calculateScrollCenter(elements) });
|
||||
setAppState({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
|
@ -271,7 +271,7 @@ export const importFromBackend = async (
|
||||
}
|
||||
|
||||
elements = data.elements || elements;
|
||||
appState = data.appState || appState;
|
||||
appState = { ...appState, ...data.appState };
|
||||
} catch (error) {
|
||||
window.alert(t("alerts.importBackendFailed"));
|
||||
console.error(error);
|
||||
|
@ -84,6 +84,5 @@ export const restoreFromLocalStorage = () => {
|
||||
// Do nothing because appState is already null
|
||||
}
|
||||
}
|
||||
|
||||
return restore(elements, appState);
|
||||
};
|
||||
|
@ -121,7 +121,10 @@ export const restore = (
|
||||
}, [] as ExcalidrawElement[]);
|
||||
|
||||
if (opts?.scrollToContent && savedState) {
|
||||
savedState = { ...savedState, ...calculateScrollCenter(elements) };
|
||||
savedState = {
|
||||
...savedState,
|
||||
...calculateScrollCenter(elements, savedState, null),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
|
||||
import { rotate } from "../math";
|
||||
import { distance2d, rotate } from "../math";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import { Drawable, Op } from "roughjs/bin/core";
|
||||
import { Point } from "../types";
|
||||
@ -342,3 +342,27 @@ export const getResizedElementAbsoluteCoords = (
|
||||
maxY + element.y,
|
||||
];
|
||||
};
|
||||
|
||||
export const getClosestElementBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
from: { x: number; y: number },
|
||||
): [number, number, number, number] => {
|
||||
if (!elements.length) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
let minDistance = Infinity;
|
||||
let closestElement = elements[0];
|
||||
|
||||
elements.forEach((element) => {
|
||||
const [x1, y1, x2, y2] = getElementBounds(element);
|
||||
const distance = distance2d((x1 + x2) / 2, (y1 + y2) / 2, from.x, from.y);
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestElement = element;
|
||||
}
|
||||
});
|
||||
|
||||
return getElementBounds(closestElement);
|
||||
};
|
||||
|
@ -17,6 +17,7 @@ export {
|
||||
getCommonBounds,
|
||||
getDiamondPoints,
|
||||
getArrowPoints,
|
||||
getClosestElementBounds,
|
||||
} from "./bounds";
|
||||
|
||||
export {
|
||||
|
@ -1,12 +1,43 @@
|
||||
import { FlooredNumber } from "../types";
|
||||
import { AppState, FlooredNumber } from "../types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { getCommonBounds } from "../element";
|
||||
import { getCommonBounds, getClosestElementBounds } from "../element";
|
||||
|
||||
import {
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
} from "../utils";
|
||||
|
||||
export const normalizeScroll = (pos: number) =>
|
||||
Math.floor(pos) as FlooredNumber;
|
||||
|
||||
function isOutsideViewPort(
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
cords: Array<number>,
|
||||
) {
|
||||
const [x1, y1, x2, y2] = cords;
|
||||
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x1, sceneY: y1 },
|
||||
appState,
|
||||
canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
const { x: viewportX2, y: viewportY2 } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x2, sceneY: y2 },
|
||||
appState,
|
||||
canvas,
|
||||
window.devicePixelRatio,
|
||||
);
|
||||
return (
|
||||
viewportX2 - viewportX1 > window.innerWidth ||
|
||||
viewportY2 - viewportY1 > window.innerHeight
|
||||
);
|
||||
}
|
||||
|
||||
export const calculateScrollCenter = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
): { scrollX: FlooredNumber; scrollY: FlooredNumber } => {
|
||||
if (!elements.length) {
|
||||
return {
|
||||
@ -14,8 +45,19 @@ export const calculateScrollCenter = (
|
||||
scrollY: normalizeScroll(0),
|
||||
};
|
||||
}
|
||||
|
||||
const [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||
const scale = window.devicePixelRatio;
|
||||
let [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||
if (isOutsideViewPort(appState, canvas, [x1, y1, x2, y2])) {
|
||||
[x1, y1, x2, y2] = getClosestElementBounds(
|
||||
elements,
|
||||
viewportCoordsToSceneCoords(
|
||||
{ clientX: appState.scrollX, clientY: appState.scrollY },
|
||||
appState,
|
||||
canvas,
|
||||
scale,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const centerX = (x1 + x2) / 2;
|
||||
const centerY = (y1 + y2) / 2;
|
||||
|
Loading…
Reference in New Issue
Block a user