diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx index c6e05e286..b9db59c65 100644 --- a/excalidraw-app/App.tsx +++ b/excalidraw-app/App.tsx @@ -51,7 +51,10 @@ import { import { isElementLink } from "@excalidraw/element/elementLink"; import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore"; import { newElementWith } from "@excalidraw/element/mutateElement"; -import { isInitializedImageElement } from "@excalidraw/element/typeChecks"; +import { + isFrameLikeElement, + isInitializedImageElement, +} from "@excalidraw/element/typeChecks"; import clsx from "clsx"; import { parseLibraryTokensFromUrl, @@ -142,6 +145,10 @@ import "./index.scss"; import type { CollabAPI } from "./collab/Collab"; import { getSelectedElements } from "@excalidraw/element/selection"; +import { + decodeConstraints, + encodeConstraints, +} from "@excalidraw/excalidraw/scene/scrollConstraints"; polyfill(); @@ -160,19 +167,37 @@ const ConstraintsSettings = ({ const [constraints, setConstraints] = useState(initialConstraints); + const frames = excalidrawAPI + .getSceneElements() + .filter((e) => isFrameLikeElement(e)); + const [activeFrameId, setActiveFrameId] = useState(null); + useEffect(() => { - // add JSON-stringified constraints into url hash for easy sharing const hash = new URLSearchParams(window.location.hash.slice(1)); - hash.set( - "constraints", - encodeURIComponent( - window.btoa(JSON.stringify(constraints)).replace(/=+/, ""), - ), - ); + hash.set("constraints", encodeConstraints(constraints)); window.location.hash = decodeURIComponent(hash.toString()); - excalidrawAPI.setScrollConstraints(constraints); + + constraints.enabled + ? excalidrawAPI.setScrollConstraints(constraints) + : excalidrawAPI.setScrollConstraints(null); }, [constraints]); + useEffect(() => { + const frame = frames.find((frame) => frame.id === activeFrameId); + if (frame) { + const { x, y, width, height } = frame; + setConstraints((s) => ({ + x: Math.round(x), + y: Math.round(y), + width: Math.round(width), + height: Math.round(height), + enabled: s.enabled, + viewportZoomFactor: s.viewportZoomFactor, + lockZoom: s.lockZoom, + })); + } + }, [activeFrameId]); + const [selection, setSelection] = useState([]); useEffect(() => { return excalidrawAPI.onChange((elements, appState) => { @@ -183,109 +208,179 @@ const ConstraintsSettings = ({ return (
- enabled:{" "} - - setConstraints((s) => ({ ...s, enabled: e.target.checked })) - } - /> - x:{" "} - - setConstraints((s) => ({ - ...s, - x: parseInt(e.target.value) ?? 0, - })) - } - /> - y:{" "} - - setConstraints((s) => ({ - ...s, - y: parseInt(e.target.value) ?? 0, - })) - } - /> - w:{" "} - - setConstraints((s) => ({ - ...s, - width: parseInt(e.target.value) ?? 200, - })) - } - /> - h:{" "} - - setConstraints((s) => ({ - ...s, - height: parseInt(e.target.value) ?? 200, - })) - } - /> - zoomFactor: - - setConstraints((s) => ({ - ...s, - viewportZoomFactor: parseFloat(e.target.value.toString()) ?? 0.7, - })) - } - /> - lockZoom:{" "} - - setConstraints((s) => ({ ...s, lockZoom: e.target.checked })) - } - /> - {selection.length > 0 && ( - + )} +
+ + {frames.length > 0 && ( +
- use selection - + + +
)} ); @@ -909,8 +1004,10 @@ const ExcalidrawWrapper = () => { let storedConstraints = {}; if (stored) { try { - storedConstraints = JSON.parse(window.atob(stored)); - } catch {} + storedConstraints = decodeConstraints(stored); + } catch { + console.error("Invalid scroll constraints in URL"); + } } return { @@ -920,13 +1017,12 @@ const ExcalidrawWrapper = () => { height: document.body.clientHeight, lockZoom: false, viewportZoomFactor: 0.7, + overscrollAllowance: 0.5, enabled: true, ...storedConstraints, }; }); - console.log(constraints); - // browsers generally prevent infinite self-embedding, there are // cases where it still happens, and while we disallow self-embedding // by not whitelisting our own origin, this serves as an additional guard diff --git a/packages/excalidraw/scene/scrollConstraints.ts b/packages/excalidraw/scene/scrollConstraints.ts index 04dc9bdb2..60fe57163 100644 --- a/packages/excalidraw/scene/scrollConstraints.ts +++ b/packages/excalidraw/scene/scrollConstraints.ts @@ -21,7 +21,6 @@ import { getNormalizedZoom } from "./normalize"; export const calculateConstrainedScrollCenter = ( state: AppState, { scrollX, scrollY }: Pick, - overscrollAllowance?: number, ): { scrollX: AppState["scrollX"]; scrollY: AppState["scrollY"]; @@ -59,7 +58,6 @@ export const calculateConstrainedScrollCenter = ( height, zoom: _zoom, allowOverscroll: false, - overscrollAllowance, }); return { @@ -69,6 +67,8 @@ export const calculateConstrainedScrollCenter = ( }; }; +const DEFAULT_OVERSCROLL_ALLOWANCE = 0.2; + interface EncodedConstraints { x: number; y: number; @@ -76,6 +76,8 @@ interface EncodedConstraints { h: number; l: boolean; v: number; + // overscrollAllowance + oa: number; } /** @@ -91,6 +93,7 @@ export const encodeConstraints = (constraints: ScrollConstraints): string => { h: constraints.height, l: !!constraints.lockZoom, v: constraints.viewportZoomFactor ?? 1, + oa: constraints.overscrollAllowance ?? DEFAULT_OVERSCROLL_ALLOWANCE, }; const serialized = JSON.stringify(payload); @@ -115,6 +118,7 @@ export const decodeConstraints = (encoded: string): ScrollConstraints => { lockZoom: parsed.l ?? false, viewportZoomFactor: parsed.v ?? 1, animateOnNextUpdate: false, + overscrollAllowance: parsed.oa ?? DEFAULT_OVERSCROLL_ALLOWANCE, }; } catch (error) { // return safe defaults if decoding fails @@ -170,22 +174,21 @@ const calculateConstraints = ({ height, zoom, allowOverscroll, - overscrollAllowance, }: { scrollConstraints: ScrollConstraints; width: AppState["width"]; height: AppState["height"]; zoom: AppState["zoom"]; allowOverscroll: boolean; - overscrollAllowance?: number; }) => { // Validate the overscroll allowance percentage + const overscrollAllowance = scrollConstraints.overscrollAllowance; const validatedOverscroll = overscrollAllowance != null && overscrollAllowance >= 0 && overscrollAllowance <= 1 ? overscrollAllowance - : 0.2; + : DEFAULT_OVERSCROLL_ALLOWANCE; /** * Calculates the center position of the constrained scroll area. diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 8141a2d55..a07b780bd 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -911,6 +911,7 @@ export type ScrollConstraints = { animateOnNextUpdate?: boolean; viewportZoomFactor?: number; lockZoom?: boolean; + overscrollAllowance?: number; }; export type PendingExcalidrawElements = ExcalidrawElement[];