feat: use component dimensions to break to mobile (#3414)
Co-authored-by: Jed Fox <git@jedfox.com>
This commit is contained in:
parent
016e69b9f2
commit
09dfd16b17
@ -8,7 +8,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
|
|||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
|
@ -8,7 +8,7 @@ import { Tooltip } from "../components/Tooltip";
|
|||||||
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
|
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
|
||||||
import { loadFromJSON, saveAsJSON } from "../data";
|
import { loadFromJSON, saveAsJSON } from "../data";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { supported } from "browser-fs-access";
|
import { supported } from "browser-fs-access";
|
||||||
|
@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import {
|
import {
|
||||||
canChangeSharpness,
|
canChangeSharpness,
|
||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Point, simplify } from "points-on-curve";
|
import { Point, simplify } from "points-on-curve";
|
||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
@ -54,6 +54,9 @@ import {
|
|||||||
GRID_SIZE,
|
GRID_SIZE,
|
||||||
LINE_CONFIRM_THRESHOLD,
|
LINE_CONFIRM_THRESHOLD,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
|
MQ_MAX_HEIGHT_LANDSCAPE,
|
||||||
|
MQ_MAX_WIDTH_LANDSCAPE,
|
||||||
|
MQ_MAX_WIDTH_PORTRAIT,
|
||||||
POINTER_BUTTON,
|
POINTER_BUTTON,
|
||||||
SCROLL_TIMEOUT,
|
SCROLL_TIMEOUT,
|
||||||
TAP_TWICE_TIMEOUT,
|
TAP_TWICE_TIMEOUT,
|
||||||
@ -178,13 +181,15 @@ import {
|
|||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
withBatchedUpdates,
|
withBatchedUpdates,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { isMobile } from "../is-mobile";
|
|
||||||
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
|
||||||
import LayerUI from "./LayerUI";
|
import LayerUI from "./LayerUI";
|
||||||
import { Stats } from "./Stats";
|
import { Stats } from "./Stats";
|
||||||
import { Toast } from "./Toast";
|
import { Toast } from "./Toast";
|
||||||
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
|
||||||
|
|
||||||
|
export const IsMobileContext = React.createContext(false);
|
||||||
|
export const useIsMobile = () => useContext(IsMobileContext);
|
||||||
|
|
||||||
const { history } = createHistory();
|
const { history } = createHistory();
|
||||||
|
|
||||||
let didTapTwice: boolean = false;
|
let didTapTwice: boolean = false;
|
||||||
@ -286,6 +291,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
rc: RoughCanvas | null = null;
|
rc: RoughCanvas | null = null;
|
||||||
unmounted: boolean = false;
|
unmounted: boolean = false;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
|
isMobile = false;
|
||||||
|
detachIsMobileMqHandler?: () => void;
|
||||||
|
|
||||||
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
public static defaultProps: Partial<AppProps> = {
|
public static defaultProps: Partial<AppProps> = {
|
||||||
@ -437,60 +445,64 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
<div
|
<div
|
||||||
className={clsx("excalidraw", {
|
className={clsx("excalidraw", {
|
||||||
"excalidraw--view-mode": viewModeEnabled,
|
"excalidraw--view-mode": viewModeEnabled,
|
||||||
|
"excalidraw--mobile": this.isMobile,
|
||||||
})}
|
})}
|
||||||
ref={this.excalidrawContainerRef}
|
ref={this.excalidrawContainerRef}
|
||||||
onDrop={this.handleAppOnDrop}
|
onDrop={this.handleAppOnDrop}
|
||||||
>
|
>
|
||||||
<LayerUI
|
<IsMobileContext.Provider value={this.isMobile}>
|
||||||
canvas={this.canvas}
|
<LayerUI
|
||||||
appState={this.state}
|
canvas={this.canvas}
|
||||||
setAppState={this.setAppState}
|
|
||||||
actionManager={this.actionManager}
|
|
||||||
elements={this.scene.getElements()}
|
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
|
||||||
onLockToggle={this.toggleLock}
|
|
||||||
onInsertElements={(elements) =>
|
|
||||||
this.addElementsFromPasteOrLibrary(
|
|
||||||
elements,
|
|
||||||
DEFAULT_PASTE_X,
|
|
||||||
DEFAULT_PASTE_Y,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
zenModeEnabled={zenModeEnabled}
|
|
||||||
toggleZenMode={this.toggleZenMode}
|
|
||||||
langCode={getLanguage().code}
|
|
||||||
isCollaborating={this.props.isCollaborating || false}
|
|
||||||
onExportToBackend={onExportToBackend}
|
|
||||||
renderCustomFooter={renderFooter}
|
|
||||||
viewModeEnabled={viewModeEnabled}
|
|
||||||
showExitZenModeBtn={
|
|
||||||
typeof this.props?.zenModeEnabled === "undefined" && zenModeEnabled
|
|
||||||
}
|
|
||||||
showThemeBtn={
|
|
||||||
typeof this.props?.theme === "undefined" &&
|
|
||||||
this.props.UIOptions.canvasActions.theme
|
|
||||||
}
|
|
||||||
libraryReturnUrl={this.props.libraryReturnUrl}
|
|
||||||
UIOptions={this.props.UIOptions}
|
|
||||||
/>
|
|
||||||
<div className="excalidraw-textEditorContainer" />
|
|
||||||
<div className="excalidraw-contextMenuContainer" />
|
|
||||||
{this.state.showStats && (
|
|
||||||
<Stats
|
|
||||||
appState={this.state}
|
appState={this.state}
|
||||||
setAppState={this.setAppState}
|
setAppState={this.setAppState}
|
||||||
|
actionManager={this.actionManager}
|
||||||
elements={this.scene.getElements()}
|
elements={this.scene.getElements()}
|
||||||
onClose={this.toggleStats}
|
onCollabButtonClick={onCollabButtonClick}
|
||||||
renderCustomStats={renderCustomStats}
|
onLockToggle={this.toggleLock}
|
||||||
|
onInsertElements={(elements) =>
|
||||||
|
this.addElementsFromPasteOrLibrary(
|
||||||
|
elements,
|
||||||
|
DEFAULT_PASTE_X,
|
||||||
|
DEFAULT_PASTE_Y,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
zenModeEnabled={zenModeEnabled}
|
||||||
|
toggleZenMode={this.toggleZenMode}
|
||||||
|
langCode={getLanguage().code}
|
||||||
|
isCollaborating={this.props.isCollaborating || false}
|
||||||
|
onExportToBackend={onExportToBackend}
|
||||||
|
renderCustomFooter={renderFooter}
|
||||||
|
viewModeEnabled={viewModeEnabled}
|
||||||
|
showExitZenModeBtn={
|
||||||
|
typeof this.props?.zenModeEnabled === "undefined" &&
|
||||||
|
zenModeEnabled
|
||||||
|
}
|
||||||
|
showThemeBtn={
|
||||||
|
typeof this.props?.theme === "undefined" &&
|
||||||
|
this.props.UIOptions.canvasActions.theme
|
||||||
|
}
|
||||||
|
libraryReturnUrl={this.props.libraryReturnUrl}
|
||||||
|
UIOptions={this.props.UIOptions}
|
||||||
/>
|
/>
|
||||||
)}
|
<div className="excalidraw-textEditorContainer" />
|
||||||
{this.state.toastMessage !== null && (
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
<Toast
|
{this.state.showStats && (
|
||||||
message={this.state.toastMessage}
|
<Stats
|
||||||
clearToast={this.clearToast}
|
appState={this.state}
|
||||||
/>
|
setAppState={this.setAppState}
|
||||||
)}
|
elements={this.scene.getElements()}
|
||||||
<main>{this.renderCanvas()}</main>
|
onClose={this.toggleStats}
|
||||||
|
renderCustomStats={renderCustomStats}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{this.state.toastMessage !== null && (
|
||||||
|
<Toast
|
||||||
|
message={this.state.toastMessage}
|
||||||
|
clearToast={this.clearToast}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<main>{this.renderCanvas()}</main>
|
||||||
|
</IsMobileContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -776,10 +788,29 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
// compute isMobile state
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
} = this.excalidrawContainerRef.current!.getBoundingClientRect();
|
||||||
|
this.isMobile =
|
||||||
|
width < MQ_MAX_WIDTH_PORTRAIT ||
|
||||||
|
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE);
|
||||||
|
// refresh offsets
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
this.updateDOMRect();
|
this.updateDOMRect();
|
||||||
});
|
});
|
||||||
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
|
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
|
||||||
|
} else if (window.matchMedia) {
|
||||||
|
const mediaQuery = 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 handler = () => (this.isMobile = mediaQuery.matches);
|
||||||
|
mediaQuery.addListener(handler);
|
||||||
|
this.detachIsMobileMqHandler = () => mediaQuery.removeListener(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
||||||
|
|
||||||
if (searchParams.has("web-share-target")) {
|
if (searchParams.has("web-share-target")) {
|
||||||
@ -839,6 +870,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.onGestureEnd as any,
|
this.onGestureEnd as any,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.detachIsMobileMqHandler?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
private addEventListeners() {
|
private addEventListeners() {
|
||||||
@ -1016,7 +1049,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
renderOptimizations: true,
|
renderOptimizations: true,
|
||||||
renderScrollbars: !isMobile(),
|
renderScrollbars: !this.isMobile,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (scrollBars) {
|
if (scrollBars) {
|
||||||
@ -3811,8 +3844,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const separator = "separator";
|
const separator = "separator";
|
||||||
|
|
||||||
const _isMobile = isMobile();
|
|
||||||
|
|
||||||
const elements = this.scene.getElements();
|
const elements = this.scene.getElements();
|
||||||
|
|
||||||
const options: ContextMenuOption[] = [];
|
const options: ContextMenuOption[] = [];
|
||||||
@ -3849,7 +3880,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: [
|
options: [
|
||||||
_isMobile &&
|
this.isMobile &&
|
||||||
navigator.clipboard && {
|
navigator.clipboard && {
|
||||||
name: "paste",
|
name: "paste",
|
||||||
perform: (elements, appStates) => {
|
perform: (elements, appStates) => {
|
||||||
@ -3860,7 +3891,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
contextItemLabel: "labels.paste",
|
contextItemLabel: "labels.paste",
|
||||||
},
|
},
|
||||||
_isMobile && navigator.clipboard && separator,
|
this.isMobile && navigator.clipboard && separator,
|
||||||
probablySupportsClipboardBlob &&
|
probablySupportsClipboardBlob &&
|
||||||
elements.length > 0 &&
|
elements.length > 0 &&
|
||||||
actionCopyAsPng,
|
actionCopyAsPng,
|
||||||
@ -3903,9 +3934,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
ContextMenu.push({
|
ContextMenu.push({
|
||||||
options: [
|
options: [
|
||||||
_isMobile && actionCut,
|
this.isMobile && actionCut,
|
||||||
_isMobile && navigator.clipboard && actionCopy,
|
this.isMobile && navigator.clipboard && actionCopy,
|
||||||
_isMobile &&
|
this.isMobile &&
|
||||||
navigator.clipboard && {
|
navigator.clipboard && {
|
||||||
name: "paste",
|
name: "paste",
|
||||||
perform: (elements, appStates) => {
|
perform: (elements, appStates) => {
|
||||||
@ -3916,7 +3947,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
contextItemLabel: "labels.paste",
|
contextItemLabel: "labels.paste",
|
||||||
},
|
},
|
||||||
_isMobile && separator,
|
this.isMobile && separator,
|
||||||
...options,
|
...options,
|
||||||
separator,
|
separator,
|
||||||
actionCopyStyles,
|
actionCopyStyles,
|
||||||
|
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { users } from "./icons";
|
import { users } from "./icons";
|
||||||
|
|
||||||
import "./CollabButton.scss";
|
import "./CollabButton.scss";
|
||||||
|
@ -218,7 +218,7 @@
|
|||||||
left: 2px;
|
left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
.context-menu-option {
|
.context-menu-option {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
padding: 0 16px 16px;
|
padding: 0 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
.Dialog {
|
.Dialog {
|
||||||
--metric: calc(var(--space-factor) * 4);
|
--metric: calc(var(--space-factor) * 4);
|
||||||
--inset-left: #{"max(var(--metric), var(--sal))"};
|
--inset-left: #{"max(var(--metric), var(--sal))"};
|
||||||
|
@ -2,7 +2,7 @@ import clsx from "clsx";
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import "./Dialog.scss";
|
import "./Dialog.scss";
|
||||||
import { back, close } from "./icons";
|
import { back, close } from "./icons";
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
.ExportDialog {
|
.ExportDialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -6,7 +6,7 @@ import { canvasToBlob } from "../data/blob";
|
|||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { CanvasError } from "../errors";
|
import { CanvasError } from "../errors";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { exportToCanvas, getExportSize } from "../scene/export";
|
import { exportToCanvas, getExportSize } from "../scene/export";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
@ -19,7 +19,7 @@ $wide-viewport-width: 1000px;
|
|||||||
color: $oc-gray-6;
|
color: $oc-gray-6;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
position: static;
|
position: static;
|
||||||
padding-right: 2em;
|
padding-right: 2em;
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
:root[dir="rtl"] & {
|
:root[dir="rtl"] & {
|
||||||
left: 2px;
|
left: 2px;
|
||||||
}
|
}
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import { Library } from "../data/library";
|
|||||||
import { isTextElement, showSelectedShapeActions } from "../element";
|
import { isTextElement, showSelectedShapeActions } from "../element";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { Language, t } from "../i18n";
|
import { Language, t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||||
import { ExportType } from "../scene/types";
|
import { ExportType } from "../scene/types";
|
||||||
import {
|
import {
|
||||||
|
@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
|
|||||||
import { close } from "../components/icons";
|
import { close } from "../components/icons";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { MIME_TYPES } from "../constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { exportToSvg } from "../scene/export";
|
import { exportToSvg } from "../scene/export";
|
||||||
import { LibraryItem } from "../types";
|
import { LibraryItem } from "../types";
|
||||||
import "./LibraryUnit.scss";
|
import "./LibraryUnit.scss";
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@ -82,7 +82,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
.Modal {
|
.Modal {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import "./Modal.scss";
|
import "./Modal.scss";
|
||||||
|
|
||||||
import React, { useState, useLayoutEffect } from "react";
|
import React, { useState, useLayoutEffect, useRef } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
import { useIsMobile } from "../components/App";
|
||||||
|
|
||||||
export const Modal = (props: {
|
export const Modal = (props: {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -48,6 +49,16 @@ export const Modal = (props: {
|
|||||||
const useBodyRoot = () => {
|
const useBodyRoot = () => {
|
||||||
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
const [div, setDiv] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const isMobileRef = useRef(isMobile);
|
||||||
|
isMobileRef.current = isMobile;
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (div) {
|
||||||
|
div.classList.toggle("excalidraw--mobile", isMobile);
|
||||||
|
}
|
||||||
|
}, [div, isMobile]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const isDarkTheme = !!document
|
const isDarkTheme = !!document
|
||||||
.querySelector(".excalidraw")
|
.querySelector(".excalidraw")
|
||||||
@ -55,6 +66,7 @@ const useBodyRoot = () => {
|
|||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
|
|
||||||
div.classList.add("excalidraw", "excalidraw-modal-container");
|
div.classList.add("excalidraw", "excalidraw-modal-container");
|
||||||
|
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
|
||||||
|
|
||||||
if (isDarkTheme) {
|
if (isDarkTheme) {
|
||||||
div.classList.add("theme--dark");
|
div.classList.add("theme--dark");
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.PasteChartDialog {
|
.PasteChartDialog {
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
.Island {
|
.Island {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -13,7 +13,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import { getCommonBounds } from "../element/bounds";
|
import { getCommonBounds } from "../element/bounds";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useIsMobile } from "../is-mobile";
|
import { useIsMobile } from "../components/App";
|
||||||
import { getTargetElements } from "../scene";
|
import { getTargetElements } from "../scene";
|
||||||
import { AppState, ExcalidrawProps } from "../types";
|
import { AppState, ExcalidrawProps } from "../types";
|
||||||
import { close } from "./icons";
|
import { close } from "./icons";
|
||||||
|
@ -193,7 +193,7 @@
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,3 +137,7 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
|||||||
theme: true,
|
theme: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
||||||
|
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
||||||
|
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
|
||||||
|
@ -480,7 +480,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
aside {
|
aside {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
@import "open-color/open-color.scss";
|
@import "open-color/open-color.scss";
|
||||||
|
|
||||||
// keep up to date with is-mobile.tsx
|
@mixin isMobile() {
|
||||||
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
|
@at-root .excalidraw--mobile#{&} {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
||||||
|
|
||||||
:export {
|
:export {
|
||||||
isMobileQuery: unquote($is-mobile-query);
|
|
||||||
themeFilter: unquote($theme-filter);
|
themeFilter: unquote($theme-filter);
|
||||||
}
|
}
|
||||||
|
@ -32,13 +32,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
.RoomDialog-usernameLabel {
|
.RoomDialog-usernameLabel {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
margin-inline-start: 1em;
|
margin-inline-start: 1em;
|
||||||
@media #{$is-mobile-query} {
|
@include isMobile {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
margin-inline-start: 0;
|
margin-inline-start: 0;
|
||||||
}
|
}
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
import React, { useState, useEffect, useRef, useContext } from "react";
|
|
||||||
import variables from "./css/variables.module.scss";
|
|
||||||
|
|
||||||
const context = React.createContext(false);
|
|
||||||
|
|
||||||
const getIsMobileMatcher = () => {
|
|
||||||
return window.matchMedia
|
|
||||||
? window.matchMedia(variables.isMobileQuery)
|
|
||||||
: (({
|
|
||||||
matches: false,
|
|
||||||
addListener: () => {},
|
|
||||||
removeListener: () => {},
|
|
||||||
} as any) as MediaQueryList);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IsMobileProvider = ({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) => {
|
|
||||||
const query = useRef<MediaQueryList>();
|
|
||||||
if (!query.current) {
|
|
||||||
query.current = getIsMobileMatcher();
|
|
||||||
}
|
|
||||||
const [isMobile, setMobile] = useState(query.current.matches);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = () => setMobile(query.current!.matches);
|
|
||||||
query.current!.addListener(handler);
|
|
||||||
return () => query.current!.removeListener(handler);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <context.Provider value={isMobile}>{children}</context.Provider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isMobile = () => getIsMobileMatcher().matches;
|
|
||||||
export const useIsMobile = () => useContext(context);
|
|
@ -11,6 +11,14 @@ The change should be grouped under one of the below section and must contain PR
|
|||||||
Please add the latest change on the top under the correct section.
|
Please add the latest change on the top under the correct section.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## Excalidraw Library
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- App now breaks into mobile view using the component dimensions, not viewport dimensions. This fixes a case where the app would break sooner than necessary when the component's size is smaller than viewport [#3414](https://github.com/excalidraw/excalidraw/pull/3414).
|
||||||
|
|
||||||
## 0.6.0 (2021-04-04)
|
## 0.6.0 (2021-04-04)
|
||||||
|
|
||||||
## Excalidraw API
|
## Excalidraw API
|
||||||
|
@ -8,7 +8,6 @@ import "../../css/app.scss";
|
|||||||
import "../../css/styles.scss";
|
import "../../css/styles.scss";
|
||||||
|
|
||||||
import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
||||||
import { IsMobileProvider } from "../../is-mobile";
|
|
||||||
import { defaultLang } from "../../i18n";
|
import { defaultLang } from "../../i18n";
|
||||||
import { DEFAULT_UI_OPTIONS } from "../../constants";
|
import { DEFAULT_UI_OPTIONS } from "../../constants";
|
||||||
|
|
||||||
@ -61,27 +60,25 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<InitializeApp langCode={langCode}>
|
<InitializeApp langCode={langCode}>
|
||||||
<IsMobileProvider>
|
<App
|
||||||
<App
|
onChange={onChange}
|
||||||
onChange={onChange}
|
initialData={initialData}
|
||||||
initialData={initialData}
|
excalidrawRef={excalidrawRef}
|
||||||
excalidrawRef={excalidrawRef}
|
onCollabButtonClick={onCollabButtonClick}
|
||||||
onCollabButtonClick={onCollabButtonClick}
|
isCollaborating={isCollaborating}
|
||||||
isCollaborating={isCollaborating}
|
onPointerUpdate={onPointerUpdate}
|
||||||
onPointerUpdate={onPointerUpdate}
|
onExportToBackend={onExportToBackend}
|
||||||
onExportToBackend={onExportToBackend}
|
renderFooter={renderFooter}
|
||||||
renderFooter={renderFooter}
|
langCode={langCode}
|
||||||
langCode={langCode}
|
viewModeEnabled={viewModeEnabled}
|
||||||
viewModeEnabled={viewModeEnabled}
|
zenModeEnabled={zenModeEnabled}
|
||||||
zenModeEnabled={zenModeEnabled}
|
gridModeEnabled={gridModeEnabled}
|
||||||
gridModeEnabled={gridModeEnabled}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
theme={theme}
|
||||||
theme={theme}
|
name={name}
|
||||||
name={name}
|
renderCustomStats={renderCustomStats}
|
||||||
renderCustomStats={renderCustomStats}
|
UIOptions={UIOptions}
|
||||||
UIOptions={UIOptions}
|
/>
|
||||||
/>
|
|
||||||
</IsMobileProvider>
|
|
||||||
</InitializeApp>
|
</InitializeApp>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user