refactor: single source of truths with editor interface (#10178)
* refactor device to editor interface and derive styles panel * allow host app to control form factor and ui mode * add editor interface event listener * put new props inside UIOptions * refactor: move related apis into one file * expose getFormFactor * privatize the setting of desktop mode and fix snapshots * refactor and fix test * remove unimplemented code * export getFormFactor() * replace `getFormFactor` with `getEditorInterface` * remove dead & useless * comment * fix ts --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
@@ -53,7 +53,11 @@ import { getToolbarTools } from "./shapes";
|
||||
|
||||
import "./Actions.scss";
|
||||
|
||||
import { useDevice, useExcalidrawContainer } from "./App";
|
||||
import {
|
||||
useEditorInterface,
|
||||
useStylesPanelMode,
|
||||
useExcalidrawContainer,
|
||||
} from "./App";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { ToolPopover } from "./ToolPopover";
|
||||
@@ -151,7 +155,7 @@ export const SelectedShapeActions = ({
|
||||
const isEditingTextOrNewElement = Boolean(
|
||||
appState.editingTextElement || appState.newElement,
|
||||
);
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||
|
||||
const showFillIcons =
|
||||
@@ -292,8 +296,10 @@ export const SelectedShapeActions = ({
|
||||
<fieldset>
|
||||
<legend>{t("labels.actions")}</legend>
|
||||
<div className="buttonList">
|
||||
{!device.editor.isMobile && renderAction("duplicateSelection")}
|
||||
{!device.editor.isMobile && renderAction("deleteSelectedElements")}
|
||||
{editorInterface.formFactor !== "phone" &&
|
||||
renderAction("duplicateSelection")}
|
||||
{editorInterface.formFactor !== "phone" &&
|
||||
renderAction("deleteSelectedElements")}
|
||||
{renderAction("group")}
|
||||
{renderAction("ungroup")}
|
||||
{showLinkIcon && renderAction("hyperlink")}
|
||||
@@ -1041,6 +1047,9 @@ export const ShapesSwitcher = ({
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
}) => {
|
||||
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||
const stylesPanelMode = useStylesPanelMode();
|
||||
const isFullStylesPanel = stylesPanelMode === "full";
|
||||
const isCompactStylesPanel = stylesPanelMode === "compact";
|
||||
|
||||
const SELECTION_TOOLS = [
|
||||
{
|
||||
@@ -1058,7 +1067,7 @@ export const ShapesSwitcher = ({
|
||||
const frameToolSelected = activeTool.type === "frame";
|
||||
const laserToolSelected = activeTool.type === "laser";
|
||||
const lassoToolSelected =
|
||||
app.state.stylesPanelMode === "full" &&
|
||||
isFullStylesPanel &&
|
||||
activeTool.type === "lasso" &&
|
||||
app.state.preferredSelectionTool.type !== "lasso";
|
||||
|
||||
@@ -1091,7 +1100,7 @@ export const ShapesSwitcher = ({
|
||||
// use a ToolPopover for selection/lasso toggle as well
|
||||
if (
|
||||
(value === "selection" || value === "lasso") &&
|
||||
app.state.stylesPanelMode === "compact"
|
||||
isCompactStylesPanel
|
||||
) {
|
||||
return (
|
||||
<ToolPopover
|
||||
@@ -1225,7 +1234,7 @@ export const ShapesSwitcher = ({
|
||||
>
|
||||
{t("toolBar.laser")}
|
||||
</DropdownMenu.Item>
|
||||
{app.state.stylesPanelMode === "full" && (
|
||||
{isFullStylesPanel && (
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "lasso" })}
|
||||
icon={LassoIcon}
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
FRAME_STYLE,
|
||||
IMAGE_MIME_TYPES,
|
||||
IMAGE_RENDER_TIMEOUT,
|
||||
isBrave,
|
||||
LINE_CONFIRM_THRESHOLD,
|
||||
MAX_ALLOWED_FILE_BYTES,
|
||||
MIME_TYPES,
|
||||
@@ -55,13 +54,11 @@ import {
|
||||
ZOOM_STEP,
|
||||
POINTER_EVENTS,
|
||||
TOOL_TYPE,
|
||||
isIOS,
|
||||
supportsResizeObserver,
|
||||
DEFAULT_COLLISION_THRESHOLD,
|
||||
DEFAULT_TEXT_ALIGN,
|
||||
ARROW_TYPE,
|
||||
DEFAULT_REDUCED_GLOBAL_ALPHA,
|
||||
isSafari,
|
||||
isLocalLink,
|
||||
normalizeLink,
|
||||
toValidURL,
|
||||
@@ -98,12 +95,16 @@ import {
|
||||
Emitter,
|
||||
MINIMUM_ARROW_SIZE,
|
||||
DOUBLE_TAP_POSITION_THRESHOLD,
|
||||
isMobileOrTablet,
|
||||
MQ_MAX_MOBILE,
|
||||
MQ_MIN_TABLET,
|
||||
MQ_MAX_TABLET,
|
||||
MQ_MAX_HEIGHT_LANDSCAPE,
|
||||
MQ_MAX_WIDTH_LANDSCAPE,
|
||||
createUserAgentDescriptor,
|
||||
getFormFactor,
|
||||
deriveStylesPanelMode,
|
||||
isIOS,
|
||||
isBrave,
|
||||
isSafari,
|
||||
type EditorInterface,
|
||||
type StylesPanelMode,
|
||||
loadDesktopUIModePreference,
|
||||
setDesktopUIMode,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
@@ -460,7 +461,6 @@ import type {
|
||||
LibraryItems,
|
||||
PointerDownState,
|
||||
SceneData,
|
||||
Device,
|
||||
FrameNameBoundsCache,
|
||||
SidebarName,
|
||||
SidebarTabName,
|
||||
@@ -481,19 +481,20 @@ import type { Action, ActionResult } from "../actions/types";
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
|
||||
const deviceContextInitialValue = {
|
||||
viewport: {
|
||||
isMobile: false,
|
||||
isLandscape: false,
|
||||
},
|
||||
editor: {
|
||||
isMobile: false,
|
||||
canFitSidebar: false,
|
||||
},
|
||||
const editorInterfaceContextInitialValue: EditorInterface = {
|
||||
formFactor: "desktop",
|
||||
desktopUIMode: "full",
|
||||
userAgent: createUserAgentDescriptor(
|
||||
typeof navigator !== "undefined" ? navigator.userAgent : "",
|
||||
),
|
||||
isTouchScreen: false,
|
||||
canFitSidebar: false,
|
||||
isLandscape: true,
|
||||
};
|
||||
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
||||
DeviceContext.displayName = "DeviceContext";
|
||||
const EditorInterfaceContext = React.createContext<EditorInterface>(
|
||||
editorInterfaceContextInitialValue,
|
||||
);
|
||||
EditorInterfaceContext.displayName = "EditorInterfaceContext";
|
||||
|
||||
export const ExcalidrawContainerContext = React.createContext<{
|
||||
container: HTMLDivElement | null;
|
||||
@@ -529,7 +530,10 @@ ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
|
||||
|
||||
export const useApp = () => useContext(AppContext);
|
||||
export const useAppProps = () => useContext(AppPropsContext);
|
||||
export const useDevice = () => useContext<Device>(DeviceContext);
|
||||
export const useEditorInterface = () =>
|
||||
useContext<EditorInterface>(EditorInterfaceContext);
|
||||
export const useStylesPanelMode = () =>
|
||||
deriveStylesPanelMode(useEditorInterface());
|
||||
export const useExcalidrawContainer = () =>
|
||||
useContext(ExcalidrawContainerContext);
|
||||
export const useExcalidrawElements = () =>
|
||||
@@ -577,7 +581,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
rc: RoughCanvas;
|
||||
unmounted: boolean = false;
|
||||
actionManager: ActionManager;
|
||||
device: Device = deviceContextInitialValue;
|
||||
editorInterface: EditorInterface = editorInterfaceContextInitialValue;
|
||||
private stylesPanelMode: StylesPanelMode = deriveStylesPanelMode(
|
||||
editorInterfaceContextInitialValue,
|
||||
);
|
||||
|
||||
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
@@ -693,6 +700,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
height: window.innerHeight,
|
||||
};
|
||||
|
||||
this.refreshEditorInterface();
|
||||
this.stylesPanelMode = deriveStylesPanelMode(this.editorInterface);
|
||||
|
||||
this.id = nanoid();
|
||||
this.library = new Library(this);
|
||||
this.actionManager = new ActionManager(
|
||||
@@ -739,6 +749,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
setActiveTool: this.setActiveTool,
|
||||
setCursor: this.setCursor,
|
||||
resetCursor: this.resetCursor,
|
||||
getEditorInterface: () => this.editorInterface,
|
||||
updateFrameRendering: this.updateFrameRendering,
|
||||
toggleSidebar: this.toggleSidebar,
|
||||
onChange: (cb) => this.onChangeEmitter.on(cb),
|
||||
@@ -1567,7 +1578,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
"excalidraw--view-mode":
|
||||
this.state.viewModeEnabled ||
|
||||
this.state.openDialog?.name === "elementLinkSelector",
|
||||
"excalidraw--mobile": this.device.editor.isMobile,
|
||||
"excalidraw--mobile": this.editorInterface.formFactor === "phone",
|
||||
})}
|
||||
style={{
|
||||
["--ui-pointerEvents" as any]: shouldBlockPointerEvents
|
||||
@@ -1589,7 +1600,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
<ExcalidrawContainerContext.Provider
|
||||
value={this.excalidrawContainerValue}
|
||||
>
|
||||
<DeviceContext.Provider value={this.device}>
|
||||
<EditorInterfaceContext.Provider value={this.editorInterface}>
|
||||
<ExcalidrawSetAppStateContext.Provider value={this.setAppState}>
|
||||
<ExcalidrawAppStateContext.Provider value={this.state}>
|
||||
<ExcalidrawElementsContext.Provider
|
||||
@@ -1817,7 +1828,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
renderScrollbars={
|
||||
this.props.renderScrollbars === true
|
||||
}
|
||||
device={this.device}
|
||||
editorInterface={this.editorInterface}
|
||||
renderInteractiveSceneCallback={
|
||||
this.renderInteractiveSceneCallback
|
||||
}
|
||||
@@ -1853,7 +1864,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
</ExcalidrawElementsContext.Provider>
|
||||
</ExcalidrawAppStateContext.Provider>
|
||||
</ExcalidrawSetAppStateContext.Provider>
|
||||
</DeviceContext.Provider>
|
||||
</EditorInterfaceContext.Provider>
|
||||
</ExcalidrawContainerContext.Provider>
|
||||
</AppPropsContext.Provider>
|
||||
</AppContext.Provider>
|
||||
@@ -2370,7 +2381,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
if (!scene.appState.preferredSelectionTool.initialized) {
|
||||
scene.appState.preferredSelectionTool = {
|
||||
type: this.device.editor.isMobile ? "lasso" : "selection",
|
||||
type:
|
||||
this.editorInterface.formFactor === "phone" ? "lasso" : "selection",
|
||||
initialized: true,
|
||||
};
|
||||
}
|
||||
@@ -2430,44 +2442,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
};
|
||||
|
||||
private isMobileBreakpoint = (width: number, height: number) => {
|
||||
private getFormFactor = (editorWidth: number, editorHeight: number) => {
|
||||
return (
|
||||
width <= MQ_MAX_MOBILE ||
|
||||
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE)
|
||||
this.props.UIOptions.formFactor ??
|
||||
getFormFactor(editorWidth, editorHeight)
|
||||
);
|
||||
};
|
||||
|
||||
private isTabletBreakpoint = (editorWidth: number, editorHeight: number) => {
|
||||
const minSide = Math.min(editorWidth, editorHeight);
|
||||
const maxSide = Math.max(editorWidth, editorHeight);
|
||||
|
||||
return minSide >= MQ_MIN_TABLET && maxSide <= MQ_MAX_TABLET;
|
||||
};
|
||||
|
||||
private refreshViewportBreakpoints = () => {
|
||||
const container = this.excalidrawContainerRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width: editorWidth, height: editorHeight } =
|
||||
container.getBoundingClientRect();
|
||||
|
||||
const prevViewportState = this.device.viewport;
|
||||
|
||||
const nextViewportState = updateObject(prevViewportState, {
|
||||
isLandscape: editorWidth > editorHeight,
|
||||
isMobile: this.isMobileBreakpoint(editorWidth, editorHeight),
|
||||
});
|
||||
|
||||
if (prevViewportState !== nextViewportState) {
|
||||
this.device = { ...this.device, viewport: nextViewportState };
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
private refreshEditorBreakpoints = () => {
|
||||
public refreshEditorInterface = () => {
|
||||
const container = this.excalidrawContainerRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
@@ -2476,47 +2458,56 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const { width: editorWidth, height: editorHeight } =
|
||||
container.getBoundingClientRect();
|
||||
|
||||
const storedDesktopUIMode = loadDesktopUIModePreference();
|
||||
const userAgentDescriptor = createUserAgentDescriptor(
|
||||
typeof navigator !== "undefined" ? navigator.userAgent : "",
|
||||
);
|
||||
// allow host app to control formFactor and desktopUIMode via props
|
||||
const sidebarBreakpoint =
|
||||
this.props.UIOptions.dockedSidebarBreakpoint != null
|
||||
? this.props.UIOptions.dockedSidebarBreakpoint
|
||||
: MQ_RIGHT_SIDEBAR_MIN_WIDTH;
|
||||
|
||||
const prevEditorState = this.device.editor;
|
||||
|
||||
const nextEditorState = updateObject(prevEditorState, {
|
||||
isMobile: this.isMobileBreakpoint(editorWidth, editorHeight),
|
||||
const nextEditorInterface = updateObject(this.editorInterface, {
|
||||
desktopUIMode:
|
||||
this.props.UIOptions.desktopUIMode ??
|
||||
storedDesktopUIMode ??
|
||||
this.editorInterface.desktopUIMode,
|
||||
formFactor: this.getFormFactor(editorWidth, editorHeight),
|
||||
userAgent: userAgentDescriptor,
|
||||
canFitSidebar: editorWidth > sidebarBreakpoint,
|
||||
isLandscape: editorWidth > editorHeight,
|
||||
});
|
||||
|
||||
const stylesPanelMode =
|
||||
// NOTE: we could also remove the isMobileOrTablet check here and
|
||||
// always switch to compact mode when the editor is narrow (e.g. < MQ_MIN_WIDTH_DESKTOP)
|
||||
// but not too narrow (> MQ_MAX_WIDTH_MOBILE)
|
||||
this.isTabletBreakpoint(editorWidth, editorHeight) && isMobileOrTablet()
|
||||
? "compact"
|
||||
: this.isMobileBreakpoint(editorWidth, editorHeight)
|
||||
? "mobile"
|
||||
: "full";
|
||||
this.editorInterface = nextEditorInterface;
|
||||
this.reconcileStylesPanelMode(nextEditorInterface);
|
||||
};
|
||||
|
||||
// also check if we need to update the app state
|
||||
this.setState((prevState) => ({
|
||||
stylesPanelMode,
|
||||
// reset to box selection mode if the UI changes to full
|
||||
// where you'd not be able to change the mode yourself currently
|
||||
preferredSelectionTool:
|
||||
stylesPanelMode === "full"
|
||||
? {
|
||||
type: "selection",
|
||||
initialized: true,
|
||||
}
|
||||
: prevState.preferredSelectionTool,
|
||||
}));
|
||||
|
||||
if (prevEditorState !== nextEditorState) {
|
||||
this.device = { ...this.device, editor: nextEditorState };
|
||||
return true;
|
||||
private reconcileStylesPanelMode = (nextEditorInterface: EditorInterface) => {
|
||||
const nextStylesPanelMode = deriveStylesPanelMode(nextEditorInterface);
|
||||
if (nextStylesPanelMode === this.stylesPanelMode) {
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
|
||||
const prevStylesPanelMode = this.stylesPanelMode;
|
||||
this.stylesPanelMode = nextStylesPanelMode;
|
||||
|
||||
if (prevStylesPanelMode !== "full" && nextStylesPanelMode === "full") {
|
||||
this.setState((prevState) => ({
|
||||
preferredSelectionTool: {
|
||||
type: "selection",
|
||||
initialized: true,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
/** TO BE USED LATER */
|
||||
private setDesktopUIMode = (mode: EditorInterface["desktopUIMode"]) => {
|
||||
const nextMode = setDesktopUIMode(mode);
|
||||
this.editorInterface = updateObject(this.editorInterface, {
|
||||
desktopUIMode: nextMode,
|
||||
});
|
||||
this.reconcileStylesPanelMode(this.editorInterface);
|
||||
};
|
||||
|
||||
private clearImageShapeCache(filesMap?: BinaryFiles) {
|
||||
@@ -2588,19 +2579,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.focusContainer();
|
||||
}
|
||||
|
||||
if (
|
||||
// bounding rects don't work in tests so updating
|
||||
// the state on init would result in making the test enviro run
|
||||
// in mobile breakpoint (0 width/height), making everything fail
|
||||
!isTestEnv()
|
||||
) {
|
||||
this.refreshViewportBreakpoints();
|
||||
this.refreshEditorBreakpoints();
|
||||
}
|
||||
|
||||
if (supportsResizeObserver && this.excalidrawContainerRef.current) {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.refreshEditorBreakpoints();
|
||||
this.refreshEditorInterface();
|
||||
this.updateDOMRect();
|
||||
});
|
||||
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
|
||||
@@ -2654,11 +2635,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene
|
||||
.getElementsIncludingDeleted()
|
||||
.forEach((element) => ShapeCache.delete(element));
|
||||
this.refreshViewportBreakpoints();
|
||||
this.refreshEditorInterface();
|
||||
this.updateDOMRect();
|
||||
if (!supportsResizeObserver) {
|
||||
this.refreshEditorBreakpoints();
|
||||
}
|
||||
this.setState({});
|
||||
});
|
||||
|
||||
@@ -2817,13 +2795,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({ showWelcomeScreen: true });
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.UIOptions.dockedSidebarBreakpoint !==
|
||||
this.props.UIOptions.dockedSidebarBreakpoint
|
||||
) {
|
||||
this.refreshEditorBreakpoints();
|
||||
}
|
||||
|
||||
const hasFollowedPersonLeft =
|
||||
prevState.userToFollow &&
|
||||
!this.state.collaborators.has(prevState.userToFollow.socketId);
|
||||
@@ -3178,7 +3149,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.addElementsFromPasteOrLibrary({
|
||||
elements,
|
||||
files: data.files || null,
|
||||
position: isMobileOrTablet() ? "center" : "cursor",
|
||||
position:
|
||||
this.editorInterface.formFactor === "desktop" ? "cursor" : "center",
|
||||
retainSeed: isPlainPaste,
|
||||
});
|
||||
return;
|
||||
@@ -3203,7 +3175,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.addElementsFromPasteOrLibrary({
|
||||
elements,
|
||||
files,
|
||||
position: isMobileOrTablet() ? "center" : "cursor",
|
||||
position:
|
||||
this.editorInterface.formFactor === "desktop" ? "cursor" : "center",
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -3429,7 +3402,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// from library, not when pasting from clipboard. Alas.
|
||||
openSidebar:
|
||||
this.state.openSidebar &&
|
||||
this.device.editor.canFitSidebar &&
|
||||
this.editorInterface.canFitSidebar &&
|
||||
editorJotaiStore.get(isSidebarDockedAtom)
|
||||
? this.state.openSidebar
|
||||
: null,
|
||||
@@ -3627,7 +3600,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
!isPlainPaste &&
|
||||
textElements.length > 1 &&
|
||||
PLAIN_PASTE_TOAST_SHOWN === false &&
|
||||
!this.device.editor.isMobile
|
||||
this.editorInterface.formFactor !== "phone"
|
||||
) {
|
||||
this.setToast({
|
||||
message: t("toast.pasteAsSingleElement", {
|
||||
@@ -3659,7 +3632,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
trackEvent(
|
||||
"toolbar",
|
||||
"toggleLock",
|
||||
`${source} (${this.device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||
`${source} (${
|
||||
this.editorInterface.formFactor === "phone" ? "mobile" : "desktop"
|
||||
})`,
|
||||
);
|
||||
}
|
||||
this.setState((prevState) => {
|
||||
@@ -4011,12 +3986,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
if (appState) {
|
||||
this.setState({
|
||||
...appState,
|
||||
// keep existing stylesPanelMode as it needs to be preserved
|
||||
// or set at startup
|
||||
stylesPanelMode: this.state.stylesPanelMode,
|
||||
} as Pick<AppState, K> | null);
|
||||
this.setState(appState as Pick<AppState, K> | null);
|
||||
}
|
||||
|
||||
if (elements) {
|
||||
@@ -4594,7 +4564,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
"toolbar",
|
||||
shape,
|
||||
`keyboard (${
|
||||
this.device.editor.isMobile ? "mobile" : "desktop"
|
||||
this.editorInterface.formFactor === "phone"
|
||||
? "mobile"
|
||||
: "desktop"
|
||||
})`,
|
||||
);
|
||||
}
|
||||
@@ -5100,7 +5072,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// caret (i.e. deselect). There's not much use for always selecting
|
||||
// the text on edit anyway (and users can select-all from contextmenu
|
||||
// if needed)
|
||||
autoSelect: !this.device.isTouchScreen,
|
||||
autoSelect: !this.editorInterface.isTouchScreen,
|
||||
});
|
||||
// deselect all other elements when inserting text
|
||||
this.deselectElements();
|
||||
@@ -5263,7 +5235,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (
|
||||
considerBoundingBox &&
|
||||
this.state.selectedElementIds[element.id] &&
|
||||
hasBoundingBox([element], this.state)
|
||||
hasBoundingBox([element], this.state, this.editorInterface)
|
||||
) {
|
||||
// if hitting the bounding box, return early
|
||||
// but if not, we should check for other cases as well (e.g. frame name)
|
||||
@@ -5733,7 +5705,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state,
|
||||
pointFrom(scenePointer.x, scenePointer.y),
|
||||
this.device.editor.isMobile,
|
||||
this.editorInterface.formFactor === "phone",
|
||||
)
|
||||
) {
|
||||
return element;
|
||||
@@ -5768,7 +5740,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
elementsMap,
|
||||
this.state,
|
||||
pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y),
|
||||
this.device.editor.isMobile,
|
||||
this.editorInterface.formFactor === "phone",
|
||||
);
|
||||
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
||||
this.lastPointerUpEvent!,
|
||||
@@ -5779,7 +5751,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
elementsMap,
|
||||
this.state,
|
||||
pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y),
|
||||
this.device.editor.isMobile,
|
||||
this.editorInterface.formFactor === "phone",
|
||||
);
|
||||
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
||||
hideHyperlinkToolip();
|
||||
@@ -6171,7 +6143,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// better way of showing them is found
|
||||
!(
|
||||
isLinearElement(selectedElements[0]) &&
|
||||
(isMobileOrTablet() || selectedElements[0].points.length === 2)
|
||||
(this.editorInterface.userAgent.isMobileDevice ||
|
||||
selectedElements[0].points.length === 2)
|
||||
)
|
||||
) {
|
||||
const elementWithTransformHandleType =
|
||||
@@ -6183,7 +6156,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.device,
|
||||
this.editorInterface,
|
||||
);
|
||||
if (
|
||||
elementWithTransformHandleType &&
|
||||
@@ -6207,7 +6180,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
scenePointerY,
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
this.device,
|
||||
this.editorInterface,
|
||||
);
|
||||
if (transformHandleType) {
|
||||
setCursor(
|
||||
@@ -6593,10 +6566,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
if (
|
||||
!this.device.isTouchScreen &&
|
||||
!this.editorInterface.isTouchScreen &&
|
||||
["pen", "touch"].includes(event.pointerType)
|
||||
) {
|
||||
this.device = updateObject(this.device, { isTouchScreen: true });
|
||||
this.editorInterface = updateObject(this.editorInterface, {
|
||||
isTouchScreen: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (isPanning) {
|
||||
@@ -6730,12 +6705,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
// block dragging after lasso selection on PCs until the next pointer down
|
||||
// (on mobile or tablet, we want to allow user to drag immediately)
|
||||
pointerDownState.drag.blockDragging = !isMobileOrTablet();
|
||||
pointerDownState.drag.blockDragging =
|
||||
this.editorInterface.formFactor === "desktop";
|
||||
}
|
||||
|
||||
// only for mobile or tablet, if we hit an element, select it immediately like normal selection
|
||||
if (
|
||||
isMobileOrTablet() &&
|
||||
this.editorInterface.formFactor !== "desktop" &&
|
||||
pointerDownState.hit.element &&
|
||||
!hitSelectedElement
|
||||
) {
|
||||
@@ -6919,7 +6895,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const clicklength =
|
||||
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
|
||||
|
||||
if (this.device.editor.isMobile && clicklength < 300) {
|
||||
if (this.editorInterface.formFactor === "phone" && clicklength < 300) {
|
||||
const hitElement = this.getElementAtPosition(
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
@@ -6938,7 +6914,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.device.isTouchScreen) {
|
||||
if (this.editorInterface.isTouchScreen) {
|
||||
const hitElement = this.getElementAtPosition(
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
@@ -6968,7 +6944,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
this.handleEmbeddableCenterClick(this.hitLinkElement);
|
||||
} else {
|
||||
this.redirectToLink(event, this.device.isTouchScreen);
|
||||
this.redirectToLink(event, this.editorInterface.isTouchScreen);
|
||||
}
|
||||
} else if (this.state.viewModeEnabled) {
|
||||
this.setState({
|
||||
@@ -7293,7 +7269,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
!isElbowArrow(selectedElements[0]) &&
|
||||
!(
|
||||
isLinearElement(selectedElements[0]) &&
|
||||
(isMobileOrTablet() || selectedElements[0].points.length === 2)
|
||||
(this.editorInterface.userAgent.isMobileDevice ||
|
||||
selectedElements[0].points.length === 2)
|
||||
) &&
|
||||
!(
|
||||
this.state.selectedLinearElement &&
|
||||
@@ -7309,7 +7286,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.device,
|
||||
this.editorInterface,
|
||||
);
|
||||
if (elementWithTransformHandleType != null) {
|
||||
if (
|
||||
@@ -7338,7 +7315,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointerDownState.origin.y,
|
||||
this.state.zoom,
|
||||
event.pointerType,
|
||||
this.device,
|
||||
this.editorInterface,
|
||||
);
|
||||
}
|
||||
if (pointerDownState.resize.handleType) {
|
||||
@@ -8540,7 +8517,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (
|
||||
this.state.activeTool.type === "lasso" &&
|
||||
this.lassoTrail.hasCurrentTrail &&
|
||||
!(isMobileOrTablet() && pointerDownState.hit.element) &&
|
||||
!(
|
||||
this.editorInterface.formFactor !== "desktop" &&
|
||||
pointerDownState.hit.element
|
||||
) &&
|
||||
!this.state.activeTool.fromSelection
|
||||
) {
|
||||
return;
|
||||
@@ -9388,7 +9368,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
newElement &&
|
||||
!multiElement
|
||||
) {
|
||||
if (this.device.isTouchScreen) {
|
||||
if (this.editorInterface.isTouchScreen) {
|
||||
const FIXED_DELTA_X = Math.min(
|
||||
(this.state.width * 0.7) / this.state.zoom.value,
|
||||
100,
|
||||
@@ -11206,7 +11186,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
const zIndexActions: ContextMenuItems =
|
||||
this.state.stylesPanelMode === "full"
|
||||
this.editorInterface.formFactor === "desktop"
|
||||
? [
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
actionSendBackward,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { KEYS } from "@excalidraw/common";
|
||||
import { getShortcutKey } from "../..//shortcut";
|
||||
import { useAtom } from "../../editor-jotai";
|
||||
import { t } from "../../i18n";
|
||||
import { useDevice } from "../App";
|
||||
import { useEditorInterface } from "../App";
|
||||
import { activeEyeDropperAtom } from "../EyeDropper";
|
||||
import { eyeDropperIcon } from "../icons";
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ColorInput = ({
|
||||
colorPickerType,
|
||||
placeholder,
|
||||
}: ColorInputProps) => {
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
const [innerValue, setInnerValue] = useState(color);
|
||||
const [activeSection, setActiveColorPickerSection] = useAtom(
|
||||
activeColorPickerSectionAtom,
|
||||
@@ -99,7 +99,7 @@ export const ColorInput = ({
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{/* TODO reenable on mobile with a better UX */}
|
||||
{!device.editor.isMobile && (
|
||||
{editorInterface.formFactor !== "phone" && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { useAtom } from "../../editor-jotai";
|
||||
import { t } from "../../i18n";
|
||||
import { useExcalidrawContainer } from "../App";
|
||||
import { useExcalidrawContainer, useStylesPanelMode } from "../App";
|
||||
import { ButtonSeparator } from "../ButtonSeparator";
|
||||
import { activeEyeDropperAtom } from "../EyeDropper";
|
||||
import { PropertiesPopover } from "../PropertiesPopover";
|
||||
@@ -73,7 +73,6 @@ interface ColorPickerProps {
|
||||
palette?: ColorPaletteCustom | null;
|
||||
topPicks?: ColorTuple;
|
||||
updateData: (formData?: any) => void;
|
||||
compactMode?: boolean;
|
||||
}
|
||||
|
||||
const ColorPickerPopupContent = ({
|
||||
@@ -100,6 +99,9 @@ const ColorPickerPopupContent = ({
|
||||
getOpenPopup: () => AppState["openPopup"];
|
||||
}) => {
|
||||
const { container } = useExcalidrawContainer();
|
||||
const stylesPanelMode = useStylesPanelMode();
|
||||
const isCompactMode = stylesPanelMode !== "full";
|
||||
const isMobileMode = stylesPanelMode === "mobile";
|
||||
const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom);
|
||||
|
||||
const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom);
|
||||
@@ -216,11 +218,8 @@ const ColorPickerPopupContent = ({
|
||||
type={type}
|
||||
elements={elements}
|
||||
updateData={updateData}
|
||||
showTitle={
|
||||
appState.stylesPanelMode === "compact" ||
|
||||
appState.stylesPanelMode === "mobile"
|
||||
}
|
||||
showHotKey={appState.stylesPanelMode !== "mobile"}
|
||||
showTitle={isCompactMode}
|
||||
showHotKey={!isMobileMode}
|
||||
>
|
||||
{colorInputJSX}
|
||||
</Picker>
|
||||
@@ -235,7 +234,6 @@ const ColorPickerTrigger = ({
|
||||
label,
|
||||
color,
|
||||
type,
|
||||
stylesPanelMode,
|
||||
mode = "background",
|
||||
onToggle,
|
||||
editingTextElement,
|
||||
@@ -243,11 +241,13 @@ const ColorPickerTrigger = ({
|
||||
color: string | null;
|
||||
label: string;
|
||||
type: ColorPickerType;
|
||||
stylesPanelMode?: AppState["stylesPanelMode"];
|
||||
mode?: "background" | "stroke";
|
||||
onToggle: () => void;
|
||||
editingTextElement?: boolean;
|
||||
}) => {
|
||||
const stylesPanelMode = useStylesPanelMode();
|
||||
const isCompactMode = stylesPanelMode !== "full";
|
||||
const isMobileMode = stylesPanelMode === "mobile";
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
// use pointerdown so we run before outside-close logic
|
||||
e.preventDefault();
|
||||
@@ -268,9 +268,8 @@ const ColorPickerTrigger = ({
|
||||
"is-transparent": !color || color === "transparent",
|
||||
"has-outline":
|
||||
!color || !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
|
||||
"compact-sizing":
|
||||
stylesPanelMode === "compact" || stylesPanelMode === "mobile",
|
||||
"mobile-border": stylesPanelMode === "mobile",
|
||||
"compact-sizing": isCompactMode,
|
||||
"mobile-border": isMobileMode,
|
||||
})}
|
||||
aria-label={label}
|
||||
style={color ? { "--swatch-color": color } : undefined}
|
||||
@@ -283,22 +282,20 @@ const ColorPickerTrigger = ({
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="color-picker__button-outline">{!color && slashIcon}</div>
|
||||
{(stylesPanelMode === "compact" || stylesPanelMode === "mobile") &&
|
||||
color &&
|
||||
mode === "stroke" && (
|
||||
<div className="color-picker__button-background">
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
color && isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD)
|
||||
? "#fff"
|
||||
: "#111",
|
||||
}}
|
||||
>
|
||||
{strokeIcon}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isCompactMode && color && mode === "stroke" && (
|
||||
<div className="color-picker__button-background">
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
color && isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD)
|
||||
? "#fff"
|
||||
: "#111",
|
||||
}}
|
||||
>
|
||||
{strokeIcon}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Popover.Trigger>
|
||||
);
|
||||
};
|
||||
@@ -318,10 +315,8 @@ export const ColorPicker = ({
|
||||
useEffect(() => {
|
||||
openRef.current = appState.openPopup;
|
||||
}, [appState.openPopup]);
|
||||
const compactMode =
|
||||
type !== "canvasBackground" &&
|
||||
(appState.stylesPanelMode === "compact" ||
|
||||
appState.stylesPanelMode === "mobile");
|
||||
const stylesPanelMode = useStylesPanelMode();
|
||||
const isCompactMode = stylesPanelMode !== "full";
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -329,10 +324,10 @@ export const ColorPicker = ({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
className={clsx("color-picker-container", {
|
||||
"color-picker-container--no-top-picks": compactMode,
|
||||
"color-picker-container--no-top-picks": isCompactMode,
|
||||
})}
|
||||
>
|
||||
{!compactMode && (
|
||||
{!isCompactMode && (
|
||||
<TopPicks
|
||||
activeColor={color}
|
||||
onChange={onChange}
|
||||
@@ -340,7 +335,7 @@ export const ColorPicker = ({
|
||||
topPicks={topPicks}
|
||||
/>
|
||||
)}
|
||||
{!compactMode && <ButtonSeparator />}
|
||||
{!isCompactMode && <ButtonSeparator />}
|
||||
<Popover.Root
|
||||
open={appState.openPopup === type}
|
||||
onOpenChange={(open) => {
|
||||
@@ -354,7 +349,6 @@ export const ColorPicker = ({
|
||||
color={color}
|
||||
label={label}
|
||||
type={type}
|
||||
stylesPanelMode={appState.stylesPanelMode}
|
||||
mode={type === "elementStroke" ? "stroke" : "background"}
|
||||
editingTextElement={!!appState.editingTextElement}
|
||||
onToggle={() => {
|
||||
|
||||
@@ -903,7 +903,7 @@ function CommandPaletteInner({
|
||||
ref={inputRef}
|
||||
/>
|
||||
|
||||
{!app.device.viewport.isMobile && (
|
||||
{app.editorInterface.formFactor !== "phone" && (
|
||||
<div className="shortcuts-wrapper">
|
||||
<CommandShortcutHint shortcut="↑↓">
|
||||
{t("commandPalette.shortcuts.select")}
|
||||
@@ -937,7 +937,7 @@ function CommandPaletteInner({
|
||||
onClick={(event) => executeCommand(lastUsed, event)}
|
||||
disabled={!isCommandAvailable(lastUsed)}
|
||||
onMouseMove={() => setCurrentCommand(lastUsed)}
|
||||
showShortcut={!app.device.viewport.isMobile}
|
||||
showShortcut={app.editorInterface.formFactor !== "phone"}
|
||||
appState={uiAppState}
|
||||
/>
|
||||
</div>
|
||||
@@ -955,7 +955,7 @@ function CommandPaletteInner({
|
||||
isSelected={command.label === currentCommand?.label}
|
||||
onClick={(event) => executeCommand(command, event)}
|
||||
onMouseMove={() => setCurrentCommand(command)}
|
||||
showShortcut={!app.device.viewport.isMobile}
|
||||
showShortcut={app.editorInterface.formFactor !== "phone"}
|
||||
appState={uiAppState}
|
||||
size={category === "Library" ? "large" : "small"}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { t } from "../i18n";
|
||||
|
||||
import {
|
||||
useExcalidrawContainer,
|
||||
useDevice,
|
||||
useEditorInterface,
|
||||
useExcalidrawSetAppState,
|
||||
} from "./App";
|
||||
import { Island } from "./Island";
|
||||
@@ -51,7 +51,7 @@ export const Dialog = (props: DialogProps) => {
|
||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||
const [lastActiveElement] = useState(document.activeElement);
|
||||
const { id } = useExcalidrawContainer();
|
||||
const isFullscreen = useDevice().viewport.isMobile;
|
||||
const isFullscreen = useEditorInterface().formFactor === "phone";
|
||||
|
||||
useEffect(() => {
|
||||
if (!islandNode) {
|
||||
|
||||
@@ -20,7 +20,12 @@ import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { Fonts } from "../../fonts";
|
||||
import { t } from "../../i18n";
|
||||
import { useApp, useAppProps, useExcalidrawContainer } from "../App";
|
||||
import {
|
||||
useApp,
|
||||
useAppProps,
|
||||
useExcalidrawContainer,
|
||||
useStylesPanelMode,
|
||||
} from "../App";
|
||||
import { PropertiesPopover } from "../PropertiesPopover";
|
||||
import { QuickSearch } from "../QuickSearch";
|
||||
import { ScrollableList } from "../ScrollableList";
|
||||
@@ -93,6 +98,7 @@ export const FontPickerList = React.memo(
|
||||
const app = useApp();
|
||||
const { fonts } = app;
|
||||
const { showDeprecatedFonts } = useAppProps();
|
||||
const stylesPanelMode = useStylesPanelMode();
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -338,7 +344,7 @@ export const FontPickerList = React.memo(
|
||||
onKeyDown={onKeyDown}
|
||||
preventAutoFocusOnTouch={!!app.state.editingTextElement}
|
||||
>
|
||||
{app.state.stylesPanelMode === "full" && (
|
||||
{stylesPanelMode === "full" && (
|
||||
<QuickSearch
|
||||
ref={inputRef}
|
||||
placeholder={t("quickSearch.placeholder")}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
|
||||
import { isNodeInFlowchart } from "@excalidraw/element";
|
||||
|
||||
import type { EditorInterface } from "@excalidraw/common";
|
||||
|
||||
import { t } from "../i18n";
|
||||
import { getShortcutKey } from "../shortcut";
|
||||
import { isEraserActive } from "../appState";
|
||||
@@ -18,12 +20,12 @@ import { isGridModeEnabled } from "../snapping";
|
||||
|
||||
import "./HintViewer.scss";
|
||||
|
||||
import type { AppClassProperties, Device, UIAppState } from "../types";
|
||||
import type { AppClassProperties, UIAppState } from "../types";
|
||||
|
||||
interface HintViewerProps {
|
||||
appState: UIAppState;
|
||||
isMobile: boolean;
|
||||
device: Device;
|
||||
editorInterface: EditorInterface;
|
||||
app: AppClassProperties;
|
||||
}
|
||||
|
||||
@@ -35,7 +37,7 @@ const getTaggedShortcutKey = (key: string | string[]) =>
|
||||
const getHints = ({
|
||||
appState,
|
||||
isMobile,
|
||||
device,
|
||||
editorInterface,
|
||||
app,
|
||||
}: HintViewerProps): null | string | string[] => {
|
||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||
@@ -51,7 +53,7 @@ const getHints = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (appState.openSidebar && !device.editor.canFitSidebar) {
|
||||
if (appState.openSidebar && !editorInterface.canFitSidebar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -225,13 +227,13 @@ const getHints = ({
|
||||
export const HintViewer = ({
|
||||
appState,
|
||||
isMobile,
|
||||
device,
|
||||
editorInterface,
|
||||
app,
|
||||
}: HintViewerProps) => {
|
||||
const hints = getHints({
|
||||
appState,
|
||||
isMobile,
|
||||
device,
|
||||
editorInterface,
|
||||
app,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { atom, useAtom } from "../editor-jotai";
|
||||
import { getLanguage, t } from "../i18n";
|
||||
|
||||
import Collapsible from "./Stats/Collapsible";
|
||||
import { useDevice, useExcalidrawContainer } from "./App";
|
||||
import { useEditorInterface, useExcalidrawContainer } from "./App";
|
||||
|
||||
import "./IconPicker.scss";
|
||||
|
||||
@@ -38,7 +38,7 @@ function Picker<T>({
|
||||
onClose: () => void;
|
||||
numberOfOptionsToAlwaysShow?: number;
|
||||
}) {
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
const { container } = useExcalidrawContainer();
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
@@ -153,7 +153,7 @@ function Picker<T>({
|
||||
);
|
||||
};
|
||||
|
||||
const isMobile = device.editor.isMobile;
|
||||
const isMobile = editorInterface.formFactor === "phone";
|
||||
|
||||
return (
|
||||
<Popover.Content
|
||||
|
||||
@@ -46,7 +46,7 @@ import Footer from "./footer/Footer";
|
||||
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||
import MainMenu from "./main-menu/MainMenu";
|
||||
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
|
||||
import { useDevice } from "./App";
|
||||
import { useEditorInterface, useStylesPanelMode } from "./App";
|
||||
import { OverwriteConfirmDialog } from "./OverwriteConfirm/OverwriteConfirm";
|
||||
import { LibraryIcon } from "./icons";
|
||||
import { DefaultSidebar } from "./DefaultSidebar";
|
||||
@@ -161,27 +161,28 @@ const LayerUI = ({
|
||||
isCollaborating,
|
||||
generateLinkForSelection,
|
||||
}: LayerUIProps) => {
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
const stylesPanelMode = useStylesPanelMode();
|
||||
const isCompactStylesPanel = stylesPanelMode === "compact";
|
||||
const tunnels = useInitializeTunnels();
|
||||
|
||||
const spacing =
|
||||
appState.stylesPanelMode === "compact"
|
||||
? {
|
||||
menuTopGap: 4,
|
||||
toolbarColGap: 4,
|
||||
toolbarRowGap: 1,
|
||||
toolbarInnerRowGap: 0.5,
|
||||
islandPadding: 1,
|
||||
collabMarginLeft: 8,
|
||||
}
|
||||
: {
|
||||
menuTopGap: 6,
|
||||
toolbarColGap: 4,
|
||||
toolbarRowGap: 1,
|
||||
toolbarInnerRowGap: 1,
|
||||
islandPadding: 1,
|
||||
collabMarginLeft: 8,
|
||||
};
|
||||
const spacing = isCompactStylesPanel
|
||||
? {
|
||||
menuTopGap: 4,
|
||||
toolbarColGap: 4,
|
||||
toolbarRowGap: 1,
|
||||
toolbarInnerRowGap: 0.5,
|
||||
islandPadding: 1,
|
||||
collabMarginLeft: 8,
|
||||
}
|
||||
: {
|
||||
menuTopGap: 6,
|
||||
toolbarColGap: 4,
|
||||
toolbarRowGap: 1,
|
||||
toolbarInnerRowGap: 1,
|
||||
islandPadding: 1,
|
||||
collabMarginLeft: 8,
|
||||
};
|
||||
|
||||
const TunnelsJotaiProvider = tunnels.tunnelsJotai.Provider;
|
||||
|
||||
@@ -236,7 +237,7 @@ const LayerUI = ({
|
||||
);
|
||||
|
||||
const renderSelectedShapeActions = () => {
|
||||
const isCompactMode = appState.stylesPanelMode === "compact";
|
||||
const isCompactMode = isCompactStylesPanel;
|
||||
|
||||
return (
|
||||
<Section
|
||||
@@ -308,7 +309,7 @@ const LayerUI = ({
|
||||
<div
|
||||
className={clsx("selected-shape-actions-container", {
|
||||
"selected-shape-actions-container--compact":
|
||||
appState.stylesPanelMode === "compact",
|
||||
isCompactStylesPanel,
|
||||
})}
|
||||
>
|
||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||
@@ -333,14 +334,13 @@ const LayerUI = ({
|
||||
padding={spacing.islandPadding}
|
||||
className={clsx("App-toolbar", {
|
||||
"zen-mode": appState.zenModeEnabled,
|
||||
"App-toolbar--compact":
|
||||
appState.stylesPanelMode === "compact",
|
||||
"App-toolbar--compact": isCompactStylesPanel,
|
||||
})}
|
||||
>
|
||||
<HintViewer
|
||||
appState={appState}
|
||||
isMobile={device.editor.isMobile}
|
||||
device={device}
|
||||
isMobile={editorInterface.formFactor === "phone"}
|
||||
editorInterface={editorInterface}
|
||||
app={app}
|
||||
/>
|
||||
{heading}
|
||||
@@ -406,8 +406,7 @@ const LayerUI = ({
|
||||
"layer-ui__wrapper__top-right zen-mode-transition",
|
||||
{
|
||||
"transition-right": appState.zenModeEnabled,
|
||||
"layer-ui__wrapper__top-right--compact":
|
||||
appState.stylesPanelMode === "compact",
|
||||
"layer-ui__wrapper__top-right--compact": isCompactStylesPanel,
|
||||
},
|
||||
)}
|
||||
>
|
||||
@@ -417,7 +416,10 @@ const LayerUI = ({
|
||||
userToFollow={appState.userToFollow?.socketId || null}
|
||||
/>
|
||||
)}
|
||||
{renderTopRightUI?.(device.editor.isMobile, appState)}
|
||||
{renderTopRightUI?.(
|
||||
editorInterface.formFactor === "phone",
|
||||
appState,
|
||||
)}
|
||||
{!appState.viewModeEnabled &&
|
||||
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||
// hide button when sidebar docked
|
||||
@@ -448,7 +450,9 @@ const LayerUI = ({
|
||||
trackEvent(
|
||||
"sidebar",
|
||||
`toggleDock (${docked ? "dock" : "undock"})`,
|
||||
`(${device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||
`(${
|
||||
editorInterface.formFactor === "phone" ? "mobile" : "desktop"
|
||||
})`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -476,13 +480,15 @@ const LayerUI = ({
|
||||
trackEvent(
|
||||
"sidebar",
|
||||
`${DEFAULT_SIDEBAR.name} (open)`,
|
||||
`button (${device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||
`button (${
|
||||
editorInterface.formFactor === "phone" ? "mobile" : "desktop"
|
||||
})`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
tab={DEFAULT_SIDEBAR.defaultTab}
|
||||
>
|
||||
{appState.stylesPanelMode === "full" &&
|
||||
{stylesPanelMode === "full" &&
|
||||
appState.width >= MQ_MIN_WIDTH_DESKTOP &&
|
||||
t("toolBar.library")}
|
||||
</DefaultSidebar.Trigger>
|
||||
@@ -496,7 +502,7 @@ const LayerUI = ({
|
||||
{appState.errorMessage}
|
||||
</ErrorDialog>
|
||||
)}
|
||||
{eyeDropperState && !device.editor.isMobile && (
|
||||
{eyeDropperState && editorInterface.formFactor !== "phone" && (
|
||||
<EyeDropper
|
||||
colorPickerType={eyeDropperState.colorPickerType}
|
||||
onCancel={() => {
|
||||
@@ -575,7 +581,7 @@ const LayerUI = ({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{device.editor.isMobile && (
|
||||
{editorInterface.formFactor === "phone" && (
|
||||
<MobileMenu
|
||||
app={app}
|
||||
appState={appState}
|
||||
@@ -593,14 +599,14 @@ const LayerUI = ({
|
||||
UIOptions={UIOptions}
|
||||
/>
|
||||
)}
|
||||
{!device.editor.isMobile && (
|
||||
{editorInterface.formFactor !== "phone" && (
|
||||
<>
|
||||
<div
|
||||
className="layer-ui__wrapper"
|
||||
style={
|
||||
appState.openSidebar &&
|
||||
isSidebarDocked &&
|
||||
device.editor.canFitSidebar
|
||||
editorInterface.canFitSidebar
|
||||
? { width: `calc(100% - var(--right-sidebar-width))` }
|
||||
: {}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import "./LibraryMenuItems.scss";
|
||||
|
||||
import { TextField } from "./TextField";
|
||||
|
||||
import { useDevice } from "./App";
|
||||
import { useEditorInterface } from "./App";
|
||||
|
||||
import { Button } from "./Button";
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function LibraryMenuItems({
|
||||
selectedItems: LibraryItem["id"][];
|
||||
onSelectItems: (id: LibraryItem["id"][]) => void;
|
||||
}) {
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
const libraryContainerRef = useRef<HTMLDivElement>(null);
|
||||
const scrollPosition = useScrollPosition<HTMLDivElement>(libraryContainerRef);
|
||||
|
||||
@@ -392,7 +392,7 @@ export default function LibraryMenuItems({
|
||||
ref={searchInputRef}
|
||||
type="search"
|
||||
className={clsx("library-menu-items-container__search", {
|
||||
hideCancelButton: !device.editor.isMobile,
|
||||
hideCancelButton: editorInterface.formFactor !== "phone",
|
||||
})}
|
||||
placeholder={t("library.search.inputPlaceholder")}
|
||||
value={searchInputValue}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { memo, useRef, useState } from "react";
|
||||
|
||||
import { useLibraryItemSvg } from "../hooks/useLibraryItemSvg";
|
||||
|
||||
import { useDevice } from "./App";
|
||||
import { useEditorInterface } from "./App";
|
||||
import { CheckboxItem } from "./CheckboxItem";
|
||||
import { PlusIcon } from "./icons";
|
||||
|
||||
@@ -36,7 +36,7 @@ export const LibraryUnit = memo(
|
||||
const svg = useLibraryItemSvg(id, elements, svgCache, ref);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isMobile = useDevice().editor.isMobile;
|
||||
const isMobile = useEditorInterface().formFactor === "phone";
|
||||
const adder = isPending && (
|
||||
<div className="library-unit__adder">{PlusIcon}</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { type ReactNode } from "react";
|
||||
|
||||
import { isInteractive } from "@excalidraw/common";
|
||||
|
||||
import { useDevice } from "./App";
|
||||
import { useEditorInterface } from "./App";
|
||||
import { Island } from "./Island";
|
||||
|
||||
interface PropertiesPopoverProps {
|
||||
@@ -39,9 +39,9 @@ export const PropertiesPopover = React.forwardRef<
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
const isMobilePortrait =
|
||||
device.editor.isMobile && !device.viewport.isLandscape;
|
||||
editorInterface.formFactor === "phone" && !editorInterface.isLandscape;
|
||||
|
||||
return (
|
||||
<Popover.Portal container={container}>
|
||||
@@ -56,7 +56,8 @@ export const PropertiesPopover = React.forwardRef<
|
||||
collisionBoundary={container ?? undefined}
|
||||
style={{
|
||||
zIndex: "var(--zIndex-ui-styles-popup)",
|
||||
marginLeft: device.editor.isMobile ? "0.5rem" : undefined,
|
||||
marginLeft:
|
||||
editorInterface.formFactor === "phone" ? "0.5rem" : undefined,
|
||||
}}
|
||||
onPointerLeave={onPointerLeave}
|
||||
onKeyDown={onKeyDown}
|
||||
@@ -64,7 +65,7 @@ export const PropertiesPopover = React.forwardRef<
|
||||
onPointerDownOutside={onPointerDownOutside}
|
||||
onOpenAutoFocus={(e) => {
|
||||
// prevent auto-focus on touch devices to avoid keyboard popup
|
||||
if (preventAutoFocusOnTouch && device.isTouchScreen) {
|
||||
if (preventAutoFocusOnTouch && editorInterface.isTouchScreen) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { useUIAppState } from "../../context/ui-appState";
|
||||
import { atom, useSetAtom } from "../../editor-jotai";
|
||||
import { useOutsideClick } from "../../hooks/useOutsideClick";
|
||||
import { useDevice, useExcalidrawSetAppState } from "../App";
|
||||
import { useEditorInterface, useExcalidrawSetAppState } from "../App";
|
||||
import { Island } from "../Island";
|
||||
|
||||
import { SidebarHeader } from "./SidebarHeader";
|
||||
@@ -96,7 +96,7 @@ export const SidebarInner = forwardRef(
|
||||
return islandRef.current!;
|
||||
});
|
||||
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
|
||||
const closeLibrary = useCallback(() => {
|
||||
const isDialogOpen = !!document.querySelector(".Dialog");
|
||||
@@ -117,11 +117,11 @@ export const SidebarInner = forwardRef(
|
||||
if ((event.target as Element).closest(".sidebar-trigger")) {
|
||||
return;
|
||||
}
|
||||
if (!docked || !device.editor.canFitSidebar) {
|
||||
if (!docked || !editorInterface.canFitSidebar) {
|
||||
closeLibrary();
|
||||
}
|
||||
},
|
||||
[closeLibrary, docked, device.editor.canFitSidebar],
|
||||
[closeLibrary, docked, editorInterface.canFitSidebar],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -129,7 +129,7 @@ export const SidebarInner = forwardRef(
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === KEYS.ESCAPE &&
|
||||
(!docked || !device.editor.canFitSidebar)
|
||||
(!docked || !editorInterface.canFitSidebar)
|
||||
) {
|
||||
closeLibrary();
|
||||
}
|
||||
@@ -138,7 +138,7 @@ export const SidebarInner = forwardRef(
|
||||
return () => {
|
||||
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||
};
|
||||
}, [closeLibrary, docked, device.editor.canFitSidebar]);
|
||||
}, [closeLibrary, docked, editorInterface.canFitSidebar]);
|
||||
|
||||
return (
|
||||
<Island
|
||||
|
||||
@@ -2,7 +2,7 @@ import clsx from "clsx";
|
||||
import { useContext } from "react";
|
||||
|
||||
import { t } from "../../i18n";
|
||||
import { useDevice } from "../App";
|
||||
import { useEditorInterface } from "../App";
|
||||
import { Button } from "../Button";
|
||||
import { Tooltip } from "../Tooltip";
|
||||
import { CloseIcon, PinIcon } from "../icons";
|
||||
@@ -16,11 +16,11 @@ export const SidebarHeader = ({
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
const props = useContext(SidebarPropsContext);
|
||||
|
||||
const renderDockButton = !!(
|
||||
device.editor.canFitSidebar && props.shouldRenderDockButton
|
||||
editorInterface.canFitSidebar && props.shouldRenderDockButton
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CURSOR_TYPE,
|
||||
isShallowEqual,
|
||||
sceneCoordsToViewportCoords,
|
||||
type EditorInterface,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type {
|
||||
@@ -20,7 +21,7 @@ import type {
|
||||
RenderableElementsMap,
|
||||
RenderInteractiveSceneCallback,
|
||||
} from "../../scene/types";
|
||||
import type { AppState, Device, InteractiveCanvasAppState } from "../../types";
|
||||
import type { AppState, InteractiveCanvasAppState } from "../../types";
|
||||
import type { DOMAttributes } from "react";
|
||||
|
||||
type InteractiveCanvasProps = {
|
||||
@@ -35,7 +36,7 @@ type InteractiveCanvasProps = {
|
||||
scale: number;
|
||||
appState: InteractiveCanvasAppState;
|
||||
renderScrollbars: boolean;
|
||||
device: Device;
|
||||
editorInterface: EditorInterface;
|
||||
renderInteractiveSceneCallback: (
|
||||
data: RenderInteractiveSceneCallback,
|
||||
) => void;
|
||||
@@ -146,7 +147,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||
selectionColor,
|
||||
renderScrollbars: props.renderScrollbars,
|
||||
},
|
||||
device: props.device,
|
||||
editorInterface: props.editorInterface,
|
||||
callback: props.renderInteractiveSceneCallback,
|
||||
},
|
||||
isRenderThrottlingEnabled(),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EVENT, KEYS } from "@excalidraw/common";
|
||||
|
||||
import { useOutsideClick } from "../../hooks/useOutsideClick";
|
||||
import { useStable } from "../../hooks/useStable";
|
||||
import { useDevice } from "../App";
|
||||
import { useEditorInterface } from "../App";
|
||||
import { Island } from "../Island";
|
||||
import Stack from "../Stack";
|
||||
|
||||
@@ -29,7 +29,7 @@ const MenuContent = ({
|
||||
style?: React.CSSProperties;
|
||||
placement?: "top" | "bottom";
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const callbacksRef = useStable({ onClickOutside });
|
||||
@@ -59,7 +59,7 @@ const MenuContent = ({
|
||||
}, [callbacksRef]);
|
||||
|
||||
const classNames = clsx(`dropdown-menu ${className}`, {
|
||||
"dropdown-menu--mobile": device.editor.isMobile,
|
||||
"dropdown-menu--mobile": editorInterface.formFactor === "phone",
|
||||
"dropdown-menu--placement-top": placement === "top",
|
||||
}).trim();
|
||||
|
||||
@@ -73,13 +73,8 @@ const MenuContent = ({
|
||||
>
|
||||
{/* the zIndex ensures this menu has higher stacking order,
|
||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||
{device.editor.isMobile ? (
|
||||
<Stack.Col
|
||||
className="dropdown-menu-container"
|
||||
style={{ ["--gap" as any]: 1.25 }}
|
||||
>
|
||||
{children}
|
||||
</Stack.Col>
|
||||
{editorInterface.formFactor === "phone" ? (
|
||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||
) : (
|
||||
<Island
|
||||
className="dropdown-menu-container"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useDevice } from "../App";
|
||||
import { useEditorInterface } from "../App";
|
||||
|
||||
import { Ellipsify } from "../Ellipsify";
|
||||
|
||||
@@ -15,14 +15,14 @@ const MenuItemContent = ({
|
||||
textStyle?: React.CSSProperties;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
return (
|
||||
<>
|
||||
{icon && <div className="dropdown-menu-item__icon">{icon}</div>}
|
||||
<div style={textStyle} className="dropdown-menu-item__text">
|
||||
<Ellipsify>{children}</Ellipsify>
|
||||
</div>
|
||||
{shortcut && !device.editor.isMobile && (
|
||||
{shortcut && editorInterface.formFactor !== "phone" && (
|
||||
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useDevice } from "../App";
|
||||
import { useEditorInterface } from "../App";
|
||||
import { RadioGroup } from "../RadioGroup";
|
||||
|
||||
type Props<T> = {
|
||||
@@ -22,7 +22,7 @@ const DropdownMenuItemContentRadio = <T,>({
|
||||
children,
|
||||
name,
|
||||
}: Props<T>) => {
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -37,7 +37,7 @@ const DropdownMenuItemContentRadio = <T,>({
|
||||
choices={choices}
|
||||
/>
|
||||
</div>
|
||||
{shortcut && !device.editor.isMobile && (
|
||||
{shortcut && editorInterface.formFactor !== "phone" && (
|
||||
<div className="dropdown-menu-item__shortcut dropdown-menu-item__shortcut--orphaned">
|
||||
{shortcut}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
import { useDevice } from "../App";
|
||||
import { useEditorInterface } from "../App";
|
||||
|
||||
const MenuTrigger = ({
|
||||
className = "",
|
||||
@@ -14,12 +14,12 @@ const MenuTrigger = ({
|
||||
onToggle: () => void;
|
||||
title?: string;
|
||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
const classNames = clsx(
|
||||
`dropdown-menu-button ${className}`,
|
||||
"zen-mode-transition",
|
||||
{
|
||||
"dropdown-menu-button--mobile": device.editor.isMobile,
|
||||
"dropdown-menu-button--mobile": editorInterface.formFactor === "phone",
|
||||
},
|
||||
).trim();
|
||||
return (
|
||||
|
||||
@@ -41,7 +41,7 @@ import { getTooltipDiv, updateTooltipPosition } from "../../components/Tooltip";
|
||||
|
||||
import { t } from "../../i18n";
|
||||
|
||||
import { useAppProps, useDevice, useExcalidrawAppState } from "../App";
|
||||
import { useAppProps, useEditorInterface, useExcalidrawAppState } from "../App";
|
||||
import { ToolButton } from "../ToolButton";
|
||||
import { FreedrawIcon, TrashIcon, elementLinkIcon } from "../icons";
|
||||
import { getSelectedElements } from "../../scene";
|
||||
@@ -88,7 +88,7 @@ export const Hyperlink = ({
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const appState = useExcalidrawAppState();
|
||||
const appProps = useAppProps();
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
|
||||
const linkVal = element.link || "";
|
||||
|
||||
@@ -189,11 +189,11 @@ export const Hyperlink = ({
|
||||
if (
|
||||
isEditing &&
|
||||
inputRef?.current &&
|
||||
!(device.viewport.isMobile || device.isTouchScreen)
|
||||
!(editorInterface.formFactor === "phone" || editorInterface.isTouchScreen)
|
||||
) {
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditing, device.viewport.isMobile, device.isTouchScreen]);
|
||||
}, [isEditing, editorInterface.formFactor, editorInterface.isTouchScreen]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: number | null = null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
import { isMobileOrTablet, MQ_MIN_WIDTH_DESKTOP } from "@excalidraw/common";
|
||||
import { MQ_MIN_WIDTH_DESKTOP, type EditorInterface } from "@excalidraw/common";
|
||||
|
||||
import { t } from "../../i18n";
|
||||
import { Button } from "../Button";
|
||||
@@ -12,15 +12,18 @@ import "./LiveCollaborationTrigger.scss";
|
||||
const LiveCollaborationTrigger = ({
|
||||
isCollaborating,
|
||||
onSelect,
|
||||
editorInterface,
|
||||
...rest
|
||||
}: {
|
||||
isCollaborating: boolean;
|
||||
onSelect: () => void;
|
||||
editorInterface?: EditorInterface;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
const appState = useUIAppState();
|
||||
|
||||
const showIconOnly =
|
||||
isMobileOrTablet() || appState.width < MQ_MIN_WIDTH_DESKTOP;
|
||||
editorInterface?.formFactor !== "desktop" ||
|
||||
appState.width < MQ_MIN_WIDTH_DESKTOP;
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -5,7 +5,7 @@ import { composeEventHandlers } from "@excalidraw/common";
|
||||
import { useTunnels } from "../../context/tunnels";
|
||||
import { useUIAppState } from "../../context/ui-appState";
|
||||
import { t } from "../../i18n";
|
||||
import { useDevice, useExcalidrawSetAppState } from "../App";
|
||||
import { useEditorInterface, useExcalidrawSetAppState } from "../App";
|
||||
import { UserList } from "../UserList";
|
||||
import DropdownMenu from "../dropdownMenu/DropdownMenu";
|
||||
import { withInternalFallback } from "../hoc/withInternalFallback";
|
||||
@@ -27,7 +27,7 @@ const MainMenu = Object.assign(
|
||||
onSelect?: (event: Event) => void;
|
||||
}) => {
|
||||
const { MainMenuTunnel } = useTunnels();
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
const appState = useUIAppState();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
|
||||
@@ -53,19 +53,24 @@ const MainMenu = Object.assign(
|
||||
setAppState({ openMenu: null });
|
||||
})}
|
||||
placement="bottom"
|
||||
className={device.editor.isMobile ? "main-menu-dropdown" : ""}
|
||||
className={
|
||||
editorInterface.formFactor === "phone"
|
||||
? "main-menu-dropdown"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{children}
|
||||
{device.editor.isMobile && appState.collaborators.size > 0 && (
|
||||
<fieldset className="UserList-Wrapper">
|
||||
<legend>{t("labels.collaborators")}</legend>
|
||||
<UserList
|
||||
mobile={true}
|
||||
collaborators={appState.collaborators}
|
||||
userToFollow={appState.userToFollow?.socketId || null}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{editorInterface.formFactor === "phone" &&
|
||||
appState.collaborators.size > 0 && (
|
||||
<fieldset className="UserList-Wrapper">
|
||||
<legend>{t("labels.collaborators")}</legend>
|
||||
<UserList
|
||||
mobile={true}
|
||||
collaborators={appState.collaborators}
|
||||
userToFollow={appState.userToFollow?.socketId || null}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</MainMenuTunnel.In>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||
import { useTunnels } from "../../context/tunnels";
|
||||
import { useUIAppState } from "../../context/ui-appState";
|
||||
import { t, useI18n } from "../../i18n";
|
||||
import { useDevice, useExcalidrawActionManager } from "../App";
|
||||
import { useEditorInterface, useExcalidrawActionManager } from "../App";
|
||||
import { ExcalidrawLogo } from "../ExcalidrawLogo";
|
||||
import { HelpIcon, LoadIcon, usersIcon } from "../icons";
|
||||
|
||||
@@ -18,12 +18,12 @@ const WelcomeScreenMenuItemContent = ({
|
||||
shortcut?: string | null;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
const editorInterface = useEditorInterface();
|
||||
return (
|
||||
<>
|
||||
<div className="welcome-screen-menu-item__icon">{icon}</div>
|
||||
<div className="welcome-screen-menu-item__text">{children}</div>
|
||||
{shortcut && !device.editor.isMobile && (
|
||||
{shortcut && editorInterface.formFactor !== "phone" && (
|
||||
<div className="welcome-screen-menu-item__shortcut">{shortcut}</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user