Compare commits
32 Commits
master
...
arnost/scr
Author | SHA1 | Date | |
---|---|---|---|
|
b0cdd00c2a | ||
|
6711735b27 | ||
|
803e14ada1 | ||
|
4469c02191 | ||
|
04e23e1d29 | ||
|
d24a032dbb | ||
|
76d3930983 | ||
|
af6e64ffc2 | ||
|
4e9039e850 | ||
|
132750f753 | ||
|
71eb3023b2 | ||
|
6d165971fc | ||
|
9562e4309f | ||
|
e8e391e465 | ||
|
92be92071a | ||
|
71918e57a8 | ||
|
c0bd9027cb | ||
|
7336b1c276 | ||
|
7fb6c23715 | ||
|
82014fe670 | ||
|
bc44c3f947 | ||
|
19ba107041 | ||
|
381ef93956 | ||
|
f82363aae9 | ||
|
485c57fd59 | ||
|
35b43c14d8 | ||
|
f7e8056abe | ||
|
71f7960606 | ||
|
2998573e79 | ||
|
209934c90a | ||
|
a8158691b7 | ||
|
75f8e904cc |
@ -16,7 +16,7 @@ export type ActionResult =
|
||||
elements?: readonly ExcalidrawElement[] | null;
|
||||
appState?: MarkOptional<
|
||||
AppState,
|
||||
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||
"offsetTop" | "offsetLeft" | "width" | "height" | "scrollConstraints"
|
||||
> | null;
|
||||
files?: BinaryFiles | null;
|
||||
commitToHistory: boolean;
|
||||
|
@ -17,7 +17,7 @@ const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
|
||||
|
||||
export const getDefaultAppState = (): Omit<
|
||||
AppState,
|
||||
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||
"offsetTop" | "offsetLeft" | "width" | "height" | "scrollConstraints"
|
||||
> => {
|
||||
return {
|
||||
showWelcomeScreen: false,
|
||||
@ -206,6 +206,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
pendingImageElementId: { browser: false, export: false, server: false },
|
||||
showHyperlinkPopup: { browser: false, export: false, server: false },
|
||||
selectedLinearElement: { browser: true, export: false, server: false },
|
||||
scrollConstraints: { browser: false, export: false, server: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <
|
||||
|
@ -209,7 +209,11 @@ import {
|
||||
isSomeElementSelected,
|
||||
} from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
import { RenderConfig, ScrollBars } from "../scene/types";
|
||||
import {
|
||||
RenderConfig,
|
||||
ScrollBars,
|
||||
ConstrainedScrollValues,
|
||||
} from "../scene/types";
|
||||
import { getStateForZoom } from "../scene/zoom";
|
||||
import { findShapeByKey, SHAPES } from "../shapes";
|
||||
import {
|
||||
@ -229,6 +233,7 @@ import {
|
||||
FrameNameBoundsCache,
|
||||
SidebarName,
|
||||
SidebarTabName,
|
||||
ScrollConstraints,
|
||||
} from "../types";
|
||||
import {
|
||||
debounce,
|
||||
@ -257,6 +262,7 @@ import {
|
||||
muteFSAbortError,
|
||||
isTestEnv,
|
||||
easeOut,
|
||||
isShallowEqual,
|
||||
} from "../utils";
|
||||
import {
|
||||
embeddableURLValidator,
|
||||
@ -380,6 +386,7 @@ const ExcalidrawAppStateContext = React.createContext<AppState>({
|
||||
height: 0,
|
||||
offsetLeft: 0,
|
||||
offsetTop: 0,
|
||||
scrollConstraints: null,
|
||||
});
|
||||
ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext";
|
||||
|
||||
@ -478,6 +485,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
lastPointerDown: React.PointerEvent<HTMLElement> | null = null;
|
||||
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
||||
lastViewportPosition = { x: 0, y: 0 };
|
||||
private memoizedScrollConstraints: ReturnType<
|
||||
App["calculateConstraints"]
|
||||
> | null = null;
|
||||
|
||||
constructor(props: AppProps) {
|
||||
super(props);
|
||||
@ -489,7 +499,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
gridModeEnabled = false,
|
||||
theme = defaultAppState.theme,
|
||||
name = defaultAppState.name,
|
||||
scrollConstraints,
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
...defaultAppState,
|
||||
theme,
|
||||
@ -501,6 +513,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
name,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
scrollConstraints: scrollConstraints ?? null,
|
||||
};
|
||||
this.id = nanoid();
|
||||
this.library = new Library(this);
|
||||
@ -532,6 +545,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
resetCursor: this.resetCursor,
|
||||
updateFrameRendering: this.updateFrameRendering,
|
||||
toggleSidebar: this.toggleSidebar,
|
||||
setScrollConstraints: this.setScrollConstraints,
|
||||
} as const;
|
||||
if (typeof excalidrawRef === "function") {
|
||||
excalidrawRef(api);
|
||||
@ -1546,7 +1560,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
isLoading: false,
|
||||
toast: this.state.toast,
|
||||
};
|
||||
if (initialData?.scrollToContent) {
|
||||
if (this.props.scrollConstraints) {
|
||||
scene.appState = {
|
||||
...scene.appState,
|
||||
...this.calculateConstrainedScrollCenter(
|
||||
this.props.scrollConstraints,
|
||||
scene.appState,
|
||||
),
|
||||
};
|
||||
} else if (initialData?.scrollToContent) {
|
||||
scene.appState = {
|
||||
...scene.appState,
|
||||
...calculateScrollCenter(
|
||||
@ -1557,6 +1579,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
height: this.state.height,
|
||||
offsetTop: this.state.offsetTop,
|
||||
offsetLeft: this.state.offsetLeft,
|
||||
scrollConstraints: this.state.scrollConstraints,
|
||||
},
|
||||
null,
|
||||
),
|
||||
@ -1843,10 +1866,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
||||
this.updateEmbeddables();
|
||||
if (
|
||||
!this.state.showWelcomeScreen &&
|
||||
!this.scene.getElementsIncludingDeleted().length
|
||||
) {
|
||||
const elementsIncludingDeleted = this.scene.getElementsIncludingDeleted();
|
||||
if (!this.state.showWelcomeScreen && !elementsIncludingDeleted.length) {
|
||||
this.setState({ showWelcomeScreen: true });
|
||||
}
|
||||
|
||||
@ -1973,7 +1994,73 @@ class App extends React.Component<AppProps, AppState> {
|
||||
),
|
||||
);
|
||||
}
|
||||
this.renderScene();
|
||||
|
||||
let constraintedScrollState;
|
||||
if (
|
||||
this.state.scrollConstraints &&
|
||||
!this.state.scrollConstraints.isAnimating
|
||||
) {
|
||||
const {
|
||||
scrollX,
|
||||
scrollY,
|
||||
width,
|
||||
height,
|
||||
scrollConstraints,
|
||||
zoom,
|
||||
cursorButton,
|
||||
} = this.state;
|
||||
|
||||
const canUseMemoizedConstraints =
|
||||
isShallowEqual(scrollConstraints, prevState.scrollConstraints ?? {}) &&
|
||||
isShallowEqual(
|
||||
{ width, height, zoom: zoom.value, cursorButton },
|
||||
{
|
||||
width: prevState.width,
|
||||
height: prevState.height,
|
||||
zoom: prevState.zoom.value,
|
||||
cursorButton: prevState.cursorButton,
|
||||
} ?? {},
|
||||
);
|
||||
|
||||
const calculatedConstraints =
|
||||
canUseMemoizedConstraints && !!this.memoizedScrollConstraints
|
||||
? this.memoizedScrollConstraints
|
||||
: this.calculateConstraints({
|
||||
scrollConstraints,
|
||||
width,
|
||||
height,
|
||||
zoom,
|
||||
cursorButton,
|
||||
});
|
||||
|
||||
this.memoizedScrollConstraints = calculatedConstraints;
|
||||
|
||||
const constrainedScrollValues = this.constrainScrollValues({
|
||||
...calculatedConstraints,
|
||||
scrollX,
|
||||
scrollY,
|
||||
});
|
||||
|
||||
const isViewportOutsideOfConstrainedArea =
|
||||
this.isViewportOutsideOfConstrainedArea({
|
||||
scrollX,
|
||||
scrollY,
|
||||
width,
|
||||
height,
|
||||
scrollConstraints,
|
||||
});
|
||||
|
||||
constraintedScrollState = this.handleConstrainedScrollStateChange({
|
||||
...constrainedScrollValues,
|
||||
shouldAnimate:
|
||||
isViewportOutsideOfConstrainedArea &&
|
||||
this.state.cursorButton !== "down" &&
|
||||
prevState.zoom.value === this.state.zoom.value &&
|
||||
elementsIncludingDeleted.length > 0, // Do not animate when app is initialized but scene is empty - this would cause flickering
|
||||
});
|
||||
}
|
||||
|
||||
this.renderScene(constraintedScrollState);
|
||||
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
|
||||
|
||||
// Do not notify consumers if we're still loading the scene. Among other
|
||||
@ -1989,7 +2076,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
private renderScene = () => {
|
||||
private renderScene = (constrainedScroll?: ConstrainedScrollValues) => {
|
||||
const cursorButton: {
|
||||
[id: string]: string | undefined;
|
||||
} = {};
|
||||
@ -2060,10 +2147,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
canvas: this.canvas!,
|
||||
renderConfig: {
|
||||
selectionColor,
|
||||
scrollX: this.state.scrollX,
|
||||
scrollY: this.state.scrollY,
|
||||
scrollX: constrainedScroll?.scrollX ?? this.state.scrollX,
|
||||
scrollY: constrainedScroll?.scrollY ?? this.state.scrollY,
|
||||
viewBackgroundColor: this.state.viewBackgroundColor,
|
||||
zoom: this.state.zoom,
|
||||
zoom: constrainedScroll?.zoom ?? this.state.zoom,
|
||||
remotePointerViewportCoords: pointerViewportCoords,
|
||||
remotePointerButton: cursorButton,
|
||||
remoteSelectedElementIds,
|
||||
@ -2081,7 +2168,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
const scrolledOutside =
|
||||
// hide when editing text
|
||||
isTextElement(this.state.editingElement)
|
||||
isTextElement(this.state.editingElement) ||
|
||||
this.state.scrollConstraints
|
||||
? false
|
||||
: !atLeastOneVisibleElement && renderingElements.length > 0;
|
||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||
@ -2619,8 +2707,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
/** decimal fraction between 0.1 (10% zoom) and 30 (3000% zoom) */
|
||||
value: number,
|
||||
) => {
|
||||
this.setState({
|
||||
...getStateForZoom(
|
||||
this.setState(
|
||||
getStateForZoom(
|
||||
{
|
||||
viewportX: this.state.width / 2 + this.state.offsetLeft,
|
||||
viewportY: this.state.height / 2 + this.state.offsetTop,
|
||||
@ -2628,7 +2716,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
this.state,
|
||||
),
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
private cancelInProgresAnimation: (() => void) | null = null;
|
||||
@ -8153,6 +8241,424 @@ class App extends React.Component<AppProps, AppState> {
|
||||
await setLanguage(currentLang);
|
||||
this.setAppState({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scroll constraints of the application state.
|
||||
*
|
||||
* @param scrollConstraints - The new scroll constraints.
|
||||
*/
|
||||
public setScrollConstraints = (
|
||||
scrollConstraints: ScrollConstraints | null,
|
||||
) => {
|
||||
if (scrollConstraints) {
|
||||
const { scrollX, scrollY, width, height, zoom, cursorButton } =
|
||||
this.state;
|
||||
const constrainedScrollValues = this.constrainScrollValues({
|
||||
...this.calculateConstraints({
|
||||
scrollConstraints,
|
||||
zoom,
|
||||
cursorButton,
|
||||
width,
|
||||
height,
|
||||
}),
|
||||
scrollX,
|
||||
scrollY,
|
||||
});
|
||||
this.animateConstrainedScroll({
|
||||
...constrainedScrollValues,
|
||||
opts: {
|
||||
onEndCallback: () => {
|
||||
this.setState({
|
||||
scrollConstraints,
|
||||
viewModeEnabled: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
scrollConstraints: null,
|
||||
viewModeEnabled: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the scroll center coordinates and the optimal zoom level to fit the constrained scrollable area within the viewport.
|
||||
*
|
||||
* This method first calculates the necessary zoom level to fit the entire constrained scrollable area within the viewport.
|
||||
* Then it calculates the constraints for the viewport given the new zoom level and the current scrollable area dimensions.
|
||||
* The function returns an object containing the optimal scroll positions and zoom level.
|
||||
*
|
||||
* @param scrollConstraints - The constraints of the scrollable area including width, height, and position.
|
||||
* @param appState - An object containing the current horizontal and vertical scroll positions.
|
||||
* @returns An object containing the calculated optimal horizontal and vertical scroll positions and zoom level.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const { scrollX, scrollY, zoom } = this.calculateConstrainedScrollCenter(scrollConstraints, { scrollX, scrollY });
|
||||
*/
|
||||
public calculateConstrainedScrollCenter = (
|
||||
scrollConstraints: AppState["scrollConstraints"],
|
||||
{ scrollX, scrollY }: Pick<AppState, "scrollX" | "scrollY">,
|
||||
): {
|
||||
scrollX: AppState["scrollX"];
|
||||
scrollY: AppState["scrollY"];
|
||||
zoom: AppState["zoom"];
|
||||
} => {
|
||||
const { width, height, zoom } = this.state;
|
||||
|
||||
if (!scrollConstraints) {
|
||||
return { scrollX, scrollY, zoom };
|
||||
}
|
||||
|
||||
const { zoomLevelX, zoomLevelY, maxZoomLevel } = this.calculateZoomLevel(
|
||||
scrollConstraints,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
|
||||
// The zoom level to contain the whole constrained area in view
|
||||
const _zoom = {
|
||||
value: getNormalizedZoom(
|
||||
maxZoomLevel ?? Math.min(zoomLevelX, zoomLevelY),
|
||||
),
|
||||
};
|
||||
|
||||
const constraints = this.calculateConstraints({
|
||||
scrollConstraints,
|
||||
width,
|
||||
height,
|
||||
zoom: _zoom,
|
||||
cursorButton: "up",
|
||||
});
|
||||
|
||||
return {
|
||||
scrollX: constraints.minScrollX,
|
||||
scrollY: constraints.minScrollY,
|
||||
zoom: constraints.constrainedZoom,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the zoom levels necessary to fit the constrained scrollable area within the viewport on the X and Y axes.
|
||||
*
|
||||
* The function considers the dimensions of the scrollable area, the dimensions of the viewport, the viewport zoom factor,
|
||||
* and whether the zoom should be locked. It then calculates the necessary zoom levels for the X and Y axes separately.
|
||||
* If the zoom should be locked, it calculates the maximum zoom level that fits the scrollable area within the viewport,
|
||||
* factoring in the viewport zoom factor. If the zoom should not be locked, the maximum zoom level is set to null.
|
||||
*
|
||||
* @param scrollConstraints - The constraints of the scrollable area including width, height, and position.
|
||||
* @param width - The width of the viewport.
|
||||
* @param height - The height of the viewport.
|
||||
* @returns An object containing the calculated zoom levels for the X and Y axes, and the maximum zoom level if applicable.
|
||||
*/
|
||||
private calculateZoomLevel = (
|
||||
scrollConstraints: ScrollConstraints,
|
||||
width: AppState["width"],
|
||||
height: AppState["height"],
|
||||
) => {
|
||||
const DEFAULT_VIEWPORT_ZOOM_FACTOR = 0.7;
|
||||
|
||||
const lockZoom = scrollConstraints.lockZoom ?? false;
|
||||
const viewportZoomFactor = scrollConstraints.viewportZoomFactor
|
||||
? Math.min(1, Math.max(scrollConstraints.viewportZoomFactor, 0.1))
|
||||
: DEFAULT_VIEWPORT_ZOOM_FACTOR;
|
||||
|
||||
const scrollableWidth = scrollConstraints.width;
|
||||
const scrollableHeight = scrollConstraints.height;
|
||||
const zoomLevelX = width / scrollableWidth;
|
||||
const zoomLevelY = height / scrollableHeight;
|
||||
const maxZoomLevel = lockZoom
|
||||
? getNormalizedZoom(Math.min(zoomLevelX, zoomLevelY) * viewportZoomFactor)
|
||||
: null;
|
||||
return { zoomLevelX, zoomLevelY, maxZoomLevel };
|
||||
};
|
||||
|
||||
private calculateConstraints = ({
|
||||
scrollConstraints,
|
||||
width,
|
||||
height,
|
||||
zoom,
|
||||
cursorButton,
|
||||
}: {
|
||||
scrollConstraints: ScrollConstraints;
|
||||
width: AppState["width"];
|
||||
height: AppState["height"];
|
||||
zoom: AppState["zoom"];
|
||||
cursorButton: AppState["cursorButton"];
|
||||
}) => {
|
||||
// Set the overscroll allowance percentage
|
||||
const OVERSCROLL_ALLOWANCE_PERCENTAGE = 0.2;
|
||||
|
||||
/**
|
||||
* Calculates the center position of the constrained scroll area.
|
||||
* @returns The X and Y coordinates of the center position.
|
||||
*/
|
||||
const calculateConstrainedScrollCenter = (zoom: number) => {
|
||||
const constrainedScrollCenterX =
|
||||
scrollConstraints.x + (scrollConstraints.width - width / zoom) / -2;
|
||||
const constrainedScrollCenterY =
|
||||
scrollConstraints.y + (scrollConstraints.height - height / zoom) / -2;
|
||||
return { constrainedScrollCenterX, constrainedScrollCenterY };
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the overscroll allowance values for the constrained area.
|
||||
* @returns The overscroll allowance values for the X and Y axes.
|
||||
*/
|
||||
const calculateOverscrollAllowance = () => {
|
||||
const overscrollAllowanceX =
|
||||
OVERSCROLL_ALLOWANCE_PERCENTAGE * scrollConstraints.width;
|
||||
const overscrollAllowanceY =
|
||||
OVERSCROLL_ALLOWANCE_PERCENTAGE * scrollConstraints.height;
|
||||
return { overscrollAllowanceX, overscrollAllowanceY };
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the minimum and maximum scroll values based on the current state.
|
||||
* @param shouldAdjustForCenteredView - Whether the view should be adjusted for centered view - when constrained area fits the viewport.
|
||||
* @param overscrollAllowanceX - The overscroll allowance value for the X axis.
|
||||
* @param overscrollAllowanceY - The overscroll allowance value for the Y axis.
|
||||
* @param constrainedScrollCenterX - The X coordinate of the constrained scroll area center.
|
||||
* @param constrainedScrollCenterY - The Y coordinate of the constrained scroll area center.
|
||||
* @returns The minimum and maximum scroll values for the X and Y axes.
|
||||
*/
|
||||
const calculateMinMaxScrollValues = (
|
||||
shouldAdjustForCenteredView: boolean,
|
||||
overscrollAllowanceX: number,
|
||||
overscrollAllowanceY: number,
|
||||
constrainedScrollCenterX: number,
|
||||
constrainedScrollCenterY: number,
|
||||
zoom: number,
|
||||
) => {
|
||||
let maxScrollX;
|
||||
let minScrollX;
|
||||
let maxScrollY;
|
||||
let minScrollY;
|
||||
|
||||
if (cursorButton === "down" && shouldAdjustForCenteredView) {
|
||||
maxScrollX = constrainedScrollCenterX + overscrollAllowanceX;
|
||||
minScrollX = constrainedScrollCenterX - overscrollAllowanceX;
|
||||
maxScrollY = constrainedScrollCenterY + overscrollAllowanceY;
|
||||
minScrollY = constrainedScrollCenterY - overscrollAllowanceY;
|
||||
} else if (cursorButton === "down" && !shouldAdjustForCenteredView) {
|
||||
maxScrollX = scrollConstraints.x + overscrollAllowanceX;
|
||||
minScrollX =
|
||||
scrollConstraints.x -
|
||||
scrollConstraints.width +
|
||||
width / zoom -
|
||||
overscrollAllowanceX;
|
||||
maxScrollY = scrollConstraints.y + overscrollAllowanceY;
|
||||
minScrollY =
|
||||
scrollConstraints.y -
|
||||
scrollConstraints.height +
|
||||
height / zoom -
|
||||
overscrollAllowanceY;
|
||||
} else if (cursorButton !== "down" && shouldAdjustForCenteredView) {
|
||||
maxScrollX = constrainedScrollCenterX;
|
||||
minScrollX = constrainedScrollCenterX;
|
||||
maxScrollY = constrainedScrollCenterY;
|
||||
minScrollY = constrainedScrollCenterY;
|
||||
} else {
|
||||
maxScrollX = scrollConstraints.x;
|
||||
minScrollX =
|
||||
scrollConstraints.x - scrollConstraints.width + width / zoom;
|
||||
maxScrollY = scrollConstraints.y;
|
||||
minScrollY =
|
||||
scrollConstraints.y - scrollConstraints.height + height / zoom;
|
||||
}
|
||||
|
||||
return { maxScrollX, minScrollX, maxScrollY, minScrollY };
|
||||
};
|
||||
|
||||
const { zoomLevelX, zoomLevelY, maxZoomLevel } = this.calculateZoomLevel(
|
||||
scrollConstraints,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
|
||||
const constrainedZoom = getNormalizedZoom(
|
||||
maxZoomLevel ? Math.max(maxZoomLevel, zoom.value) : zoom.value,
|
||||
);
|
||||
const { constrainedScrollCenterX, constrainedScrollCenterY } =
|
||||
calculateConstrainedScrollCenter(constrainedZoom);
|
||||
const { overscrollAllowanceX, overscrollAllowanceY } =
|
||||
calculateOverscrollAllowance();
|
||||
const shouldAdjustForCenteredView =
|
||||
constrainedZoom <= zoomLevelX || constrainedZoom <= zoomLevelY;
|
||||
const { maxScrollX, minScrollX, maxScrollY, minScrollY } =
|
||||
calculateMinMaxScrollValues(
|
||||
shouldAdjustForCenteredView,
|
||||
overscrollAllowanceX,
|
||||
overscrollAllowanceY,
|
||||
constrainedScrollCenterX,
|
||||
constrainedScrollCenterY,
|
||||
constrainedZoom,
|
||||
);
|
||||
|
||||
return {
|
||||
maxScrollX,
|
||||
minScrollX,
|
||||
maxScrollY,
|
||||
minScrollY,
|
||||
constrainedZoom: {
|
||||
value: constrainedZoom,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Constrains the scroll values within the constrained area.
|
||||
* @param maxScrollX - The maximum scroll value for the X axis.
|
||||
* @param minScrollX - The minimum scroll value for the X axis.
|
||||
* @param maxScrollY - The maximum scroll value for the Y axis.
|
||||
* @param minScrollY - The minimum scroll value for the Y axis.
|
||||
* @returns The constrained scroll values for the X and Y axes.
|
||||
*/
|
||||
private constrainScrollValues = ({
|
||||
scrollX,
|
||||
scrollY,
|
||||
maxScrollX,
|
||||
minScrollX,
|
||||
maxScrollY,
|
||||
minScrollY,
|
||||
constrainedZoom,
|
||||
}: {
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
maxScrollX: number;
|
||||
minScrollX: number;
|
||||
maxScrollY: number;
|
||||
minScrollY: number;
|
||||
constrainedZoom: AppState["zoom"];
|
||||
}) => {
|
||||
const constrainedScrollX = Math.min(
|
||||
maxScrollX,
|
||||
Math.max(scrollX, minScrollX),
|
||||
);
|
||||
const constrainedScrollY = Math.min(
|
||||
maxScrollY,
|
||||
Math.max(scrollY, minScrollY),
|
||||
);
|
||||
return { constrainedScrollX, constrainedScrollY, constrainedZoom };
|
||||
};
|
||||
|
||||
/**
|
||||
* Animate the scroll values to the constrained area
|
||||
*/
|
||||
private animateConstrainedScroll = ({
|
||||
constrainedScrollX,
|
||||
constrainedScrollY,
|
||||
opts,
|
||||
}: {
|
||||
constrainedScrollX: number;
|
||||
constrainedScrollY: number;
|
||||
opts?: {
|
||||
onStartCallback?: () => void;
|
||||
onEndCallback?: () => void;
|
||||
};
|
||||
}) => {
|
||||
const { scrollX, scrollY, scrollConstraints } = this.state;
|
||||
|
||||
const { onStartCallback, onEndCallback } = opts || {};
|
||||
|
||||
if (!scrollConstraints) {
|
||||
return null;
|
||||
}
|
||||
|
||||
easeToValuesRAF({
|
||||
fromValues: { scrollX, scrollY },
|
||||
toValues: {
|
||||
scrollX: constrainedScrollX,
|
||||
scrollY: constrainedScrollY,
|
||||
},
|
||||
onStep: ({ scrollX, scrollY }) => {
|
||||
this.setState({ scrollX, scrollY });
|
||||
},
|
||||
onStart: () => {
|
||||
this.setState({
|
||||
scrollConstraints: { ...scrollConstraints, isAnimating: true },
|
||||
});
|
||||
onStartCallback && onStartCallback();
|
||||
},
|
||||
onEnd: () => {
|
||||
this.setState({
|
||||
scrollConstraints: { ...scrollConstraints, isAnimating: false },
|
||||
});
|
||||
onEndCallback && onEndCallback();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private isViewportOutsideOfConstrainedArea = ({
|
||||
scrollX,
|
||||
scrollY,
|
||||
width,
|
||||
height,
|
||||
scrollConstraints,
|
||||
}: {
|
||||
scrollX: AppState["scrollX"];
|
||||
scrollY: AppState["scrollY"];
|
||||
width: AppState["width"];
|
||||
height: AppState["height"];
|
||||
scrollConstraints: AppState["scrollConstraints"];
|
||||
}) => {
|
||||
if (!scrollConstraints) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
scrollX < scrollConstraints.x ||
|
||||
scrollX + width > scrollConstraints.x + scrollConstraints.width ||
|
||||
scrollY < scrollConstraints.y ||
|
||||
scrollY + height > scrollConstraints.y + scrollConstraints.height
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the state change based on the constrained scroll values.
|
||||
* Also handles the animation to the constrained area when the viewport is outside of constrained area.
|
||||
* @param constrainedScrollX - The constrained scroll value for the X axis.
|
||||
* @param constrainedScrollY - The constrained scroll value for the Y axis.
|
||||
* @returns The constrained state if the state has changed, when needs to be passed into render function, otherwise null.
|
||||
*/
|
||||
private handleConstrainedScrollStateChange = ({
|
||||
constrainedScrollX,
|
||||
constrainedScrollY,
|
||||
constrainedZoom,
|
||||
shouldAnimate,
|
||||
}: {
|
||||
constrainedScrollX: number;
|
||||
constrainedScrollY: number;
|
||||
constrainedZoom: AppState["zoom"];
|
||||
shouldAnimate?: boolean;
|
||||
}) => {
|
||||
const { scrollX, scrollY } = this.state;
|
||||
const isStateChanged =
|
||||
constrainedScrollX !== scrollX || constrainedScrollY !== scrollY;
|
||||
|
||||
if (isStateChanged) {
|
||||
if (shouldAnimate) {
|
||||
this.animateConstrainedScroll({
|
||||
constrainedScrollX,
|
||||
constrainedScrollY,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
const constrainedState = {
|
||||
scrollX: constrainedScrollX,
|
||||
scrollY: constrainedScrollY,
|
||||
zoom: constrainedZoom,
|
||||
};
|
||||
|
||||
this.setState(constrainedState);
|
||||
return constrainedState;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
@ -46,7 +46,7 @@ import { normalizeLink } from "./url";
|
||||
|
||||
type RestoredAppState = Omit<
|
||||
AppState,
|
||||
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||
"offsetTop" | "offsetLeft" | "width" | "height" | "scrollConstraints"
|
||||
>;
|
||||
|
||||
export const AllowedExcalidrawActiveTools: Record<
|
||||
|
@ -731,6 +731,13 @@ const ExcalidrawWrapper = () => {
|
||||
/>
|
||||
);
|
||||
}}
|
||||
scrollConstraints={{
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2560,
|
||||
height: 1300,
|
||||
lockZoom: true,
|
||||
}}
|
||||
>
|
||||
<AppMainMenu
|
||||
setCollabDialogShown={setCollabDialogShown}
|
||||
|
@ -65,6 +65,7 @@ const canvas = exportToCanvas(
|
||||
offsetLeft: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
scrollConstraints: null,
|
||||
},
|
||||
{}, // files
|
||||
{
|
||||
|
@ -42,6 +42,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
onPointerDown,
|
||||
onScrollChange,
|
||||
children,
|
||||
scrollConstraints,
|
||||
validateEmbeddable,
|
||||
renderEmbeddable,
|
||||
} = props;
|
||||
@ -100,7 +101,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
onPointerUpdate={onPointerUpdate}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
langCode={langCode}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
viewModeEnabled={viewModeEnabled /* || !!scrollConstraints */}
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
gridModeEnabled={gridModeEnabled}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
@ -117,6 +118,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onScrollChange={onScrollChange}
|
||||
scrollConstraints={scrollConstraints}
|
||||
validateEmbeddable={validateEmbeddable}
|
||||
renderEmbeddable={renderEmbeddable}
|
||||
>
|
||||
|
@ -64,7 +64,14 @@ export const exportToCanvas = ({
|
||||
const { exportBackground, viewBackgroundColor } = restoredAppState;
|
||||
return _exportToCanvas(
|
||||
passElementsSafely(restoredElements),
|
||||
{ ...restoredAppState, offsetTop: 0, offsetLeft: 0, width: 0, height: 0 },
|
||||
{
|
||||
...restoredAppState,
|
||||
offsetTop: 0,
|
||||
offsetLeft: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
scrollConstraints: null,
|
||||
},
|
||||
files || {},
|
||||
{ exportBackground, exportPadding, viewBackgroundColor },
|
||||
(width: number, height: number) => {
|
||||
|
@ -60,3 +60,8 @@ export type ScrollBars = {
|
||||
height: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type ConstrainedScrollValues = Pick<
|
||||
AppState,
|
||||
"scrollX" | "scrollY" | "zoom"
|
||||
> | null;
|
||||
|
@ -346,6 +346,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -539,6 +540,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -738,6 +740,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -1111,6 +1114,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -1484,6 +1488,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -1683,6 +1688,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -1919,6 +1925,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -2222,6 +2229,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"id1": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -2609,6 +2617,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -3488,6 +3497,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -3861,6 +3871,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -4236,6 +4247,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"id1": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -4968,6 +4980,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -5548,6 +5561,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -6050,6 +6064,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -6446,6 +6461,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -6820,6 +6836,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
|
@ -74,6 +74,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
||||
"id2": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -527,6 +528,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
||||
"id2": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -978,6 +980,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -1808,6 +1811,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -2018,6 +2022,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -2469,6 +2474,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -2706,6 +2712,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -2873,6 +2880,7 @@ exports[`regression tests > can drag element that covers another element, while
|
||||
"id2": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -3312,6 +3320,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -3608,6 +3617,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -3850,6 +3860,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -4103,6 +4114,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
||||
"id1": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -4342,6 +4354,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
||||
"id2": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -4712,6 +4725,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
||||
"id1": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -5006,6 +5020,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
||||
"id1": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -5271,6 +5286,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -5464,6 +5480,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -5627,6 +5644,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -6079,6 +6097,7 @@ exports[`regression tests > drags selected elements from point inside common bou
|
||||
"id1": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -6390,6 +6409,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -8457,6 +8477,7 @@ exports[`regression tests > given a group of selected elements with an element t
|
||||
"id2": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -8797,6 +8818,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -9037,6 +9059,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -9233,6 +9256,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -9499,6 +9523,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -9664,6 +9689,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -9829,6 +9855,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -9994,6 +10021,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -10197,6 +10225,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -10400,6 +10429,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -10583,6 +10613,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -10786,6 +10817,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -10951,6 +10983,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -11154,6 +11187,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -11319,6 +11353,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -11502,6 +11537,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -11671,6 +11707,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
||||
"id2": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -12330,6 +12367,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -12567,6 +12605,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": -2.916666666666668,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -12687,6 +12726,7 @@ exports[`regression tests > rerenders UI on language change > [end of test] appS
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -12809,6 +12849,7 @@ exports[`regression tests > shift click on selected element should deselect it o
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -12975,6 +13016,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
||||
"id1": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -13290,6 +13332,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
||||
"id2": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -13844,6 +13887,7 @@ exports[`regression tests > should show fill icons when element has non transpar
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -14057,6 +14101,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
||||
"id6": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -14905,6 +14950,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 60,
|
||||
"scrollY": 60,
|
||||
"scrolledOutside": false,
|
||||
@ -15027,6 +15073,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
||||
"id0": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -15840,6 +15887,7 @@ exports[`regression tests > switches from group of selected elements to another
|
||||
"id2": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -16236,6 +16284,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
||||
"id1": true,
|
||||
},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -16501,6 +16550,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 10,
|
||||
"scrollY": -10,
|
||||
"scrolledOutside": false,
|
||||
@ -16621,6 +16671,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -17101,6 +17152,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
@ -17221,6 +17273,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
||||
"pendingImageElementId": null,
|
||||
"previousSelectedElementIds": {},
|
||||
"resizingElement": null,
|
||||
"scrollConstraints": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
|
13
src/types.ts
13
src/types.ts
@ -238,6 +238,7 @@ export type AppState = {
|
||||
pendingImageElementId: ExcalidrawImageElement["id"] | null;
|
||||
showHyperlinkPopup: false | "info" | "editor";
|
||||
selectedLinearElement: LinearElementEditor | null;
|
||||
scrollConstraints: ScrollConstraints | null;
|
||||
};
|
||||
|
||||
export type UIAppState = Omit<
|
||||
@ -376,6 +377,7 @@ export interface ExcalidrawProps {
|
||||
) => void;
|
||||
onScrollChange?: (scrollX: number, scrollY: number) => void;
|
||||
children?: React.ReactNode;
|
||||
scrollConstraints?: AppState["scrollConstraints"];
|
||||
validateEmbeddable?:
|
||||
| boolean
|
||||
| string[]
|
||||
@ -574,6 +576,7 @@ export type ExcalidrawImperativeAPI = {
|
||||
* used in conjunction with view mode (props.viewModeEnabled).
|
||||
*/
|
||||
updateFrameRendering: InstanceType<typeof App>["updateFrameRendering"];
|
||||
setScrollConstraints: InstanceType<typeof App>["setScrollConstraints"];
|
||||
};
|
||||
|
||||
export type Device = Readonly<{
|
||||
@ -602,3 +605,13 @@ export type FrameNameBoundsCache = {
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export type ScrollConstraints = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
isAnimating?: boolean;
|
||||
viewportZoomFactor?: number;
|
||||
lockZoom?: boolean;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user