|
@@ -74,7 +74,6 @@ import {
|
|
MQ_MAX_WIDTH_LANDSCAPE,
|
|
MQ_MAX_WIDTH_LANDSCAPE,
|
|
MQ_MAX_WIDTH_PORTRAIT,
|
|
MQ_MAX_WIDTH_PORTRAIT,
|
|
MQ_RIGHT_SIDEBAR_MIN_WIDTH,
|
|
MQ_RIGHT_SIDEBAR_MIN_WIDTH,
|
|
- MQ_SM_MAX_WIDTH,
|
|
|
|
POINTER_BUTTON,
|
|
POINTER_BUTTON,
|
|
ROUNDNESS,
|
|
ROUNDNESS,
|
|
SCROLL_TIMEOUT,
|
|
SCROLL_TIMEOUT,
|
|
@@ -381,11 +380,15 @@ const AppContext = React.createContext<AppClassProperties>(null!);
|
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
|
|
|
|
|
const deviceContextInitialValue = {
|
|
const deviceContextInitialValue = {
|
|
- isSmScreen: false,
|
|
|
|
- isMobile: false,
|
|
|
|
|
|
+ viewport: {
|
|
|
|
+ isMobile: false,
|
|
|
|
+ isLandscape: false,
|
|
|
|
+ },
|
|
|
|
+ editor: {
|
|
|
|
+ isMobile: false,
|
|
|
|
+ canFitSidebar: false,
|
|
|
|
+ },
|
|
isTouchScreen: false,
|
|
isTouchScreen: false,
|
|
- canDeviceFitSidebar: false,
|
|
|
|
- isLandscape: false,
|
|
|
|
};
|
|
};
|
|
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
|
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
|
DeviceContext.displayName = "DeviceContext";
|
|
DeviceContext.displayName = "DeviceContext";
|
|
@@ -436,6 +439,9 @@ export const useExcalidrawSetAppState = () =>
|
|
export const useExcalidrawActionManager = () =>
|
|
export const useExcalidrawActionManager = () =>
|
|
useContext(ExcalidrawActionManagerContext);
|
|
useContext(ExcalidrawActionManagerContext);
|
|
|
|
|
|
|
|
+const supportsResizeObserver =
|
|
|
|
+ typeof window !== "undefined" && "ResizeObserver" in window;
|
|
|
|
+
|
|
let didTapTwice: boolean = false;
|
|
let didTapTwice: boolean = false;
|
|
let tappedTwiceTimer = 0;
|
|
let tappedTwiceTimer = 0;
|
|
let isHoldingSpace: boolean = false;
|
|
let isHoldingSpace: boolean = false;
|
|
@@ -472,7 +478,6 @@ class App extends React.Component<AppProps, AppState> {
|
|
unmounted: boolean = false;
|
|
unmounted: boolean = false;
|
|
actionManager: ActionManager;
|
|
actionManager: ActionManager;
|
|
device: Device = deviceContextInitialValue;
|
|
device: Device = deviceContextInitialValue;
|
|
- detachIsMobileMqHandler?: () => void;
|
|
|
|
|
|
|
|
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
|
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
|
|
|
|
|
@@ -1180,7 +1185,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
<div
|
|
<div
|
|
className={clsx("excalidraw excalidraw-container", {
|
|
className={clsx("excalidraw excalidraw-container", {
|
|
"excalidraw--view-mode": this.state.viewModeEnabled,
|
|
"excalidraw--view-mode": this.state.viewModeEnabled,
|
|
- "excalidraw--mobile": this.device.isMobile,
|
|
|
|
|
|
+ "excalidraw--mobile": this.device.editor.isMobile,
|
|
})}
|
|
})}
|
|
style={{
|
|
style={{
|
|
["--ui-pointerEvents" as any]:
|
|
["--ui-pointerEvents" as any]:
|
|
@@ -1657,20 +1662,62 @@ class App extends React.Component<AppProps, AppState> {
|
|
});
|
|
});
|
|
};
|
|
};
|
|
|
|
|
|
- private refreshDeviceState = (container: HTMLDivElement) => {
|
|
|
|
- const { width, height } = container.getBoundingClientRect();
|
|
|
|
|
|
+ private isMobileBreakpoint = (width: number, height: number) => {
|
|
|
|
+ return (
|
|
|
|
+ width < MQ_MAX_WIDTH_PORTRAIT ||
|
|
|
|
+ (height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE)
|
|
|
|
+ );
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ private refreshViewportBreakpoints = () => {
|
|
|
|
+ const container = this.excalidrawContainerRef.current;
|
|
|
|
+ if (!container) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const { clientWidth: viewportWidth, clientHeight: viewportHeight } =
|
|
|
|
+ document.body;
|
|
|
|
+
|
|
|
|
+ const prevViewportState = this.device.viewport;
|
|
|
|
+
|
|
|
|
+ const nextViewportState = updateObject(prevViewportState, {
|
|
|
|
+ isLandscape: viewportWidth > viewportHeight,
|
|
|
|
+ isMobile: this.isMobileBreakpoint(viewportWidth, viewportHeight),
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ if (prevViewportState !== nextViewportState) {
|
|
|
|
+ this.device = { ...this.device, viewport: nextViewportState };
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ return false;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ private refreshEditorBreakpoints = () => {
|
|
|
|
+ const container = this.excalidrawContainerRef.current;
|
|
|
|
+ if (!container) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const { width: editorWidth, height: editorHeight } =
|
|
|
|
+ container.getBoundingClientRect();
|
|
|
|
+
|
|
const sidebarBreakpoint =
|
|
const sidebarBreakpoint =
|
|
this.props.UIOptions.dockedSidebarBreakpoint != null
|
|
this.props.UIOptions.dockedSidebarBreakpoint != null
|
|
? this.props.UIOptions.dockedSidebarBreakpoint
|
|
? this.props.UIOptions.dockedSidebarBreakpoint
|
|
: MQ_RIGHT_SIDEBAR_MIN_WIDTH;
|
|
: MQ_RIGHT_SIDEBAR_MIN_WIDTH;
|
|
- this.device = updateObject(this.device, {
|
|
|
|
- isLandscape: width > height,
|
|
|
|
- isSmScreen: width < MQ_SM_MAX_WIDTH,
|
|
|
|
- isMobile:
|
|
|
|
- width < MQ_MAX_WIDTH_PORTRAIT ||
|
|
|
|
- (height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE),
|
|
|
|
- canDeviceFitSidebar: width > sidebarBreakpoint,
|
|
|
|
|
|
+
|
|
|
|
+ const prevEditorState = this.device.editor;
|
|
|
|
+
|
|
|
|
+ const nextEditorState = updateObject(prevEditorState, {
|
|
|
|
+ isMobile: this.isMobileBreakpoint(editorWidth, editorHeight),
|
|
|
|
+ canFitSidebar: editorWidth > sidebarBreakpoint,
|
|
});
|
|
});
|
|
|
|
+
|
|
|
|
+ if (prevEditorState !== nextEditorState) {
|
|
|
|
+ this.device = { ...this.device, editor: nextEditorState };
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ return false;
|
|
};
|
|
};
|
|
|
|
|
|
public async componentDidMount() {
|
|
public async componentDidMount() {
|
|
@@ -1712,52 +1759,21 @@ class App extends React.Component<AppProps, AppState> {
|
|
}
|
|
}
|
|
|
|
|
|
if (
|
|
if (
|
|
- this.excalidrawContainerRef.current &&
|
|
|
|
// bounding rects don't work in tests so updating
|
|
// bounding rects don't work in tests so updating
|
|
// the state on init would result in making the test enviro run
|
|
// the state on init would result in making the test enviro run
|
|
// in mobile breakpoint (0 width/height), making everything fail
|
|
// in mobile breakpoint (0 width/height), making everything fail
|
|
!isTestEnv()
|
|
!isTestEnv()
|
|
) {
|
|
) {
|
|
- this.refreshDeviceState(this.excalidrawContainerRef.current);
|
|
|
|
|
|
+ this.refreshViewportBreakpoints();
|
|
|
|
+ this.refreshEditorBreakpoints();
|
|
}
|
|
}
|
|
|
|
|
|
- if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
|
|
|
|
|
+ if (supportsResizeObserver && this.excalidrawContainerRef.current) {
|
|
this.resizeObserver = new ResizeObserver(() => {
|
|
this.resizeObserver = new ResizeObserver(() => {
|
|
- // recompute device dimensions state
|
|
|
|
- // ---------------------------------------------------------------------
|
|
|
|
- this.refreshDeviceState(this.excalidrawContainerRef.current!);
|
|
|
|
- // refresh offsets
|
|
|
|
- // ---------------------------------------------------------------------
|
|
|
|
|
|
+ this.refreshEditorBreakpoints();
|
|
this.updateDOMRect();
|
|
this.updateDOMRect();
|
|
});
|
|
});
|
|
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
|
|
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
|
|
- } else if (window.matchMedia) {
|
|
|
|
- const mdScreenQuery = window.matchMedia(
|
|
|
|
- `(max-width: ${MQ_MAX_WIDTH_PORTRAIT}px), (max-height: ${MQ_MAX_HEIGHT_LANDSCAPE}px) and (max-width: ${MQ_MAX_WIDTH_LANDSCAPE}px)`,
|
|
|
|
- );
|
|
|
|
- const smScreenQuery = window.matchMedia(
|
|
|
|
- `(max-width: ${MQ_SM_MAX_WIDTH}px)`,
|
|
|
|
- );
|
|
|
|
- const canDeviceFitSidebarMediaQuery = window.matchMedia(
|
|
|
|
- `(min-width: ${
|
|
|
|
- // NOTE this won't update if a different breakpoint is supplied
|
|
|
|
- // after mount
|
|
|
|
- this.props.UIOptions.dockedSidebarBreakpoint != null
|
|
|
|
- ? this.props.UIOptions.dockedSidebarBreakpoint
|
|
|
|
- : MQ_RIGHT_SIDEBAR_MIN_WIDTH
|
|
|
|
- }px)`,
|
|
|
|
- );
|
|
|
|
- const handler = () => {
|
|
|
|
- this.excalidrawContainerRef.current!.getBoundingClientRect();
|
|
|
|
- this.device = updateObject(this.device, {
|
|
|
|
- isSmScreen: smScreenQuery.matches,
|
|
|
|
- isMobile: mdScreenQuery.matches,
|
|
|
|
- canDeviceFitSidebar: canDeviceFitSidebarMediaQuery.matches,
|
|
|
|
- });
|
|
|
|
- };
|
|
|
|
- mdScreenQuery.addListener(handler);
|
|
|
|
- this.detachIsMobileMqHandler = () =>
|
|
|
|
- mdScreenQuery.removeListener(handler);
|
|
|
|
}
|
|
}
|
|
|
|
|
|
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
|
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
|
@@ -1802,6 +1818,11 @@ class App extends React.Component<AppProps, AppState> {
|
|
this.scene
|
|
this.scene
|
|
.getElementsIncludingDeleted()
|
|
.getElementsIncludingDeleted()
|
|
.forEach((element) => ShapeCache.delete(element));
|
|
.forEach((element) => ShapeCache.delete(element));
|
|
|
|
+ this.refreshViewportBreakpoints();
|
|
|
|
+ this.updateDOMRect();
|
|
|
|
+ if (!supportsResizeObserver) {
|
|
|
|
+ this.refreshEditorBreakpoints();
|
|
|
|
+ }
|
|
this.setState({});
|
|
this.setState({});
|
|
});
|
|
});
|
|
|
|
|
|
@@ -1855,7 +1876,6 @@ class App extends React.Component<AppProps, AppState> {
|
|
false,
|
|
false,
|
|
);
|
|
);
|
|
|
|
|
|
- this.detachIsMobileMqHandler?.();
|
|
|
|
window.removeEventListener(EVENT.MESSAGE, this.onWindowMessage, false);
|
|
window.removeEventListener(EVENT.MESSAGE, this.onWindowMessage, false);
|
|
}
|
|
}
|
|
|
|
|
|
@@ -1940,11 +1960,10 @@ class App extends React.Component<AppProps, AppState> {
|
|
}
|
|
}
|
|
|
|
|
|
if (
|
|
if (
|
|
- this.excalidrawContainerRef.current &&
|
|
|
|
prevProps.UIOptions.dockedSidebarBreakpoint !==
|
|
prevProps.UIOptions.dockedSidebarBreakpoint !==
|
|
- this.props.UIOptions.dockedSidebarBreakpoint
|
|
|
|
|
|
+ this.props.UIOptions.dockedSidebarBreakpoint
|
|
) {
|
|
) {
|
|
- this.refreshDeviceState(this.excalidrawContainerRef.current);
|
|
|
|
|
|
+ this.refreshEditorBreakpoints();
|
|
}
|
|
}
|
|
|
|
|
|
if (
|
|
if (
|
|
@@ -2410,7 +2429,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
// from library, not when pasting from clipboard. Alas.
|
|
// from library, not when pasting from clipboard. Alas.
|
|
openSidebar:
|
|
openSidebar:
|
|
this.state.openSidebar &&
|
|
this.state.openSidebar &&
|
|
- this.device.canDeviceFitSidebar &&
|
|
|
|
|
|
+ this.device.editor.canFitSidebar &&
|
|
jotaiStore.get(isSidebarDockedAtom)
|
|
jotaiStore.get(isSidebarDockedAtom)
|
|
? this.state.openSidebar
|
|
? this.state.openSidebar
|
|
: null,
|
|
: null,
|
|
@@ -2624,7 +2643,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
!isPlainPaste &&
|
|
!isPlainPaste &&
|
|
textElements.length > 1 &&
|
|
textElements.length > 1 &&
|
|
PLAIN_PASTE_TOAST_SHOWN === false &&
|
|
PLAIN_PASTE_TOAST_SHOWN === false &&
|
|
- !this.device.isMobile
|
|
|
|
|
|
+ !this.device.editor.isMobile
|
|
) {
|
|
) {
|
|
this.setToast({
|
|
this.setToast({
|
|
message: t("toast.pasteAsSingleElement", {
|
|
message: t("toast.pasteAsSingleElement", {
|
|
@@ -2658,7 +2677,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
trackEvent(
|
|
trackEvent(
|
|
"toolbar",
|
|
"toolbar",
|
|
"toggleLock",
|
|
"toggleLock",
|
|
- `${source} (${this.device.isMobile ? "mobile" : "desktop"})`,
|
|
|
|
|
|
+ `${source} (${this.device.editor.isMobile ? "mobile" : "desktop"})`,
|
|
);
|
|
);
|
|
}
|
|
}
|
|
this.setState((prevState) => {
|
|
this.setState((prevState) => {
|
|
@@ -3153,7 +3172,9 @@ class App extends React.Component<AppProps, AppState> {
|
|
trackEvent(
|
|
trackEvent(
|
|
"toolbar",
|
|
"toolbar",
|
|
shape,
|
|
shape,
|
|
- `keyboard (${this.device.isMobile ? "mobile" : "desktop"})`,
|
|
|
|
|
|
+ `keyboard (${
|
|
|
|
+ this.device.editor.isMobile ? "mobile" : "desktop"
|
|
|
|
+ })`,
|
|
);
|
|
);
|
|
}
|
|
}
|
|
this.setActiveTool({ type: shape });
|
|
this.setActiveTool({ type: shape });
|
|
@@ -3887,7 +3908,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
element,
|
|
element,
|
|
this.state,
|
|
this.state,
|
|
[scenePointer.x, scenePointer.y],
|
|
[scenePointer.x, scenePointer.y],
|
|
- this.device.isMobile,
|
|
|
|
|
|
+ this.device.editor.isMobile,
|
|
)
|
|
)
|
|
);
|
|
);
|
|
});
|
|
});
|
|
@@ -3919,7 +3940,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
this.hitLinkElement,
|
|
this.hitLinkElement,
|
|
this.state,
|
|
this.state,
|
|
[lastPointerDownCoords.x, lastPointerDownCoords.y],
|
|
[lastPointerDownCoords.x, lastPointerDownCoords.y],
|
|
- this.device.isMobile,
|
|
|
|
|
|
+ this.device.editor.isMobile,
|
|
);
|
|
);
|
|
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
|
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
|
this.lastPointerUpEvent!,
|
|
this.lastPointerUpEvent!,
|
|
@@ -3929,7 +3950,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
this.hitLinkElement,
|
|
this.hitLinkElement,
|
|
this.state,
|
|
this.state,
|
|
[lastPointerUpCoords.x, lastPointerUpCoords.y],
|
|
[lastPointerUpCoords.x, lastPointerUpCoords.y],
|
|
- this.device.isMobile,
|
|
|
|
|
|
+ this.device.editor.isMobile,
|
|
);
|
|
);
|
|
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
|
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
|
let url = this.hitLinkElement.link;
|
|
let url = this.hitLinkElement.link;
|
|
@@ -4791,7 +4812,7 @@ class App extends React.Component<AppProps, AppState> {
|
|
);
|
|
);
|
|
const clicklength =
|
|
const clicklength =
|
|
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
|
|
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
|
|
- if (this.device.isMobile && clicklength < 300) {
|
|
|
|
|
|
+ if (this.device.editor.isMobile && clicklength < 300) {
|
|
const hitElement = this.getElementAtPosition(
|
|
const hitElement = this.getElementAtPosition(
|
|
scenePointer.x,
|
|
scenePointer.x,
|
|
scenePointer.y,
|
|
scenePointer.y,
|