Compare commits

..

2 Commits

Author SHA1 Message Date
Panayiotis Lipiridis 7988cdfb60 Merge branch 'master' of github.com:excalidraw/excalidraw into gcp-portal
* 'master' of github.com:excalidraw/excalidraw: (194 commits)
  fix: Make help toggle tabbable (#3310)
  chore: Update translations from Crowdin (#3270)
  chore(deps): bump @types/jest from 26.0.20 to 26.0.21 (#3298)
  chore(deps): bump @types/react-dom from 17.0.1 to 17.0.2 (#3296)
  chore(deps): bump @types/react from 17.0.2 to 17.0.3 (#3297)
  fix: Show Windows share icon for Windows users (#3306)
  fix: Update browser-fs-access to use new supported export (#3303)
  feat: replaces fontSize and fontFamily text with icons (#2857)
  fix: use random IV for link-sharing encryption (#2829) (#2833)
  fix: Don't scroll to content on INIT websocket message (#3291)
  docs: Release @excalidraw/excalidraw@0.5.0 🎉  (#3289)
  feat: set window.name in excalidraw app & also support target for excalidraw libraries (#3299)
  chore(deps-dev): bump css-loader in /src/packages/utils (#3292)
  chore(deps-dev): bump css-loader in /src/packages/excalidraw (#3293)
  chore(deps-dev): bump webpack in /src/packages/utils (#3294)
  chore(deps-dev): bump webpack in /src/packages/excalidraw (#3295)
  feat: support pasting file contents & always prefer system clip (#3257)
  fix: Don't show export and delete when library is empty (#3288)
  feat: Add label for name field and use input when editable in export dialog (#3286)
  fix: overflow in textinput in export dialog (#3284)
  ...
2021-03-23 13:58:07 +02:00
Panayiotis Lipiridis 1f7c75de63 feat: GCP Portal 2021-02-08 00:17:36 +02:00
142 changed files with 6132 additions and 9901 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
REACT_APP_BACKEND_V1_GET_URL=https://json.excalidraw.com/api/v1/
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
REACT_APP_SOCKET_SERVER_URL=https://portal.excalidraw.com
REACT_APP_SOCKET_SERVER_URL=https://excalidraw-portal.uc.r.appspot.com
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
-1
View File
@@ -20,4 +20,3 @@ package-lock.json
static
yarn-debug.log*
yarn-error.log*
src/packages/excalidraw/types
+14 -14
View File
@@ -19,20 +19,20 @@
]
},
"dependencies": {
"@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.11.10",
"@testing-library/react": "11.2.6",
"@types/jest": "26.0.22",
"@sentry/browser": "6.2.2",
"@sentry/integrations": "6.2.1",
"@testing-library/jest-dom": "5.11.9",
"@testing-library/react": "11.2.5",
"@types/jest": "26.0.21",
"@types/react": "17.0.3",
"@types/react-dom": "17.0.3",
"@types/react-dom": "17.0.2",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.16.4",
"browser-fs-access": "0.15.3",
"clsx": "1.1.1",
"firebase": "8.3.2",
"i18next-browser-languagedetector": "6.1.0",
"firebase": "8.2.10",
"i18next-browser-languagedetector": "6.0.1",
"lodash.throttle": "4.1.1",
"nanoid": "3.1.22",
"nanoid": "3.1.21",
"open-color": "1.8.0",
"pako": "1.0.11",
"png-chunk-text": "1.0.0",
@@ -40,13 +40,13 @@
"png-chunks-extract": "1.0.0",
"points-on-curve": "0.2.0",
"pwacompat": "2.0.17",
"react": "17.0.2",
"react-dom": "17.0.2",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-scripts": "4.0.3",
"roughjs": "4.3.1",
"sass": "1.32.8",
"socket.io-client": "2.3.1",
"typescript": "4.2.4"
"typescript": "4.2.3"
},
"devDependencies": {
"@excalidraw/eslint-config": "1.0.0",
@@ -56,7 +56,7 @@
"@types/resize-observer-browser": "0.1.5",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-prettier": "3.3.1",
"firebase-tools": "9.9.0",
"firebase-tools": "9.6.1",
"husky": "4.3.8",
"jest-canvas-mock": "2.3.1",
"lint-staged": "10.5.4",
+2 -4
View File
@@ -51,7 +51,8 @@
name="twitter:description"
content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
/>
<!-- OG tags require absolute url for images -->
<meta name="twitter:image" content="https://excalidraw.com/og-image.png" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<!-- Excalidraw version -->
@@ -147,9 +148,6 @@
color: var(--popup-text-color);
font-size: 1.3em;
}
#root {
height: 100%;
}
</style>
</head>
+1 -33
View File
@@ -39,37 +39,5 @@
}
]
}
},
"screenshots": [
{
"src": "/screenshots/virtual-whiteboard.png",
"type": "image/png",
"sizes": "462x945"
},
{
"src": "/screenshots/wireframe.png",
"type": "image/png",
"sizes": "462x945"
},
{
"src": "/screenshots/illustration.png",
"type": "image/png",
"sizes": "462x945"
},
{
"src": "/screenshots/shapes.png",
"type": "image/png",
"sizes": "462x945"
},
{
"src": "/screenshots/collaboration.png",
"type": "image/png",
"sizes": "462x945"
},
{
"src": "/screenshots/export.png",
"type": "image/png",
"sizes": "462x945"
}
]
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

+1 -3
View File
@@ -8,7 +8,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
import { newElementWith } from "../element/mutateElement";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import useIsMobile from "../is-mobile";
import { CODES, KEYS } from "../keys";
import { getNormalizedZoom, getSelectedElements } from "../scene";
import { centerScrollOn } from "../scene/scroll";
@@ -33,7 +33,6 @@ export const actionChangeViewBackgroundColor = register({
type="canvasBackground"
color={appState.viewBackgroundColor}
onChange={(color) => updateData(color)}
data-testid="canvas-background-picker"
/>
</div>
);
@@ -73,7 +72,6 @@ export const actionClearCanvas = register({
updateData(null);
}
}}
data-testid="clear-canvas-button"
/>
),
});
+1 -4
View File
@@ -8,7 +8,7 @@ import { Tooltip } from "../components/Tooltip";
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
import { loadFromJSON, saveAsJSON } from "../data";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import useIsMobile from "../is-mobile";
import { KEYS } from "../keys";
import { register } from "./register";
import { supported } from "browser-fs-access";
@@ -136,7 +136,6 @@ export const actionSaveScene = register({
aria-label={t("buttons.save")}
showAriaLabel={useIsMobile()}
onClick={() => updateData(null)}
data-testid="save-button"
/>
),
});
@@ -168,7 +167,6 @@ export const actionSaveAsScene = register({
showAriaLabel={useIsMobile()}
hidden={!supported}
onClick={() => updateData(null)}
data-testid="save-as-button"
/>
),
});
@@ -206,7 +204,6 @@ export const actionLoadScene = register({
aria-label={t("buttons.load")}
showAriaLabel={useIsMobile()}
onClick={updateData}
data-testid="load-button"
/>
),
});
-207
View File
@@ -1,207 +0,0 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getElementMap, getNonDeletedElements } from "../element";
import { mutateElement } from "../element/mutateElement";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
import { AppState } from "../types";
import { getTransformHandles } from "../element/transformHandles";
import { isLinearElement } from "../element/typeChecks";
import { updateBoundElements } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
const enableActionFlipHorizontal = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const eligibleElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return eligibleElements.length === 1 && eligibleElements[0].type !== "text";
};
const enableActionFlipVertical = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const eligibleElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return eligibleElements.length === 1;
};
export const actionFlipHorizontal = register({
name: "flipHorizontal",
perform: (elements, appState) => {
return {
elements: flipSelectedElements(elements, appState, "horizontal"),
appState,
commitToHistory: true,
};
},
keyTest: (event) => event.shiftKey && event.code === "KeyH",
contextItemLabel: "labels.flipHorizontal",
contextItemPredicate: (elements, appState) =>
enableActionFlipHorizontal(elements, appState),
});
export const actionFlipVertical = register({
name: "flipVertical",
perform: (elements, appState) => {
return {
elements: flipSelectedElements(elements, appState, "vertical"),
appState,
commitToHistory: true,
};
},
keyTest: (event) => event.shiftKey && event.code === "KeyV",
contextItemLabel: "labels.flipVertical",
contextItemPredicate: (elements, appState) =>
enableActionFlipVertical(elements, appState),
});
const flipSelectedElements = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
flipDirection: "horizontal" | "vertical",
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
// remove once we allow for groups of elements to be flipped
if (selectedElements.length > 1) {
return elements;
}
const updatedElements = flipElements(
selectedElements,
appState,
flipDirection,
);
const updatedElementsMap = getElementMap(updatedElements);
return elements.map((element) => updatedElementsMap[element.id] || element);
};
const flipElements = (
elements: NonDeleted<ExcalidrawElement>[],
appState: AppState,
flipDirection: "horizontal" | "vertical",
): ExcalidrawElement[] => {
for (let i = 0; i < elements.length; i++) {
flipElement(elements[i], appState);
// If vertical flip, rotate an extra 180
if (flipDirection === "vertical") {
rotateElement(elements[i], Math.PI);
}
}
return elements;
};
const flipElement = (
element: NonDeleted<ExcalidrawElement>,
appState: AppState,
) => {
const originalX = element.x;
const originalY = element.y;
const width = element.width;
const height = element.height;
const originalAngle = normalizeAngle(element.angle);
let finalOffsetX = 0;
if (isLinearElement(element)) {
finalOffsetX =
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
element.width;
}
// Rotate back to zero, if necessary
mutateElement(element, {
angle: normalizeAngle(0),
});
// Flip unrotated by pulling TransformHandle to opposite side
const transformHandles = getTransformHandles(element, appState.zoom);
let usingNWHandle = true;
let newNCoordsX = 0;
let nHandle = transformHandles.nw;
if (!nHandle) {
// Use ne handle instead
usingNWHandle = false;
nHandle = transformHandles.ne;
if (!nHandle) {
mutateElement(element, {
angle: originalAngle,
});
return;
}
}
if (isLinearElement(element)) {
for (let i = 1; i < element.points.length; i++) {
LinearElementEditor.movePoint(element, i, [
-element.points[i][0],
element.points[i][1],
]);
}
LinearElementEditor.normalizePoints(element);
} else {
// calculate new x-coord for transformation
newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
resizeSingleElement(
element,
true,
element,
usingNWHandle ? "nw" : "ne",
false,
newNCoordsX,
nHandle[1],
);
// fix the size to account for handle sizes
mutateElement(element, {
width,
height,
});
}
// Rotate by (360 degrees - original angle)
let angle = normalizeAngle(2 * Math.PI - originalAngle);
if (angle < 0) {
// check, probably unnecessary
angle = normalizeAngle(angle + 2 * Math.PI);
}
mutateElement(element, {
angle,
});
// Move back to original spot to appear "flipped in place"
mutateElement(element, {
x: originalX + finalOffsetX,
y: originalY,
});
updateBoundElements(element);
};
const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => {
const originalX = element.x;
const originalY = element.y;
let angle = normalizeAngle(element.angle + rotationAngle);
if (angle < 0) {
// check, probably unnecessary
angle = normalizeAngle(2 * Math.PI + angle);
}
mutateElement(element, {
angle,
});
// Move back to original spot
mutateElement(element, {
x: originalX,
y: originalY,
});
};
-2
View File
@@ -66,8 +66,6 @@ export {
distributeVertically,
} from "./actionDistribute";
export { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
export {
actionCopy,
actionCut,
+3 -15
View File
@@ -7,12 +7,12 @@ import {
ActionResult,
} from "./types";
import { ExcalidrawElement } from "../element/types";
import { AppProps, AppState } from "../types";
import { AppState, ExcalidrawProps } from "../types";
import { MODES } from "../constants";
// This is the <App> component, but for now we don't care about anything but its
// `canvas` state.
type App = { canvas: HTMLCanvasElement | null; props: AppProps };
type App = { canvas: HTMLCanvasElement | null; props: ExcalidrawProps };
export class ActionManager implements ActionsManagerInterface {
actions = {} as ActionsManagerInterface["actions"];
@@ -52,14 +52,10 @@ export class ActionManager implements ActionsManagerInterface {
}
handleKeyDown(event: KeyboardEvent) {
const canvasActions = this.app.props.UIOptions.canvasActions;
const data = Object.values(this.actions)
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
.filter(
(action) =>
(action.name in canvasActions
? canvasActions[action.name as keyof typeof canvasActions]
: true) &&
action.keyTest &&
action.keyTest(
event,
@@ -106,15 +102,7 @@ export class ActionManager implements ActionsManagerInterface {
// like the user list. We can use this key to extract more
// data from app state. This is an alternative to generic prop hell!
renderAction = (name: ActionName, id?: string) => {
const canvasActions = this.app.props.UIOptions.canvasActions;
if (
this.actions[name] &&
"PanelComponent" in this.actions[name] &&
(name in canvasActions
? canvasActions[name as keyof typeof canvasActions]
: true)
) {
if (this.actions[name] && "PanelComponent" in this.actions[name]) {
const action = this.actions[name];
const PanelComponent = action.PanelComponent!;
const updateData = (formState?: any) => {
+1 -5
View File
@@ -23,9 +23,7 @@ export type ShortcutName =
| "zenMode"
| "stats"
| "addToLibrary"
| "viewMode"
| "flipHorizontal"
| "flipVertical";
| "viewMode";
const shortcutMap: Record<ShortcutName, string[]> = {
cut: [getShortcutKey("CtrlOrCmd+X")],
@@ -59,8 +57,6 @@ const shortcutMap: Record<ShortcutName, string[]> = {
zenMode: [getShortcutKey("Alt+Z")],
stats: [],
addToLibrary: [],
flipHorizontal: [getShortcutKey("Shift+H")],
flipVertical: [getShortcutKey("Shift+V")],
viewMode: [getShortcutKey("Alt+R")],
};
+1 -6
View File
@@ -6,10 +6,7 @@ import { AppState, ExcalidrawProps } from "../types";
export type ActionResult =
| {
elements?: readonly ExcalidrawElement[] | null;
appState?: MarkOptional<
AppState,
"offsetTop" | "offsetLeft" | "width" | "height"
> | null;
appState?: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null;
commitToHistory: boolean;
syncHistory?: boolean;
}
@@ -88,8 +85,6 @@ export type ActionName =
| "alignHorizontallyCentered"
| "distributeHorizontally"
| "distributeVertically"
| "flipHorizontal"
| "flipVertical"
| "viewMode"
| "exportWithDarkMode";
+3 -1
View File
@@ -10,7 +10,7 @@ import { getDateTime } from "./utils";
export const getDefaultAppState = (): Omit<
AppState,
"offsetTop" | "offsetLeft" | "width" | "height"
"offsetTop" | "offsetLeft"
> => {
return {
theme: "light",
@@ -43,6 +43,7 @@ export const getDefaultAppState = (): Omit<
exportWithDarkMode: false,
fileHandle: null,
gridSize: null,
height: window.innerHeight,
isBindingEnabled: true,
isLibraryOpen: false,
isLoading: false,
@@ -69,6 +70,7 @@ export const getDefaultAppState = (): Omit<
suggestedBindings: [],
toastMessage: null,
viewBackgroundColor: oc.white,
width: window.innerWidth,
zenModeEnabled: false,
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
viewModeEnabled: false,
+6 -8
View File
@@ -14,13 +14,6 @@ type ElementsClipboard = {
elements: ExcalidrawElement[];
};
export interface ClipboardData {
spreadsheet?: Spreadsheet;
elements?: readonly ExcalidrawElement[];
text?: string;
errorMessage?: string;
}
let CLIPBOARD = "";
let PREFER_APP_CLIPBOARD = false;
@@ -117,7 +110,12 @@ const getSystemClipboard = async (
*/
export const parseClipboard = async (
event: ClipboardEvent | null,
): Promise<ClipboardData> => {
): Promise<{
spreadsheet?: Spreadsheet;
elements?: readonly ExcalidrawElement[];
text?: string;
errorMessage?: string;
}> => {
const systemClipboard = await getSystemClipboard(event);
// if system clipboard empty, couldn't be resolved, or contains previously
+1 -1
View File
@@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import useIsMobile from "../is-mobile";
import {
canChangeSharpness,
canHaveArrowheads,
+174 -316
View File
@@ -1,5 +1,5 @@
import { Point, simplify } from "points-on-curve";
import React, { useContext } from "react";
import React from "react";
import { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough";
import clsx from "clsx";
@@ -17,8 +17,6 @@ import {
actionDeleteSelected,
actionDuplicateSelection,
actionFinalize,
actionFlipHorizontal,
actionFlipVertical,
actionGroup,
actionPasteStyles,
actionSelectAll,
@@ -44,9 +42,7 @@ import {
import {
APP_NAME,
CURSOR_TYPE,
DEFAULT_UI_OPTIONS,
DEFAULT_VERTICAL_ALIGN,
DETECT_POSITION_CHANGE_INTERVAL,
DRAGGING_THRESHOLD,
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
ELEMENT_TRANSLATE_AMOUNT,
@@ -55,16 +51,11 @@ import {
GRID_SIZE,
LINE_CONFIRM_THRESHOLD,
MIME_TYPES,
MQ_MAX_HEIGHT_LANDSCAPE,
MQ_MAX_WIDTH_LANDSCAPE,
MQ_MAX_WIDTH_PORTRAIT,
POINTER_BUTTON,
SCROLL_TIMEOUT,
TAP_TWICE_TIMEOUT,
TEXT_TO_CENTER_SNAP_THRESHOLD,
TOUCH_CTX_MENU_TIMEOUT,
URL_HASH_KEYS,
URL_QUERY_KEYS,
ZOOM_STEP,
} from "../constants";
import { loadFromBlob } from "../data";
@@ -165,11 +156,16 @@ import Scene from "../scene/Scene";
import { SceneState, ScrollBars } from "../scene/types";
import { getNewZoom } from "../scene/zoom";
import { findShapeByKey } from "../shapes";
import { AppProps, AppState, Gesture, GestureEvent, SceneData } from "../types";
import {
AppState,
ExcalidrawProps,
Gesture,
GestureEvent,
SceneData,
} from "../types";
import {
debounce,
distance,
getNearestScrollableContainer,
isInputLike,
isToolIcon,
isWritableElement,
@@ -183,15 +179,13 @@ import {
viewportCoordsToSceneCoords,
withBatchedUpdates,
} from "../utils";
import { isMobile } from "../is-mobile";
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
import LayerUI from "./LayerUI";
import { Stats } from "./Stats";
import { Toast } from "./Toast";
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
export const IsMobileContext = React.createContext(false);
export const useIsMobile = () => useContext(IsMobileContext);
const { history } = createHistory();
let didTapTwice: boolean = false;
@@ -281,37 +275,30 @@ export type ExcalidrawImperativeAPI = {
setScrollToContent: InstanceType<typeof App>["setScrollToContent"];
getSceneElements: InstanceType<typeof App>["getSceneElements"];
getAppState: () => InstanceType<typeof App>["state"];
refresh: InstanceType<typeof App>["refresh"];
importLibrary: InstanceType<typeof App>["importLibraryFromUrl"];
setToastMessage: InstanceType<typeof App>["setToastMessage"];
setCanvasOffsets: InstanceType<typeof App>["setCanvasOffsets"];
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
ready: true;
};
class App extends React.Component<AppProps, AppState> {
class App extends React.Component<ExcalidrawProps, AppState> {
canvas: HTMLCanvasElement | null = null;
rc: RoughCanvas | null = null;
unmounted: boolean = false;
actionManager: ActionManager;
isMobile = false;
detachIsMobileMqHandler?: () => void;
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
public static defaultProps: Partial<AppProps> = {
// needed for tests to pass since we directly render App in many tests
UIOptions: DEFAULT_UI_OPTIONS,
public static defaultProps: Partial<ExcalidrawProps> = {
width: window.innerWidth,
height: window.innerHeight,
};
private scene: Scene;
private resizeObserver: ResizeObserver | undefined;
private nearestScrollableContainer: HTMLElement | Document | undefined;
private detectPositionIntervalId: NodeJS.Timeout | undefined;
constructor(props: AppProps) {
constructor(props: ExcalidrawProps) {
super(props);
const defaultAppState = getDefaultAppState();
const {
width = window.innerWidth,
height = window.innerHeight,
excalidrawRef,
viewModeEnabled = false,
zenModeEnabled = false,
@@ -323,13 +310,13 @@ class App extends React.Component<AppProps, AppState> {
...defaultAppState,
theme,
isLoading: true,
width,
height,
...this.getCanvasOffsets(),
viewModeEnabled,
zenModeEnabled,
gridSize: gridModeEnabled ? GRID_SIZE : null,
name,
width: window.innerWidth,
height: window.innerHeight,
};
if (excalidrawRef) {
const readyPromise =
@@ -348,9 +335,7 @@ class App extends React.Component<AppProps, AppState> {
setScrollToContent: this.setScrollToContent,
getSceneElements: this.getSceneElements,
getAppState: () => this.state,
refresh: this.refresh,
importLibrary: this.importLibraryFromUrl,
setToastMessage: this.setToastMessage,
setCanvasOffsets: this.setCanvasOffsets,
} as const;
if (typeof excalidrawRef === "function") {
excalidrawRef(api);
@@ -422,6 +407,7 @@ class App extends React.Component<AppProps, AppState> {
onPointerUp={this.removePointer}
onPointerCancel={this.removePointer}
onTouchMove={this.handleTouchMove}
onDrop={this.handleCanvasOnDrop}
>
{t("labels.drawingCanvas")}
</canvas>
@@ -436,12 +422,7 @@ class App extends React.Component<AppProps, AppState> {
viewModeEnabled,
} = this.state;
const {
onCollabButtonClick,
onExportToBackend,
renderFooter,
renderCustomStats,
} = this.props;
const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props;
const DEFAULT_PASTE_X = canvasDOMWidth / 2;
const DEFAULT_PASTE_Y = canvasDOMHeight / 2;
@@ -450,64 +431,57 @@ class App extends React.Component<AppProps, AppState> {
<div
className={clsx("excalidraw", {
"excalidraw--view-mode": viewModeEnabled,
"excalidraw--mobile": this.isMobile,
})}
ref={this.excalidrawContainerRef}
onDrop={this.handleAppOnDrop}
style={{
width: canvasDOMWidth,
height: canvasDOMHeight,
}}
>
<IsMobileContext.Provider value={this.isMobile}>
<LayerUI
canvas={this.canvas}
<LayerUI
canvas={this.canvas}
appState={this.state}
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"}
libraryReturnUrl={this.props.libraryReturnUrl}
/>
<div className="excalidraw-textEditorContainer" />
{this.state.showStats && (
<Stats
appState={this.state}
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}
onClose={this.toggleStats}
/>
<div className="excalidraw-textEditorContainer" />
<div className="excalidraw-contextMenuContainer" />
{this.state.showStats && (
<Stats
appState={this.state}
setAppState={this.setAppState}
elements={this.scene.getElements()}
onClose={this.toggleStats}
renderCustomStats={renderCustomStats}
/>
)}
{this.state.toastMessage !== null && (
<Toast
message={this.state.toastMessage}
clearToast={this.clearToast}
/>
)}
<main>{this.renderCanvas()}</main>
</IsMobileContext.Provider>
)}
{this.state.toastMessage !== null && (
<Toast
message={this.state.toastMessage}
clearToast={this.clearToast}
/>
)}
<main>{this.renderCanvas()}</main>
</div>
);
}
@@ -572,6 +546,7 @@ class App extends React.Component<AppProps, AppState> {
if (typeof this.props.name !== "undefined") {
name = this.props.name;
}
this.setState(
(state) => {
// using Object.assign instead of spread to fool TS 4.2.2+ into
@@ -580,6 +555,10 @@ class App extends React.Component<AppProps, AppState> {
return Object.assign(actionResult.appState || {}, {
editingElement:
editingElement || actionResult.appState?.editingElement || null,
width: state.width,
height: state.height,
offsetTop: state.offsetTop,
offsetLeft: state.offsetLeft,
viewModeEnabled,
zenModeEnabled,
gridSize,
@@ -624,17 +603,8 @@ class App extends React.Component<AppProps, AppState> {
this.onSceneUpdated();
};
private importLibraryFromUrl = async (url: string, token?: string | null) => {
if (window.location.hash.includes(URL_HASH_KEYS.addLibrary)) {
const hash = new URLSearchParams(window.location.hash.slice(1));
hash.delete(URL_HASH_KEYS.addLibrary);
window.history.replaceState({}, APP_NAME, `#${hash.toString()}`);
} else if (window.location.search.includes(URL_QUERY_KEYS.addLibrary)) {
const query = new URLSearchParams(window.location.search);
query.delete(URL_QUERY_KEYS.addLibrary);
window.history.replaceState({}, APP_NAME, `?${query.toString()}`);
}
private importLibraryFromUrl = async (url: string) => {
window.history.replaceState({}, APP_NAME, window.location.origin);
try {
const request = await fetch(decodeURIComponent(url));
const blob = await request.blob();
@@ -643,17 +613,14 @@ class App extends React.Component<AppProps, AppState> {
throw new Error();
}
if (
token === Library.csrfToken ||
window.confirm(
t("alerts.confirmAddLibrary", { numShapes: json.library.length }),
)
) {
await Library.importLibrary(blob);
// hack to rerender the library items after import
if (this.state.isLibraryOpen) {
this.setState({ isLibraryOpen: false });
}
this.setState({ isLibraryOpen: true });
this.setState({
isLibraryOpen: true,
});
}
} catch (error) {
window.alert(t("alerts.errorLoadingLibrary"));
@@ -712,6 +679,7 @@ class App extends React.Component<AppProps, AppState> {
if (!this.state.isLoading) {
this.setState({ isLoading: true });
}
let initialData = null;
try {
initialData = (await this.props.initialData) || null;
@@ -720,6 +688,7 @@ class App extends React.Component<AppProps, AppState> {
}
const scene = restore(initialData, null);
scene.appState = {
...scene.appState,
isLoading: false,
@@ -747,18 +716,12 @@ class App extends React.Component<AppProps, AppState> {
commitToHistory: true,
});
const libraryUrl =
// current
new URLSearchParams(window.location.hash.slice(1)).get(
URL_HASH_KEYS.addLibrary,
) ||
// legacy, kept for compat reasons
new URLSearchParams(window.location.search).get(
URL_QUERY_KEYS.addLibrary,
);
const addToLibraryUrl = new URLSearchParams(window.location.search).get(
"addLibrary",
);
if (libraryUrl) {
await this.importLibraryFromUrl(libraryUrl);
if (addToLibraryUrl) {
await this.importLibraryFromUrl(addToLibraryUrl);
}
};
@@ -791,57 +754,23 @@ class App extends React.Component<AppProps, AppState> {
this.scene.addCallback(this.onSceneUpdated);
this.addEventListeners();
if (this.props.detectPosition) {
this.detectPositionIntervalId = setInterval(
this.updateOffsetsIfChanged,
DETECT_POSITION_CHANGE_INTERVAL,
);
}
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
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.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));
if (searchParams.has("web-share-target")) {
// Obtain a file that was shared via the Web Share Target API.
this.restoreFileFromShare();
} else {
this.updateDOMRect(this.initializeScene);
this.setState(this.getCanvasOffsets(), () => {
this.initializeScene();
});
}
}
public componentWillUnmount() {
this.resizeObserver?.disconnect();
this.unmounted = true;
this.removeEventListeners();
this.scene.destroy();
clearTimeout(touchTimeout);
if (this.detectPositionIntervalId) {
clearInterval(this.detectPositionIntervalId);
}
touchTimeout = 0;
}
@@ -856,10 +785,6 @@ class App extends React.Component<AppProps, AppState> {
document.removeEventListener(EVENT.COPY, this.onCopy);
document.removeEventListener(EVENT.PASTE, this.pasteFromClipboard);
document.removeEventListener(EVENT.CUT, this.onCut);
this.nearestScrollableContainer?.removeEventListener(
EVENT.SCROLL,
this.onScroll,
);
document.removeEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
document.removeEventListener(
@@ -889,8 +814,6 @@ class App extends React.Component<AppProps, AppState> {
this.onGestureEnd as any,
false,
);
this.detachIsMobileMqHandler?.();
}
private addEventListeners() {
@@ -926,15 +849,8 @@ class App extends React.Component<AppProps, AppState> {
document.addEventListener(EVENT.PASTE, this.pasteFromClipboard);
document.addEventListener(EVENT.CUT, this.onCut);
if (this.props.detectScroll) {
this.nearestScrollableContainer = getNearestScrollableContainer(
this.excalidrawContainerRef.current!,
);
this.nearestScrollableContainer.addEventListener(
EVENT.SCROLL,
this.onScroll,
);
}
document.addEventListener(EVENT.SCROLL, this.onScroll);
window.addEventListener(EVENT.RESIZE, this.onResize, false);
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
window.addEventListener(EVENT.BLUR, this.onBlur, false);
@@ -942,11 +858,22 @@ class App extends React.Component<AppProps, AppState> {
window.addEventListener(EVENT.DROP, this.disableEvent, false);
}
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) {
if (prevProps.langCode !== this.props.langCode) {
this.updateLanguage();
}
if (
prevProps.width !== this.props.width ||
prevProps.height !== this.props.height
) {
this.setState({
width: this.props.width ?? window.innerWidth,
height: this.props.height ?? window.innerHeight,
...this.getCanvasOffsets(),
});
}
if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) {
this.setState(
{ viewModeEnabled: !!this.props.viewModeEnabled },
@@ -1075,7 +1002,7 @@ class App extends React.Component<AppProps, AppState> {
},
{
renderOptimizations: true,
renderScrollbars: !this.isMobile,
renderScrollbars: !isMobile(),
},
);
if (scrollBars) {
@@ -1104,7 +1031,7 @@ class App extends React.Component<AppProps, AppState> {
}
}
private updateOffsetsIfChanged = () => {
private onScroll = debounce(() => {
const { offsetTop, offsetLeft } = this.getCanvasOffsets();
this.setState((state) => {
if (state.offsetLeft === offsetLeft && state.offsetTop === offsetTop) {
@@ -1112,9 +1039,7 @@ class App extends React.Component<AppProps, AppState> {
}
return { offsetTop, offsetLeft };
});
};
private onScroll = debounce(this.updateOffsetsIfChanged, SCROLL_TIMEOUT);
}, SCROLL_TIMEOUT);
// Copy/paste
@@ -1216,11 +1141,6 @@ class App extends React.Component<AppProps, AppState> {
return;
}
const data = await parseClipboard(event);
if (this.props.onPaste) {
if (await this.props.onPaste(data, event)) {
return;
}
}
if (data.errorMessage) {
this.setState({ errorMessage: data.errorMessage });
} else if (data.spreadsheet) {
@@ -1388,10 +1308,6 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ toastMessage: null });
};
setToastMessage = (toastMessage: string) => {
this.setState({ toastMessage });
};
restoreFileFromShare = async () => {
try {
const webShareTargetCache = await caches.open("web-share-target");
@@ -1965,7 +1881,8 @@ class App extends React.Component<AppProps, AppState> {
}
resetCursor(this.canvas);
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
if (!event[KEYS.CTRL_OR_CMD]) {
this.startTextEditing({
sceneX,
sceneY,
@@ -2339,7 +2256,10 @@ class App extends React.Component<AppProps, AppState> {
touchTimeout = window.setTimeout(() => {
touchTimeout = 0;
if (!invalidateContextMenu) {
this.handleCanvasContextMenu(event);
this.openContextMenu({
clientX: event.clientX,
clientY: event.clientY,
});
}
}, TOUCH_CTX_MENU_TIMEOUT);
}
@@ -3650,7 +3570,9 @@ class App extends React.Component<AppProps, AppState> {
}
};
private handleAppOnDrop = async (event: React.DragEvent<HTMLDivElement>) => {
private handleCanvasOnDrop = async (
event: React.DragEvent<HTMLCanvasElement>,
) => {
try {
const file = event.dataTransfer.files[0];
if (file?.type === "image/png" || file?.type === "image/svg+xml") {
@@ -3684,20 +3606,9 @@ class App extends React.Component<AppProps, AppState> {
const file = event.dataTransfer?.files[0];
if (
file?.type === MIME_TYPES.excalidrawlib ||
file?.name?.endsWith(".excalidrawlib")
file?.type === "application/json" ||
file?.name.endsWith(".excalidraw")
) {
Library.importLibrary(file)
.then(() => {
// Close and then open to get the libraries updated
this.setState({ isLibraryOpen: false });
this.setState({ isLibraryOpen: true });
})
.catch((error) =>
this.setState({ isLoading: false, errorMessage: error.message }),
);
// default: assume an Excalidraw file regardless of extension/MimeType
} else {
this.setState({ isLoading: true });
if (supported) {
try {
@@ -3709,7 +3620,23 @@ class App extends React.Component<AppProps, AppState> {
console.warn(error.name, error.message);
}
}
await this.loadFileToCanvas(file);
this.loadFileToCanvas(file);
} else if (
file?.type === MIME_TYPES.excalidrawlib ||
file?.name.endsWith(".excalidrawlib")
) {
Library.importLibrary(file)
.then(() => {
this.setState({ isLibraryOpen: false });
})
.catch((error) =>
this.setState({ isLoading: false, errorMessage: error.message }),
);
} else {
this.setState({
isLoading: false,
errorMessage: t("alerts.couldNotLoadInvalidFile"),
});
}
};
@@ -3734,27 +3661,7 @@ class App extends React.Component<AppProps, AppState> {
event: React.PointerEvent<HTMLCanvasElement>,
) => {
event.preventDefault();
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
const element = this.getElementAtPosition(x, y);
const type = element ? "element" : "canvas";
const container = this.excalidrawContainerRef.current!;
const {
top: offsetTop,
left: offsetLeft,
} = container.getBoundingClientRect();
const left = event.clientX - offsetLeft;
const top = event.clientY - offsetTop;
if (element && !this.state.selectedElementIds[element.id]) {
this.setState({ selectedElementIds: { [element.id]: true } }, () => {
this._openContextMenu({ top, left }, type);
});
} else {
this._openContextMenu({ top, left }, type);
}
this.openContextMenu(event);
};
private maybeDragNewGenericElement = (
@@ -3844,17 +3751,18 @@ class App extends React.Component<AppProps, AppState> {
return false;
};
/** @private use this.handleCanvasContextMenu */
private _openContextMenu = (
{
left,
top,
}: {
left: number;
top: number;
},
type: "canvas" | "element",
) => {
private openContextMenu = ({
clientX,
clientY,
}: {
clientX: number;
clientY: number;
}) => {
const { x, y } = viewportCoordsToSceneCoords(
{ clientX, clientY },
this.state,
);
const maybeGroupAction = actionGroup.contextItemPredicate!(
this.actionManager.getElementsIncludingDeleted(),
this.actionManager.getAppState(),
@@ -3865,20 +3773,12 @@ class App extends React.Component<AppProps, AppState> {
this.actionManager.getAppState(),
);
const maybeFlipHorizontal = actionFlipHorizontal.contextItemPredicate!(
this.actionManager.getElementsIncludingDeleted(),
this.actionManager.getAppState(),
);
const maybeFlipVertical = actionFlipVertical.contextItemPredicate!(
this.actionManager.getElementsIncludingDeleted(),
this.actionManager.getAppState(),
);
const separator = "separator";
const elements = this.scene.getElements();
const _isMobile = isMobile();
const elements = this.scene.getElements();
const element = this.getElementAtPosition(x, y);
const options: ContextMenuOption[] = [];
if (probablySupportsClipboardBlob && elements.length > 0) {
options.push(actionCopyAsPng);
@@ -3887,7 +3787,7 @@ class App extends React.Component<AppProps, AppState> {
if (probablySupportsClipboardWriteText && elements.length > 0) {
options.push(actionCopyAsSvg);
}
if (type === "canvas") {
if (!element) {
const viewModeOptions = [
...options,
typeof this.props.gridModeEnabled === "undefined" &&
@@ -3900,11 +3800,10 @@ class App extends React.Component<AppProps, AppState> {
ContextMenu.push({
options: viewModeOptions,
top,
left,
top: clientY,
left: clientX,
actionManager: this.actionManager,
appState: this.state,
container: this.excalidrawContainerRef.current!,
});
if (this.state.viewModeEnabled) {
@@ -3913,7 +3812,7 @@ class App extends React.Component<AppProps, AppState> {
ContextMenu.push({
options: [
this.isMobile &&
_isMobile &&
navigator.clipboard && {
name: "paste",
perform: (elements, appStates) => {
@@ -3924,7 +3823,7 @@ class App extends React.Component<AppProps, AppState> {
},
contextItemLabel: "labels.paste",
},
this.isMobile && navigator.clipboard && separator,
_isMobile && navigator.clipboard && separator,
probablySupportsClipboardBlob &&
elements.length > 0 &&
actionCopyAsPng,
@@ -3944,32 +3843,34 @@ class App extends React.Component<AppProps, AppState> {
actionToggleViewMode,
actionToggleStats,
],
top,
left,
top: clientY,
left: clientX,
actionManager: this.actionManager,
appState: this.state,
container: this.excalidrawContainerRef.current!,
});
return;
}
if (!this.state.selectedElementIds[element.id]) {
this.setState({ selectedElementIds: { [element.id]: true } });
}
if (this.state.viewModeEnabled) {
ContextMenu.push({
options: [navigator.clipboard && actionCopy, ...options],
top,
left,
top: clientY,
left: clientX,
actionManager: this.actionManager,
appState: this.state,
container: this.excalidrawContainerRef.current!,
});
return;
}
ContextMenu.push({
options: [
this.isMobile && actionCut,
this.isMobile && navigator.clipboard && actionCopy,
this.isMobile &&
_isMobile && actionCut,
_isMobile && navigator.clipboard && actionCopy,
_isMobile &&
navigator.clipboard && {
name: "paste",
perform: (elements, appStates) => {
@@ -3980,7 +3881,7 @@ class App extends React.Component<AppProps, AppState> {
},
contextItemLabel: "labels.paste",
},
this.isMobile && separator,
_isMobile && separator,
...options,
separator,
actionCopyStyles,
@@ -3996,17 +3897,13 @@ class App extends React.Component<AppProps, AppState> {
actionSendToBack,
actionBringToFront,
separator,
maybeFlipHorizontal && actionFlipHorizontal,
maybeFlipVertical && actionFlipVertical,
(maybeFlipHorizontal || maybeFlipVertical) && separator,
actionDuplicateSelection,
actionDeleteSelected,
],
top,
left,
top: clientY,
left: clientX,
actionManager: this.actionManager,
appState: this.state,
container: this.excalidrawContainerRef.current!,
});
};
@@ -4140,56 +4037,14 @@ class App extends React.Component<AppProps, AppState> {
}
}, 300);
private updateDOMRect = (cb?: () => void) => {
if (this.excalidrawContainerRef?.current) {
const excalidrawContainer = this.excalidrawContainerRef.current;
const {
width,
height,
left: offsetLeft,
top: offsetTop,
} = excalidrawContainer.getBoundingClientRect();
const {
width: currentWidth,
height: currentHeight,
offsetTop: currentOffsetTop,
offsetLeft: currentOffsetLeft,
} = this.state;
if (
width === currentWidth &&
height === currentHeight &&
offsetLeft === currentOffsetLeft &&
offsetTop === currentOffsetTop
) {
if (cb) {
cb();
}
return;
}
this.setState(
{
width,
height,
offsetLeft,
offsetTop,
},
() => {
cb && cb();
},
);
}
};
public refresh = () => {
public setCanvasOffsets = () => {
this.setState({ ...this.getCanvasOffsets() });
};
private getCanvasOffsets(): Pick<AppState, "offsetTop" | "offsetLeft"> {
if (this.excalidrawContainerRef?.current) {
const excalidrawContainer = this.excalidrawContainerRef.current;
const { left, top } = excalidrawContainer.getBoundingClientRect();
if (this.excalidrawContainerRef?.current?.parentElement) {
const parentElement = this.excalidrawContainerRef.current.parentElement;
const { left, top } = parentElement.getBoundingClientRect();
return {
offsetLeft: left,
offsetTop: top,
@@ -4223,6 +4078,9 @@ declare global {
history: SceneHistory;
app: InstanceType<typeof App>;
library: typeof Library;
collab: InstanceType<
typeof import("../excalidraw-app/collab/CollabWrapper").default
>;
};
}
}
+3 -2
View File
@@ -1,3 +1,4 @@
import React from "react";
import clsx from "clsx";
export const ButtonIconCycle = <T extends any>({
@@ -13,11 +14,11 @@ export const ButtonIconCycle = <T extends any>({
}) => {
const current = options.find((op) => op.value === value);
const cycle = () => {
function cycle() {
const index = options.indexOf(current!);
const next = (index + 1) % options.length;
onChange(options[next].value);
};
}
return (
<label key={group} className={clsx({ active: current!.value !== null })}>
+1 -1
View File
@@ -2,7 +2,7 @@ import React from "react";
import clsx from "clsx";
import { ToolButton } from "./ToolButton";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import useIsMobile from "../is-mobile";
import { users } from "./icons";
import "./CollabButton.scss";
+1 -1
View File
@@ -218,7 +218,7 @@
left: 2px;
}
@include isMobile {
@media #{$is-mobile-query} {
display: none;
}
}
+1 -1
View File
@@ -76,7 +76,7 @@
z-index: 1;
}
@include isMobile {
@media #{$is-mobile-query} {
.context-menu-option {
display: block;
+55 -57
View File
@@ -32,63 +32,67 @@ const ContextMenu = ({
actionManager,
appState,
}: ContextMenuProps) => {
const isDarkTheme = !!document
.querySelector(".excalidraw")
?.classList.contains("theme--dark");
return (
<Popover
onCloseRequest={onCloseRequest}
top={top}
left={left}
fitInViewport={true}
<div
className={clsx("excalidraw", {
"theme--dark theme--dark-background-none": isDarkTheme,
})}
>
<ul
className="context-menu"
onContextMenu={(event) => event.preventDefault()}
<Popover
onCloseRequest={onCloseRequest}
top={top}
left={left}
fitInViewport={true}
>
{options.map((option, idx) => {
if (option === "separator") {
return <hr key={idx} className="context-menu-option-separator" />;
}
<ul
className="context-menu"
onContextMenu={(event) => event.preventDefault()}
>
{options.map((option, idx) => {
if (option === "separator") {
return <hr key={idx} className="context-menu-option-separator" />;
}
const actionName = option.name;
const label = option.contextItemLabel
? t(option.contextItemLabel)
: "";
return (
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
<button
className={clsx("context-menu-option", {
dangerous: actionName === "deleteSelectedElements",
checkmark: option.checked?.(appState),
})}
onClick={() => actionManager.executeAction(option)}
>
<div className="context-menu-option__label">{label}</div>
<kbd className="context-menu-option__shortcut">
{actionName
? getShortcutFromShortcutName(actionName as ShortcutName)
: ""}
</kbd>
</button>
</li>
);
})}
</ul>
</Popover>
const actionName = option.name;
const label = option.contextItemLabel
? t(option.contextItemLabel)
: "";
return (
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
<button
className={clsx("context-menu-option", {
dangerous: actionName === "deleteSelectedElements",
checkmark: option.checked?.(appState),
})}
onClick={() => actionManager.executeAction(option)}
>
<div className="context-menu-option__label">{label}</div>
<kbd className="context-menu-option__shortcut">
{actionName
? getShortcutFromShortcutName(actionName as ShortcutName)
: ""}
</kbd>
</button>
</li>
);
})}
</ul>
</Popover>
</div>
);
};
const contextMenuNodeByContainer = new WeakMap<HTMLElement, HTMLDivElement>();
const getContextMenuNode = (container: HTMLElement): HTMLDivElement => {
let contextMenuNode = contextMenuNodeByContainer.get(container);
let contextMenuNode: HTMLDivElement;
const getContextMenuNode = (): HTMLDivElement => {
if (contextMenuNode) {
return contextMenuNode;
}
contextMenuNode = document.createElement("div");
container
.querySelector(".excalidraw-contextMenuContainer")!
.appendChild(contextMenuNode);
contextMenuNodeByContainer.set(container, contextMenuNode);
return contextMenuNode;
const div = document.createElement("div");
document.body.appendChild(div);
return (contextMenuNode = div);
};
type ContextMenuParams = {
@@ -97,16 +101,10 @@ type ContextMenuParams = {
left: ContextMenuProps["left"];
actionManager: ContextMenuProps["actionManager"];
appState: Readonly<AppState>;
container: HTMLElement;
};
const handleClose = (container: HTMLElement) => {
const contextMenuNode = contextMenuNodeByContainer.get(container);
if (contextMenuNode) {
unmountComponentAtNode(contextMenuNode);
contextMenuNode.remove();
contextMenuNodeByContainer.delete(container);
}
const handleClose = () => {
unmountComponentAtNode(getContextMenuNode());
};
export default {
@@ -123,11 +121,11 @@ export default {
top={params.top}
left={params.left}
options={options}
onCloseRequest={() => handleClose(params.container)}
onCloseRequest={handleClose}
actionManager={params.actionManager}
appState={params.appState}
/>,
getContextMenuNode(params.container),
getContextMenuNode(),
);
}
},
+1 -1
View File
@@ -31,7 +31,7 @@
padding: 0 16px 16px;
}
@include isMobile {
@media #{$is-mobile-query} {
.Dialog {
--metric: calc(var(--space-factor) * 4);
--inset-left: #{"max(var(--metric), var(--sal))"};
+1 -1
View File
@@ -2,7 +2,7 @@ import clsx from "clsx";
import React, { useEffect } from "react";
import { useCallbackRefState } from "../hooks/useCallbackRefState";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import useIsMobile from "../is-mobile";
import { KEYS } from "../keys";
import "./Dialog.scss";
import { back, close } from "./icons";
+8 -1
View File
@@ -28,7 +28,14 @@ export const ErrorDialog = ({
onCloseRequest={handleClose}
title={t("errorDialog.title")}
>
<div style={{ whiteSpace: "pre-wrap" }}>{message}</div>
<div>
{message.split("\n").map((line) => (
<>
{line}
<br />
</>
))}
</div>
</Dialog>
)}
</>
+1 -1
View File
@@ -55,7 +55,7 @@
}
}
@include isMobile {
@media #{$is-mobile-query} {
.ExportDialog {
display: flex;
flex-direction: column;
+1 -1
View File
@@ -6,7 +6,7 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types";
import { CanvasError } from "../errors";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import useIsMobile from "../is-mobile";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas, getExportSize } from "../scene/export";
import { AppState } from "../types";
-8
View File
@@ -349,14 +349,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.ungroup")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
/>
<Shortcut
label={t("labels.flipHorizontal")}
shortcuts={[getShortcutKey("Shift+H")]}
/>
<Shortcut
label={t("labels.flipVertical")}
shortcuts={[getShortcutKey("Shift+V")]}
/>
</ShortcutIsland>
</Column>
</Columns>
+1 -1
View File
@@ -19,7 +19,7 @@ $wide-viewport-width: 1000px;
color: $oc-gray-6;
font-size: 0.8rem;
@include isMobile {
@media #{$is-mobile-query} {
position: static;
padding-right: 2em;
}
+1 -1
View File
@@ -111,7 +111,7 @@
:root[dir="rtl"] & {
left: 2px;
}
@include isMobile {
@media #{$is-mobile-query} {
display: none;
}
}
+7 -21
View File
@@ -14,16 +14,10 @@ import { Library } from "../data/library";
import { isTextElement, showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n";
import { useIsMobile } from "../components/App";
import useIsMobile from "../is-mobile";
import { calculateScrollCenter, getSelectedElements } from "../scene";
import { ExportType } from "../scene/types";
import {
AppProps,
AppState,
ExcalidrawProps,
LibraryItem,
LibraryItems,
} from "../types";
import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types";
import { muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
@@ -71,7 +65,6 @@ interface LayerUIProps {
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
viewModeEnabled: boolean;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
UIOptions: AppProps["UIOptions"];
}
const useOnClickOutside = (
@@ -128,11 +121,10 @@ const LibraryMenuItems = ({
const rows = [];
let addedPendingElements = false;
const referrer =
libraryReturnUrl || window.location.origin + window.location.pathname;
const referrer = libraryReturnUrl || window.location.origin;
rows.push(
<div className="layer-ui__library-header" key="library-header">
<div className="layer-ui__library-header">
<ToolButton
key="import"
type="button"
@@ -142,9 +134,9 @@ const LibraryMenuItems = ({
onClick={() => {
importLibraryFromJSON()
.then(() => {
// Close and then open to get the libraries updated
// Maybe we should close and open the menu so that the items get updated.
// But for now we just close the menu.
setAppState({ isLibraryOpen: false });
setAppState({ isLibraryOpen: true });
})
.catch(muteFSAbortError)
.catch((error) => {
@@ -186,7 +178,7 @@ const LibraryMenuItems = ({
<a
href={`https://libraries.excalidraw.com?target=${
window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${Library.csrfToken}`}
}&referrer=${referrer}`}
target="_excalidraw_libraries"
>
{t("labels.libraries")}
@@ -346,7 +338,6 @@ const LayerUI = ({
renderCustomFooter,
viewModeEnabled,
libraryReturnUrl,
UIOptions,
}: LayerUIProps) => {
const isMobile = useIsMobile();
@@ -358,7 +349,6 @@ const LayerUI = ({
href="https://blog.excalidraw.com/end-to-end-encryption/"
target="_blank"
rel="noopener noreferrer"
aria-label={t("encrypted.link")}
>
<Tooltip label={t("encrypted.tooltip")} position="above" long={true}>
{shield}
@@ -367,10 +357,6 @@ const LayerUI = ({
);
const renderExportDialog = () => {
if (!UIOptions.canvasActions.export) {
return null;
}
const createExporter = (type: ExportType): ExportCB => async (
exportedElements,
scale,
+1 -1
View File
@@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
import { close } from "../components/icons";
import { MIME_TYPES } from "../constants";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import useIsMobile from "../is-mobile";
import { exportToSvg } from "../scene/export";
import { LibraryItem } from "../types";
import "./LibraryUnit.scss";
+2 -2
View File
@@ -52,7 +52,7 @@
border-radius: 6px;
box-sizing: border-box;
@include isMobile {
@media #{$is-mobile-query} {
max-width: 100%;
border: 0;
border-radius: 0;
@@ -82,7 +82,7 @@
}
}
@include isMobile {
@media #{$is-mobile-query} {
.Modal {
padding: 0;
}
+1 -13
View File
@@ -1,10 +1,9 @@
import "./Modal.scss";
import React, { useState, useLayoutEffect, useRef } from "react";
import React, { useState, useLayoutEffect } from "react";
import { createPortal } from "react-dom";
import clsx from "clsx";
import { KEYS } from "../keys";
import { useIsMobile } from "../components/App";
export const Modal = (props: {
className?: string;
@@ -49,16 +48,6 @@ export const Modal = (props: {
const useBodyRoot = () => {
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(() => {
const isDarkTheme = !!document
.querySelector(".excalidraw")
@@ -66,7 +55,6 @@ const useBodyRoot = () => {
const div = document.createElement("div");
div.classList.add("excalidraw", "excalidraw-modal-container");
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
if (isDarkTheme) {
div.classList.add("theme--dark");
+2 -2
View File
@@ -2,7 +2,7 @@
.excalidraw {
.PasteChartDialog {
@include isMobile {
@media #{$is-mobile-query} {
.Island {
display: flex;
flex-direction: column;
@@ -13,7 +13,7 @@
align-items: center;
justify-content: space-around;
flex-wrap: wrap;
@include isMobile {
@media #{$is-mobile-query} {
flex-direction: column;
justify-content: center;
}
+1 -1
View File
@@ -1,6 +1,6 @@
.excalidraw {
.popover {
position: absolute;
position: fixed;
z-index: 10;
}
}
+95 -10
View File
@@ -1,22 +1,49 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { copyTextToSystemClipboard } from "../clipboard";
import { DEFAULT_VERSION } from "../constants";
import { getCommonBounds } from "../element/bounds";
import { NonDeletedExcalidrawElement } from "../element/types";
import {
getElementsStorageSize,
getTotalStorageSize,
} from "../excalidraw-app/data/localStorage";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import useIsMobile from "../is-mobile";
import { getTargetElements } from "../scene";
import { AppState, ExcalidrawProps } from "../types";
import { AppState } from "../types";
import { debounce, getVersion, nFormatter } from "../utils";
import { close } from "./icons";
import { Island } from "./Island";
import "./Stats.scss";
type StorageSizes = { scene: number; total: number };
const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
cb({
scene: getElementsStorageSize(),
total: getTotalStorageSize(),
});
}, 500);
export const Stats = (props: {
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
onClose: () => void;
renderCustomStats: ExcalidrawProps["renderCustomStats"];
}) => {
const isMobile = useIsMobile();
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
scene: 0,
total: 0,
});
useEffect(() => {
getStorageSizes((sizes) => {
setStorageSizes(sizes);
});
});
useEffect(() => () => getStorageSizes.cancel(), []);
const boundingBox = getCommonBounds(props.elements);
const selectedElements = getTargetElements(props.elements, props.appState);
@@ -26,6 +53,17 @@ export const Stats = (props: {
return null;
}
const version = getVersion();
let hash;
let timestamp;
if (version !== DEFAULT_VERSION) {
timestamp = version.slice(0, 16).replace("T", " ");
hash = version.slice(21);
} else {
timestamp = t("stats.versionNotAvailable");
}
return (
<div className="Stats">
<Island padding={2}>
@@ -50,7 +88,17 @@ export const Stats = (props: {
<td>{t("stats.height")}</td>
<td>{Math.round(boundingBox[3]) - Math.round(boundingBox[1])}</td>
</tr>
<tr>
<th colSpan={2}>{t("stats.storage")}</th>
</tr>
<tr>
<td>{t("stats.scene")}</td>
<td>{nFormatter(storageSizes.scene, 1)}</td>
</tr>
<tr>
<td>{t("stats.total")}</td>
<td>{nFormatter(storageSizes.total, 1)}</td>
</tr>
{selectedElements.length === 1 && (
<tr>
<th colSpan={2}>{t("stats.element")}</th>
@@ -72,17 +120,31 @@ export const Stats = (props: {
<>
<tr>
<td>{"x"}</td>
<td>{Math.round(selectedBoundingBox[0])}</td>
<td>
{Math.round(
selectedElements.length === 1
? selectedElements[0].x
: selectedBoundingBox[0],
)}
</td>
</tr>
<tr>
<td>{"y"}</td>
<td>{Math.round(selectedBoundingBox[1])}</td>
<td>
{Math.round(
selectedElements.length === 1
? selectedElements[0].y
: selectedBoundingBox[1],
)}
</td>
</tr>
<tr>
<td>{t("stats.width")}</td>
<td>
{Math.round(
selectedBoundingBox[2] - selectedBoundingBox[0],
selectedElements.length === 1
? selectedElements[0].width
: selectedBoundingBox[2] - selectedBoundingBox[0],
)}
</td>
</tr>
@@ -90,7 +152,9 @@ export const Stats = (props: {
<td>{t("stats.height")}</td>
<td>
{Math.round(
selectedBoundingBox[3] - selectedBoundingBox[1],
selectedElements.length === 1
? selectedElements[0].height
: selectedBoundingBox[3] - selectedBoundingBox[1],
)}
</td>
</tr>
@@ -106,7 +170,28 @@ export const Stats = (props: {
</td>
</tr>
)}
{props.renderCustomStats?.(props.elements, props.appState)}
<tr>
<th colSpan={2}>{t("stats.version")}</th>
</tr>
<tr>
<td
colSpan={2}
style={{ textAlign: "center", cursor: "pointer" }}
onClick={async () => {
try {
await copyTextToSystemClipboard(getVersion());
props.setAppState({
toastMessage: t("toast.copyToClipboard"),
});
} catch {}
}}
title={t("stats.versionCopy")}
>
{timestamp}
<br />
{hash}
</td>
</tr>
</tbody>
</table>
</Island>
+1 -1
View File
@@ -193,7 +193,7 @@
margin-left: 5px;
margin-top: 1px;
@include isMobile {
@media #{$is-mobile-query} {
display: none;
}
}
+1 -26
View File
@@ -1,6 +1,5 @@
import { FontFamily } from "./element/types";
import cssVariables from "./css/variables.module.scss";
import { AppProps } from "./types";
export const APP_NAME = "Excalidraw";
@@ -102,7 +101,7 @@ export const TITLE_TIMEOUT = 10000;
export const TOAST_TIMEOUT = 5000;
export const VERSION_TIMEOUT = 30000;
export const SCROLL_TIMEOUT = 100;
export const DETECT_POSITION_CHANGE_INTERVAL = 500;
export const ZOOM_STEP = 0.1;
// Report a user inactive after IDLE_THRESHOLD milliseconds
@@ -117,27 +116,3 @@ export const MODES = {
};
export const THEME_FILTER = cssVariables.themeFilter;
export const URL_QUERY_KEYS = {
addLibrary: "addLibrary",
} as const;
export const URL_HASH_KEYS = {
addLibrary: "addLibrary",
} as const;
export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
canvasActions: {
changeViewBackgroundColor: true,
clearCanvas: true,
export: true,
loadScene: true,
saveAsScene: true,
saveScene: true,
theme: true,
},
};
export const MQ_MAX_WIDTH_PORTRAIT = 730;
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
+1 -9
View File
@@ -16,8 +16,6 @@
bottom: 0;
left: 0;
right: 0;
height: 100%;
width: 100%;
// serves 2 purposes:
// 1. prevent selecting text outside the component when double-clicking or
@@ -49,12 +47,6 @@
z-index: var(--zIndex-canvas);
}
#canvas {
// Remove the main canvas from document flow to avoid resizeObserver
// feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
position: absolute;
}
&.theme--dark {
// The percentage is inspired by
// https://material.io/design/color/dark-theme.html#properties, which
@@ -480,7 +472,7 @@
}
}
@include isMobile {
@media #{$is-mobile-query} {
aside {
display: none;
}
+3 -6
View File
@@ -1,13 +1,10 @@
@import "open-color/open-color.scss";
@mixin isMobile() {
@at-root .excalidraw--mobile#{&} {
@content;
}
}
// keep up to date with is-mobile.tsx
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
$theme-filter: "invert(93%) hue-rotate(180deg)";
:export {
isMobileQuery: unquote($is-mobile-query);
themeFilter: unquote($theme-filter);
}
+8 -2
View File
@@ -1,5 +1,5 @@
import { cleanAppStateForExport } from "../appState";
import { EXPORT_DATA_TYPES } from "../constants";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { CanvasError } from "../errors";
import { t } from "../i18n";
@@ -95,7 +95,13 @@ export const loadFromBlob = async (
elements: clearElementsForExport(data.elements || []),
appState: {
theme: localAppState?.theme,
fileHandle: (!blob.type.startsWith("image/") && blob.handle) || null,
fileHandle:
blob.handle &&
["application/json", MIME_TYPES.excalidraw].includes(
getMimeType(blob),
)
? blob.handle
: null,
...cleanAppStateForExport(data.appState || {}),
...(localAppState
? calculateScrollCenter(data.elements || [], localAppState, null)
+1 -1
View File
@@ -116,5 +116,5 @@ export const importLibraryFromJSON = async () => {
extensions: [".json", ".excalidrawlib"],
*/
});
await Library.importLibrary(blob);
Library.importLibrary(blob);
};
-2
View File
@@ -4,11 +4,9 @@ import { restoreElements } from "./restore";
import { STORAGE_KEYS } from "../constants";
import { getNonDeletedElements } from "../element";
import { NonDeleted, ExcalidrawElement } from "../element/types";
import { nanoid } from "nanoid";
export class Library {
private static libraryCache: LibraryItems | null = null;
public static csrfToken = nanoid();
static resetLibrary = () => {
Library.libraryCache = null;
+3 -1
View File
@@ -144,7 +144,7 @@ export const restoreElements = (
export const restoreAppState = (
appState: ImportedDataState["appState"],
localAppState: Partial<AppState> | null,
): DataState["appState"] => {
): AppState => {
appState = appState || {};
const defaultAppState = getDefaultAppState();
@@ -166,6 +166,8 @@ export const restoreAppState = (
return {
...nextAppState,
offsetLeft: appState.offsetLeft || 0,
offsetTop: appState.offsetTop || 0,
// Migrates from previous version where appState.zoom was a number
zoom:
typeof appState.zoom === "number"
+1 -1
View File
@@ -6,7 +6,7 @@ export interface DataState {
version?: string;
source?: string;
elements: readonly ExcalidrawElement[];
appState: Omit<AppState, "offsetTop" | "offsetLeft" | "width" | "height">;
appState: MarkOptional<AppState, "offsetTop" | "offsetLeft">;
}
export interface ImportedDataState {
+3 -3
View File
@@ -31,7 +31,7 @@ import {
import { PointerDownState } from "../components/App";
import { Point } from "../types";
export const normalizeAngle = (angle: number): number => {
const normalizeAngle = (angle: number): number => {
if (angle >= 2 * Math.PI) {
return angle - 2 * Math.PI;
}
@@ -181,7 +181,7 @@ const getPerfectElementSizeWithRotation = (
return rotate(size.width, size.height, 0, 0, -angle);
};
export const reshapeSingleTwoPointElement = (
const reshapeSingleTwoPointElement = (
element: NonDeleted<ExcalidrawLinearElement>,
resizeArrowDirection: "origin" | "end",
isRotateWithDiscreteAngle: boolean,
@@ -378,7 +378,7 @@ const resizeSingleTextElement = (
}
};
export const resizeSingleElement = (
const resizeSingleElement = (
stateAtResizeStart: NonDeletedExcalidrawElement,
shouldKeepSidesRatio: boolean,
element: NonDeletedExcalidrawElement,
-85
View File
@@ -1,85 +0,0 @@
import React, { useEffect, useState } from "react";
import { debounce, getVersion, nFormatter } from "../utils";
import {
getElementsStorageSize,
getTotalStorageSize,
} from "./data/localStorage";
import { DEFAULT_VERSION } from "../constants";
import { t } from "../i18n";
import { copyTextToSystemClipboard } from "../clipboard";
type StorageSizes = { scene: number; total: number };
const STORAGE_SIZE_TIMEOUT = 500;
const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
cb({
scene: getElementsStorageSize(),
total: getTotalStorageSize(),
});
}, STORAGE_SIZE_TIMEOUT);
type Props = {
setToastMessage: (message: string) => void;
};
const CustomStats = (props: Props) => {
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
scene: 0,
total: 0,
});
useEffect(() => {
getStorageSizes((sizes) => {
setStorageSizes(sizes);
});
});
useEffect(() => () => getStorageSizes.cancel(), []);
const version = getVersion();
let hash;
let timestamp;
if (version !== DEFAULT_VERSION) {
timestamp = version.slice(0, 16).replace("T", " ");
hash = version.slice(21);
} else {
timestamp = t("stats.versionNotAvailable");
}
return (
<>
<tr>
<th colSpan={2}>{t("stats.storage")}</th>
</tr>
<tr>
<td>{t("stats.scene")}</td>
<td>{nFormatter(storageSizes.scene, 1)}</td>
</tr>
<tr>
<td>{t("stats.total")}</td>
<td>{nFormatter(storageSizes.total, 1)}</td>
</tr>
<tr>
<th colSpan={2}>{t("stats.version")}</th>
</tr>
<tr>
<td
colSpan={2}
style={{ textAlign: "center", cursor: "pointer" }}
onClick={async () => {
try {
await copyTextToSystemClipboard(getVersion());
props.setToastMessage(t("toast.copyToClipboard"));
} catch {}
}}
title={t("stats.versionCopy")}
>
{timestamp}
<br />
{hash}
</td>
</tr>
</>
);
};
export default CustomStats;
+3 -16
View File
@@ -38,7 +38,7 @@ import Portal from "./Portal";
import RoomDialog from "./RoomDialog";
import { createInverseContext } from "../../createInverseContext";
import { t } from "../../i18n";
import { UserIdleState } from "../../types";
import { UserIdleState } from "./types";
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
import { trackEvent } from "../../analytics";
@@ -113,8 +113,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
process.env.NODE_ENV === ENV.TEST ||
process.env.NODE_ENV === ENV.DEVELOPMENT
) {
window.collab = window.collab || ({} as Window["collab"]);
Object.defineProperties(window, {
window.h = window.h || ({} as Window["h"]);
Object.defineProperties(window.h, {
collab: {
configurable: true,
value: this,
@@ -658,17 +658,4 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
}
}
declare global {
interface Window {
collab: InstanceType<typeof CollabWrapper>;
}
}
if (
process.env.NODE_ENV === ENV.TEST ||
process.env.NODE_ENV === ENV.DEVELOPMENT
) {
window.collab = window.collab || ({} as Window["collab"]);
}
export default CollabWrapper;
+1 -1
View File
@@ -9,7 +9,7 @@ import CollabWrapper from "./CollabWrapper";
import { getSyncableElements } from "../../packages/excalidraw/index";
import { ExcalidrawElement } from "../../element/types";
import { BROADCAST, SCENE } from "../app_constants";
import { UserIdleState } from "../../types";
import { UserIdleState } from "./types";
import { trackEvent } from "../../analytics";
class Portal {
+3 -3
View File
@@ -32,13 +32,13 @@
display: flex;
align-items: center;
justify-content: center;
@include isMobile {
@media #{$is-mobile-query} {
flex-direction: column;
align-items: stretch;
}
}
@include isMobile {
@media #{$is-mobile-query} {
.RoomDialog-usernameLabel {
font-weight: bold;
}
@@ -51,7 +51,7 @@
min-width: 0;
flex: 1 1 auto;
margin-inline-start: 1em;
@include isMobile {
@media #{$is-mobile-query} {
margin-top: 0.5em;
margin-inline-start: 0;
}
+5
View File
@@ -0,0 +1,5 @@
export enum UserIdleState {
ACTIVE = "active",
AWAY = "away",
IDLE = "idle",
}
+2 -1
View File
@@ -3,7 +3,8 @@ import { restore } from "../../data/restore";
import { ImportedDataState } from "../../data/types";
import { ExcalidrawElement } from "../../element/types";
import { t } from "../../i18n";
import { AppState, UserIdleState } from "../../types";
import { AppState } from "../../types";
import { UserIdleState } from "../collab/types";
const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2);
+4 -4
View File
@@ -101,7 +101,7 @@ export const importFromLocalStorage = () => {
export const getElementsStorageSize = () => {
try {
const elements = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS);
const elementsSize = elements?.length || 0;
const elementsSize = elements ? JSON.stringify(elements).length : 0;
return elementsSize;
} catch (error) {
console.error(error);
@@ -117,9 +117,9 @@ export const getTotalStorageSize = () => {
APP_STORAGE_KEYS.LOCAL_STORAGE_LIBRARY,
);
const appStateSize = appState?.length || 0;
const collabSize = collab?.length || 0;
const librarySize = library?.length || 0;
const appStateSize = appState ? JSON.stringify(appState).length : 0;
const collabSize = collab ? JSON.stringify(collab).length : 0;
const librarySize = library ? JSON.stringify(library).length : 0;
return appStateSize + collabSize + librarySize + getElementsStorageSize();
} catch (error) {
+34 -42
View File
@@ -3,6 +3,7 @@ import React, {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
@@ -11,13 +12,7 @@ import { getDefaultAppState } from "../appState";
import { ExcalidrawImperativeAPI } from "../components/App";
import { ErrorDialog } from "../components/ErrorDialog";
import { TopErrorBoundary } from "../components/TopErrorBoundary";
import {
APP_NAME,
EVENT,
TITLE_TIMEOUT,
URL_HASH_KEYS,
VERSION_TIMEOUT,
} from "../constants";
import { APP_NAME, EVENT, TITLE_TIMEOUT, VERSION_TIMEOUT } from "../constants";
import { loadFromBlob } from "../data/blob";
import { DataState, ImportedDataState } from "../data/types";
import {
@@ -49,7 +44,6 @@ import {
importFromLocalStorage,
saveToLocalStorage,
} from "./data/localStorage";
import CustomStats from "./CustomStats";
const languageDetector = new LanguageDetector();
languageDetector.init({
@@ -161,11 +155,31 @@ const initializeScene = async (opts: {
return null;
};
const ExcalidrawWrapper = () => {
function ExcalidrawWrapper() {
// dimensions
// ---------------------------------------------------------------------------
const [dimensions, setDimensions] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
const [errorMessage, setErrorMessage] = useState("");
const currentLangCode = languageDetector.detect() || defaultLang.code;
const [langCode, setLangCode] = useState(currentLangCode);
useLayoutEffect(() => {
const onResize = () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
// initial state
// ---------------------------------------------------------------------------
@@ -199,24 +213,12 @@ const ExcalidrawWrapper = () => {
initialStatePromiseRef.current.promise.resolve(scene);
});
const onHashChange = (event: HashChangeEvent) => {
event.preventDefault();
const hash = new URLSearchParams(window.location.hash.slice(1));
const libraryUrl = hash.get(URL_HASH_KEYS.addLibrary);
if (libraryUrl) {
// If hash changed and it contains library url, import it and replace
// the url to its previous state (important in case of collaboration
// and similar).
// Using history API won't trigger another hashchange.
window.history.replaceState({}, "", event.oldURL);
excalidrawAPI.importLibrary(libraryUrl, hash.get("token"));
} else {
initializeScene({ collabAPI }).then((scene) => {
if (scene) {
excalidrawAPI.updateScene(scene);
}
});
}
const onHashChange = (_: HashChangeEvent) => {
initializeScene({ collabAPI }).then((scene) => {
if (scene) {
excalidrawAPI.updateScene(scene);
}
});
};
const titleTimeout = setTimeout(
@@ -303,19 +305,13 @@ const ExcalidrawWrapper = () => {
[langCode],
);
const renderCustomStats = () => {
return (
<CustomStats
setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
/>
);
};
return (
<>
<Excalidraw
ref={excalidrawRefCallback}
onChange={onChange}
width={dimensions.width}
height={dimensions.height}
initialData={initialStatePromiseRef.current.promise}
onCollabButtonClick={collabAPI?.onCollabButtonClick}
isCollaborating={collabAPI?.isCollaborating()}
@@ -323,8 +319,6 @@ const ExcalidrawWrapper = () => {
onExportToBackend={onExportToBackend}
renderFooter={renderFooter}
langCode={langCode}
renderCustomStats={renderCustomStats}
detectScroll={false}
/>
{excalidrawAPI && <CollabWrapper excalidrawAPI={excalidrawAPI} />}
{errorMessage && (
@@ -335,9 +329,9 @@ const ExcalidrawWrapper = () => {
)}
</>
);
};
}
const ExcalidrawApp = () => {
export default function ExcalidrawApp() {
return (
<TopErrorBoundary>
<CollabContextConsumer>
@@ -345,6 +339,4 @@ const ExcalidrawApp = () => {
</CollabContextConsumer>
</TopErrorBoundary>
);
};
export default ExcalidrawApp;
}
-2
View File
@@ -89,5 +89,3 @@ interface Blob {
handle?: import("browser-fs-acces").FileSystemHandle;
name?: string;
}
declare module "*.scss";
-2
View File
@@ -63,8 +63,6 @@ const canvas = exportToCanvas(
...getDefaultAppState(),
offsetTop: 0,
offsetLeft: 0,
width: 0,
height: 0,
},
{
exportBackground: true,
+40
View File
@@ -0,0 +1,40 @@
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 default function useIsMobile() {
return useContext(context);
}
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "توسيط أفقي",
"distributeHorizontally": "التوزيع الأفقي",
"distributeVertically": "التوزيع عمودياً",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "نمط العرض",
"toggleExportColorScheme": "",
"share": "مشاركة"
@@ -230,8 +228,7 @@
"zoomToSelection": ""
},
"encrypted": {
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.",
"link": ""
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا."
},
"stats": {
"angle": "الزاوية",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Центрирай хоризонтално",
"distributeHorizontally": "Разпредели хоризонтално",
"distributeVertically": "Разпредели вертикално",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Изглед",
"toggleExportColorScheme": "",
"share": ""
@@ -230,8 +228,7 @@
"zoomToSelection": "Приближи селекцията"
},
"encrypted": {
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат.",
"link": ""
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат."
},
"stats": {
"angle": "Ъгъл",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Centrar horitzontalment",
"distributeHorizontally": "Distribuir horitzontalment",
"distributeVertically": "Distribuir verticalment",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Mode de visualització",
"toggleExportColorScheme": "Canvia l'esquema de colors de l'exportació",
"share": "Compartir"
@@ -230,8 +228,7 @@
"zoomToSelection": "Zoom per veure la selecció"
},
"encrypted": {
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors dExcalidraw no els veuran mai.",
"link": ""
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors dExcalidraw no els veuran mai."
},
"stats": {
"angle": "Angle",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Horizontal zentrieren",
"distributeHorizontally": "Horizontal verteilen",
"distributeVertically": "Vertikal verteilen",
"flipHorizontal": "Horizontal spiegeln",
"flipVertical": "Vertikal spiegeln",
"viewMode": "Ansichtsmodus",
"toggleExportColorScheme": "Farbschema für Export umschalten",
"share": "Teilen"
@@ -230,8 +228,7 @@
"zoomToSelection": "Auf Auswahl zoomen"
},
"encrypted": {
"tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals.",
"link": "Blogbeitrag über Ende-zu-Ende-Verschlüsselung in Excalidraw"
"tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals."
},
"stats": {
"angle": "Winkel",
+2 -5
View File
@@ -61,7 +61,7 @@
"architect": "Αρχιτέκτονας",
"artist": "Καλλιτέχνης",
"cartoonist": "Σκιτσογράφος",
"fileTitle": "Όνομα αρχείου",
"fileTitle": "",
"colorPicker": "Επιλογή Χρώματος",
"canvasBackground": "Φόντο καμβά",
"drawingCanvas": "Σχεδίαση καμβά",
@@ -92,8 +92,6 @@
"centerHorizontally": "Κέντρο οριζόντια",
"distributeHorizontally": "Οριζόντια κατανομή",
"distributeVertically": "Κατακόρυφη κατανομή",
"flipHorizontal": "Οριζόντια αναστροφή",
"flipVertical": "Κατακόρυφη αναστροφή",
"viewMode": "Λειτουργία προβολής",
"toggleExportColorScheme": "Εναλλαγή εξαγωγής θέματος χρωμάτων",
"share": "Κοινοποίηση"
@@ -230,8 +228,7 @@
"zoomToSelection": "Ζουμ στην επιλογή"
},
"encrypted": {
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw.",
"link": ""
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw."
},
"stats": {
"angle": "Γωνία",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Center horizontally",
"distributeHorizontally": "Distribute horizontally",
"distributeVertically": "Distribute vertically",
"flipHorizontal": "Flip horizontal",
"flipVertical": "Flip vertical",
"viewMode": "View mode",
"toggleExportColorScheme": "Toggle export color scheme",
"share": "Share"
@@ -230,8 +228,7 @@
"zoomToSelection": "Zoom to selection"
},
"encrypted": {
"tooltip": "Your drawings are end-to-end encrypted so Excalidraw's servers will never see them.",
"link": "Blog post on end-to-end encryption in Excalidraw"
"tooltip": "Your drawings are end-to-end encrypted so Excalidraw's servers will never see them."
},
"stats": {
"angle": "Angle",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Centrar horizontalmente",
"distributeHorizontally": "Distribuir horizontalmente",
"distributeVertically": "Distribuir verticalmente",
"flipHorizontal": "Girar horizontalmente",
"flipVertical": "Girar verticalmente",
"viewMode": "Modo presentación",
"toggleExportColorScheme": "Cambiar el esquema de colores de exportación",
"share": "Compartir"
@@ -230,8 +228,7 @@
"zoomToSelection": "Hacer zoom a la selección"
},
"encrypted": {
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán.",
"link": ""
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán."
},
"stats": {
"angle": "Ángulo",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "وسط قرار دادن به صورت افقی",
"distributeHorizontally": "توزیع کردن به صورت افقی",
"distributeVertically": "توزیع کردن به صورت عمودی",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "",
"toggleExportColorScheme": "",
"share": ""
@@ -230,8 +228,7 @@
"zoomToSelection": "بزرگنمایی قسمت انتخاب شده"
},
"encrypted": {
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
"link": ""
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند."
},
"stats": {
"angle": "زاویه",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Keskitä vaakasuunnassa",
"distributeHorizontally": "Jaa vaakasuunnassa",
"distributeVertically": "Jaa pystysuunnassa",
"flipHorizontal": "Käännä vaakasuunnassa",
"flipVertical": "Käännä pystysuunnassa",
"viewMode": "Katselutila",
"toggleExportColorScheme": "Vaihda viennin väriteema",
"share": "Jaa"
@@ -230,8 +228,7 @@
"zoomToSelection": "Näytä valinta"
},
"encrypted": {
"tooltip": "Piirroksesi ovat päästä-päähän-salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä.",
"link": ""
"tooltip": "Piirroksesi ovat päästä-päähän-salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä."
},
"stats": {
"angle": "Kulma",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Centrer horizontalement",
"distributeHorizontally": "Distribuer horizontalement",
"distributeVertically": "Distribuer verticalement",
"flipHorizontal": "Retourner horizontalement",
"flipVertical": "Retourner verticalement",
"viewMode": "Mode présentation",
"toggleExportColorScheme": "Activer/Désactiver l'export du thème de couleur",
"share": "Partager"
@@ -230,8 +228,7 @@
"zoomToSelection": "Zoomer sur la sélection"
},
"encrypted": {
"tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais.",
"link": "Article de blog sur le chiffrement de bout en bout dans Excalidraw"
"tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais."
},
"stats": {
"angle": "Angle",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "מרכז אופקית",
"distributeHorizontally": "חלוקה אופקית",
"distributeVertically": "חלוקה אנכית",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "מצב תצוגה",
"toggleExportColorScheme": "",
"share": ""
@@ -230,8 +228,7 @@
"zoomToSelection": "התמקד בבחירה"
},
"encrypted": {
"tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.",
"link": ""
"tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם."
},
"stats": {
"angle": "זווית",
+2 -5
View File
@@ -61,7 +61,7 @@
"architect": "वास्तुकार",
"artist": "कलाकार",
"cartoonist": "व्यंग्य चित्रकार",
"fileTitle": "फ़ाइल का नाम",
"fileTitle": "",
"colorPicker": "रंग चयन",
"canvasBackground": "कैनवास बैकग्राउंड",
"drawingCanvas": "कैनवास बना रहे हैं",
@@ -92,8 +92,6 @@
"centerHorizontally": "क्षैतिज केन्द्रित",
"distributeHorizontally": "क्षैतिज रूप से वितरित करें",
"distributeVertically": "खड़ी रूप से वितरित करें",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "",
"toggleExportColorScheme": "",
"share": ""
@@ -230,8 +228,7 @@
"zoomToSelection": "चयन तक ज़ूम करे"
},
"encrypted": {
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।",
"link": ""
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।"
},
"stats": {
"angle": "कोण",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Vízszintesen középre igazított",
"distributeHorizontally": "Vízszintes elosztás",
"distributeVertically": "Függőleges elosztás",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "",
"toggleExportColorScheme": "",
"share": ""
@@ -230,8 +228,7 @@
"zoomToSelection": ""
},
"encrypted": {
"tooltip": "A rajzaidat végpontok közötti titkosítással tároljuk, tehát az Excalidraw szervereiről se tud más belenézni.",
"link": ""
"tooltip": "A rajzaidat végpontok közötti titkosítással tároljuk, tehát az Excalidraw szervereiről se tud más belenézni."
},
"stats": {
"angle": "Szög",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Pusatkan secara horizontal",
"distributeHorizontally": "Distribusikan horizontal",
"distributeVertically": "Distribusikan vertikal",
"flipHorizontal": "Balikkan horizontal",
"flipVertical": "Balikkan vertikal",
"viewMode": "Mode tampilan",
"toggleExportColorScheme": "Ubah skema warna ekspor",
"share": "Bagikan"
@@ -230,8 +228,7 @@
"zoomToSelection": "Perbesar ke seleksi"
},
"encrypted": {
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya.",
"link": "Pos blog tentang enkripsi ujung ke ujung di Excalidraw"
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya."
},
"stats": {
"angle": "Sudut",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Centra orizzontalmente",
"distributeHorizontally": "Distribuisci orizzontalmente",
"distributeVertically": "Distribuisci verticalmente",
"flipHorizontal": "Capovolgi orizzontalmente",
"flipVertical": "Capovolgi verticalmente",
"viewMode": "Modalità visualizzazione",
"toggleExportColorScheme": "Cambia lo schema di colori in esportazione",
"share": "Condividi"
@@ -230,8 +228,7 @@
"zoomToSelection": "Zoom alla selezione"
},
"encrypted": {
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere.",
"link": "Articolo del blog sulla crittografia end-to-end di Excalidraw"
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere."
},
"stats": {
"angle": "Angolo",
+4 -7
View File
@@ -61,7 +61,7 @@
"architect": "正確",
"artist": "アート",
"cartoonist": "漫画風",
"fileTitle": "ファイル名",
"fileTitle": "",
"colorPicker": "色選択",
"canvasBackground": "キャンバスの背景",
"drawingCanvas": "キャンバスの描画",
@@ -92,8 +92,6 @@
"centerHorizontally": "横方向に中央揃え",
"distributeHorizontally": "水平方向に分散配置",
"distributeVertically": "垂直方向に分散配置",
"flipHorizontal": "水平方向に反転",
"flipVertical": "垂直方向に反転",
"viewMode": "閲覧モード",
"toggleExportColorScheme": "",
"share": ""
@@ -203,7 +201,7 @@
"desc_inProgressIntro": "共同編集セッションが有効になっています。",
"desc_shareLink": "下記URLを共同編集したい人に共有してください:",
"desc_exitSession": "セッションを終了すると部屋から切断されますが、手元の環境で編集を続けることができます。変更内容は他の人には反映されません。",
"shareTitle": "Excalidrawのライブコラボレーションセッションに参加する"
"shareTitle": ""
},
"errorDialog": {
"title": "エラー"
@@ -230,8 +228,7 @@
"zoomToSelection": "選択要素にズーム"
},
"encrypted": {
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。",
"link": "Excalidrawのエンドツーエンド暗号化に関するブログ記事"
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。"
},
"stats": {
"angle": "角度",
@@ -254,7 +251,7 @@
"copyToClipboardAsPng": "",
"fileSaved": "ファイルを保存しました",
"fileSavedToFilename": "{filename} に保存しました",
"canvas": "キャンバス",
"canvas": "",
"selection": ""
}
}
+2 -5
View File
@@ -61,7 +61,7 @@
"architect": "Amasdag",
"artist": "Anaẓur",
"cartoonist": "",
"fileTitle": "Isem n ufaylu",
"fileTitle": "",
"colorPicker": "Amafran n yini",
"canvasBackground": "Agilal n teɣzut n usuneɣ",
"drawingCanvas": "Taɣzut n usuneɣ",
@@ -92,8 +92,6 @@
"centerHorizontally": "Di tlemmast s uglawi",
"distributeHorizontally": "Freq s uglawi",
"distributeVertically": "Freq s yibeddi",
"flipHorizontal": "Tuttya taglawant",
"flipVertical": "Tuttya tubdidt",
"viewMode": "Askar n tmuɣli",
"toggleExportColorScheme": "Sermed/sens asifeḍ usentel n yini",
"share": "Bḍu"
@@ -230,8 +228,7 @@
"zoomToSelection": "Simɣur ɣer tefrayt"
},
"encrypted": {
"tooltip": "Unuɣen-inek (m) ttuwgelhnen seg yixef s ixef dɣa iqeddacen n Excalidraw werǧin ad ten-walin. ",
"link": "Amagrad ɣef uwgelhen ixef s ixef di Excalidraw"
"tooltip": "Unuɣen-inek (m) ttuwgelhnen seg yixef s ixef dɣa iqeddacen n Excalidraw werǧin ad ten-walin. "
},
"stats": {
"angle": "Tiɣmeṛt",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "수평으로 중앙 정렬",
"distributeHorizontally": "수평으로 분배",
"distributeVertically": "수직으로 분배",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "보기 모드",
"toggleExportColorScheme": "",
"share": ""
@@ -230,8 +228,7 @@
"zoomToSelection": "선택 영역으로 확대/축소"
},
"encrypted": {
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다.",
"link": ""
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다."
},
"stats": {
"angle": "각도",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "အလျားလိုက်အလယ်ညှိ",
"distributeHorizontally": "အလျားလိုက်",
"distributeVertically": "ထောင်လိုက်",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "",
"toggleExportColorScheme": "",
"share": ""
@@ -230,8 +228,7 @@
"zoomToSelection": ""
},
"encrypted": {
"tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။",
"link": ""
"tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။"
},
"stats": {
"angle": "ထောင့်",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Midtstill horisontalt",
"distributeHorizontally": "Distribuer horisontalt",
"distributeVertically": "Distribuer vertikalt",
"flipHorizontal": "Snu horisontalt",
"flipVertical": "Snu vertikalt",
"viewMode": "Visningsmodus",
"toggleExportColorScheme": "Veksle eksport av fargepalett",
"share": "Del"
@@ -230,8 +228,7 @@
"zoomToSelection": "Zoom til utvalg"
},
"encrypted": {
"tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem.",
"link": "Blogginnlegg om ende-til-ende-kryptering i Excalidraw"
"tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem."
},
"stats": {
"angle": "Vinkel",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Horizontaal Centreren",
"distributeHorizontally": "Horizontaal verspreiden",
"distributeVertically": "Verticaal distribueren",
"flipHorizontal": "Horizontaal spiegelen",
"flipVertical": "Verticaal spiegelen",
"viewMode": "Weergavemodus",
"toggleExportColorScheme": "Kleurenschema exporteren aan/uit",
"share": "Deel"
@@ -230,8 +228,7 @@
"zoomToSelection": "Inzoomen op selectie"
},
"encrypted": {
"tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent.",
"link": "Blog post over end-to-end versleuteling in Excalidraw"
"tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent."
},
"stats": {
"angle": "Hoek",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Midtstill horisontalt",
"distributeHorizontally": "Sprei horisontalt",
"distributeVertically": "Sprei vertikalt",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "",
"toggleExportColorScheme": "",
"share": ""
@@ -230,8 +228,7 @@
"zoomToSelection": ""
},
"encrypted": {
"tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei.",
"link": ""
"tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei."
},
"stats": {
"angle": "Vinkel",
+4 -7
View File
@@ -38,7 +38,7 @@
"fontSize": "Talha poliça",
"fontFamily": "Familha de poliça",
"onlySelected": "Seleccion sonque",
"withBackground": "Inclure lo rèireplan",
"withBackground": "Inclure rèireplan",
"exportEmbedScene": "Integrar la scèna al fichièr dexpo",
"exportEmbedScene_details": "Las donadas de scèna seràn enregistradas dins lo fichièr PNG/SVG exportat, per que la scèna pòsca èsser restaurada a partir daqueste fichièr.\nAumentarà la talha del fichièr exportat.",
"addWatermark": "Apondre « Fabricat amb Excalidraw »",
@@ -92,8 +92,6 @@
"centerHorizontally": "Centrar orizontalament",
"distributeHorizontally": "Distribuir orizontalament",
"distributeVertically": "Distribuir verticalament",
"flipHorizontal": "Virar orizontalament",
"flipVertical": "Virar verticalament",
"viewMode": "Mòde de vista",
"toggleExportColorScheme": "Alternar lesquèma de color dexpòrt",
"share": "Partejar"
@@ -101,8 +99,8 @@
"buttons": {
"clearReset": "Reïnicializar lo canabàs",
"export": "Exportar",
"exportToPng": "Exportar en PNG",
"exportToSvg": "Exportar en SVG",
"exportToPng": "Export en PNG",
"exportToSvg": "Export en SVG",
"copyToClipboard": "Copiar al quichapapièrs",
"copyPngToClipboard": "Copiar PNG al quichapapièrs",
"scale": "Escala",
@@ -230,8 +228,7 @@
"zoomToSelection": "Zoomar la seleccion"
},
"encrypted": {
"tooltip": "Vòstres dessenhs son chifrats del cap a la fin en consequéncia los servidors dExcalidraw los veiràn pas jamai.",
"link": "Article de blòg sul chiframent del cap a la fin dins Excalidraw"
"tooltip": "Vòstres dessenhs son chifrats del cap a la fin en consequéncia los servidors dExcalidraw los veiràn pas jamai."
},
"stats": {
"angle": "Angle",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "ਖੜ੍ਹਵੇਂ ਵਿਚਕਾਰ ਕਰੋ",
"distributeHorizontally": "ਖੜ੍ਹਵੇਂ ਇਕਸਾਰ ਵੰਡੋ",
"distributeVertically": "ਲੇਟਵੇਂ ਇਕਸਾਰ ਵੰਡੋ",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "ਦੇਖੋ ਮੋਡ",
"toggleExportColorScheme": "",
"share": ""
@@ -230,8 +228,7 @@
"zoomToSelection": "ਚੋਣ ਤੱਕ ਜ਼ੂਮ ਕਰੋ"
},
"encrypted": {
"tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।",
"link": ""
"tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।"
},
"stats": {
"angle": "ਕੋਣ",
+22 -22
View File
@@ -1,37 +1,37 @@
{
"ar-SA": 85,
"bg-BG": 93,
"ca-ES": 98,
"ar-SA": 86,
"bg-BG": 94,
"ca-ES": 99,
"de-DE": 100,
"el-GR": 98,
"en": 100,
"es-ES": 99,
"fa-IR": 88,
"fi-FI": 99,
"es-ES": 100,
"fa-IR": 89,
"fi-FI": 100,
"fr-FR": 100,
"he-IL": 89,
"hi-IN": 91,
"hu-HU": 81,
"he-IL": 90,
"hi-IN": 92,
"hu-HU": 82,
"id-ID": 100,
"it-IT": 100,
"ja-JP": 97,
"kab-KAB": 99,
"ko-KR": 92,
"my-MM": 76,
"ja-JP": 96,
"kab-KAB": 98,
"ko-KR": 94,
"my-MM": 77,
"nb-NO": 100,
"nl-NL": 100,
"nn-NO": 83,
"nn-NO": 84,
"oc-FR": 100,
"pa-IN": 94,
"pl-PL": 95,
"pa-IN": 95,
"pl-PL": 96,
"pt-BR": 100,
"pt-PT": 95,
"pt-PT": 96,
"ro-RO": 100,
"ru-RU": 98,
"sk-SK": 99,
"ru-RU": 100,
"sk-SK": 100,
"sv-SE": 100,
"tr-TR": 99,
"uk-UA": 99,
"tr-TR": 97,
"uk-UA": 100,
"zh-CN": 99,
"zh-TW": 99
"zh-TW": 100
}
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Wyśrodkuj w poziomie",
"distributeHorizontally": "Rozłóż poziomo",
"distributeVertically": "Rozłóż pionowo",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Tryb widoku",
"toggleExportColorScheme": "",
"share": ""
@@ -230,8 +228,7 @@
"zoomToSelection": "Przybliż do zaznaczenia"
},
"encrypted": {
"tooltip": "Twoje rysunki są zabezpieczone szyfrowaniem end-to-end, tak więc nawet w Excalidraw nie jesteśmy w stanie zobaczyć tego co tworzysz.",
"link": ""
"tooltip": "Twoje rysunki są zabezpieczone szyfrowaniem end-to-end, tak więc nawet w Excalidraw nie jesteśmy w stanie zobaczyć tego co tworzysz."
},
"stats": {
"angle": "Kąt",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Centralizar horizontalmente",
"distributeHorizontally": "Distribuir horizontalmente",
"distributeVertically": "Distribuir verticalmente",
"flipHorizontal": "Inverter horizontalmente",
"flipVertical": "Inverter verticalmente",
"viewMode": "Modo de visualização",
"toggleExportColorScheme": "Alternar esquema de cores de exportação",
"share": "Compartilhar"
@@ -230,8 +228,7 @@
"zoomToSelection": "Ampliar a seleção"
},
"encrypted": {
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão.",
"link": "Postagem de blog com uma criptografia de ponta a ponta no Excalidraw"
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão."
},
"stats": {
"angle": "Ângulo",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Centralizar horizontalmente",
"distributeHorizontally": "Distribuir horizontalmente",
"distributeVertically": "Distribuir verticalmente",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Modo de visualização",
"toggleExportColorScheme": "Alternar esquema de cores de exportação",
"share": ""
@@ -230,8 +228,7 @@
"zoomToSelection": "Ampliar a seleção"
},
"encrypted": {
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão.",
"link": ""
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão."
},
"stats": {
"angle": "Ângulo",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Centrare orizontală",
"distributeHorizontally": "Distribuie orizontal",
"distributeVertically": "Distribuie vertical",
"flipHorizontal": "Răsturnare orizontală",
"flipVertical": "Răsturnare verticală",
"viewMode": "Mod de vizualizare",
"toggleExportColorScheme": "Comutare schemă de culori de export",
"share": "Distribuie"
@@ -230,8 +228,7 @@
"zoomToSelection": "Panoramare la selecție"
},
"encrypted": {
"tooltip": "Desenele tale sunt criptate integral, astfel că serverele Excalidraw nu le vor vedea niciodată.",
"link": "Articol de blog pe criptarea integrală din Excalidraw"
"tooltip": "Desenele tale sunt criptate integral, astfel că serverele Excalidraw nu le vor vedea niciodată."
},
"stats": {
"angle": "Unghi",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Центрировать по горизонтали",
"distributeHorizontally": "Распределить по горизонтали",
"distributeVertically": "Распределить по вертикали",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "Вид",
"toggleExportColorScheme": "Экспортировать цветовую схему",
"share": "Поделиться"
@@ -230,8 +228,7 @@
"zoomToSelection": "Увеличить до выделенного"
},
"encrypted": {
"tooltip": "Ваши данные защищены сквозным (End-to-end) шифрованием. Серверы Excalidraw никогда не получат доступ к ним.",
"link": ""
"tooltip": "Ваши данные защищены сквозным (End-to-end) шифрованием. Серверы Excalidraw никогда не получат доступ к ним."
},
"stats": {
"angle": "Угол",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Zarovnať vodorovne na stred",
"distributeHorizontally": "Rozmiestniť vodorovne",
"distributeVertically": "Rozmiestniť zvisle",
"flipHorizontal": "Prevrátiť vodorovne",
"flipVertical": "Prevrátiť zvislo",
"viewMode": "Režim zobrazenia",
"toggleExportColorScheme": "Prepnúť exportovanie farebnej schémy",
"share": "Zdieľať"
@@ -230,8 +228,7 @@
"zoomToSelection": "Priblížiť na výber"
},
"encrypted": {
"tooltip": "Vaše kresby používajú end-to-end šifrovanie, takže ich Excalidraw server nedokáže prečítať.",
"link": ""
"tooltip": "Vaše kresby používajú end-to-end šifrovanie, takže ich Excalidraw server nedokáže prečítať."
},
"stats": {
"angle": "Uhol",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Centrera horisontellt",
"distributeHorizontally": "Fördela horisontellt",
"distributeVertically": "Fördela vertikalt",
"flipHorizontal": "Vänd horisontellt",
"flipVertical": "Vänd vertikalt",
"viewMode": "Visningsläge",
"toggleExportColorScheme": "Växla färgschema för export",
"share": "Dela"
@@ -230,8 +228,7 @@
"zoomToSelection": "Zooma till markering"
},
"encrypted": {
"tooltip": "Dina skisser är krypterade från ände till ände så Excalidraws servrar kommer aldrig att se dem.",
"link": "Blogginlägg om kryptering från ände till ände i Excalidraw"
"tooltip": "Dina skisser är krypterade från ände till ände så Excalidraws servrar kommer aldrig att se dem."
},
"stats": {
"angle": "Vinkel",
+6 -9
View File
@@ -92,10 +92,8 @@
"centerHorizontally": "Yatayda ortala",
"distributeHorizontally": "Yatay dağıt",
"distributeVertically": "Dikey dağıt",
"flipHorizontal": "Yatay döndür",
"flipVertical": "Dikey döndür",
"viewMode": "Görünüm modu",
"toggleExportColorScheme": "Renk şemasını dışa aktar/aktarma",
"viewMode": "",
"toggleExportColorScheme": "",
"share": "Paylaş"
},
"buttons": {
@@ -215,23 +213,22 @@
"curvedLine": "Eğri çizgi",
"documentation": "Dokümantasyon",
"drag": "sürükle",
"editor": "Düzenleyici",
"editor": "",
"github": "Bir hata mı buldun? Bildir",
"howto": "Rehberlerimizi takip edin",
"or": "veya",
"preventBinding": "Ok bağlamayı önleyin",
"preventBinding": "",
"shapes": "Şekiller",
"shortcuts": "Klavye kısayolları",
"textFinish": "(Metin) düzenlemeyi bitir",
"textNewLine": "Yeni satır ekle (metin)",
"title": "Yardım",
"view": "Görünüm",
"view": "",
"zoomToFit": "Tüm öğeleri sığdırmak için yakınlaştır",
"zoomToSelection": "Seçime yakınlaş"
},
"encrypted": {
"tooltip": "Çizimleriniz uçtan-uca şifrelenmiştir, Excalidraw'ın sunucuları bile onları göremez.",
"link": ""
"tooltip": "Çizimleriniz uçtan-uca şifrelenmiştir, Excalidraw'ın sunucuları bile onları göremez."
},
"stats": {
"angle": "Açı",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "Центрувати по горизонталі",
"distributeHorizontally": "Розподілити по горизонталі",
"distributeVertically": "Розподілити вертикально",
"flipHorizontal": "Віддзеркалити горизонтально",
"flipVertical": "Віддзеркалити вертикально",
"viewMode": "Режим перегляду",
"toggleExportColorScheme": "Переключити колірну схему експорту",
"share": "Поділитися"
@@ -230,8 +228,7 @@
"zoomToSelection": "Наблизити вибране"
},
"encrypted": {
"tooltip": "Ваші креслення захищені наскрізним шифруванням — сервери Excalidraw ніколи їх не побачать.",
"link": ""
"tooltip": "Ваші креслення захищені наскрізним шифруванням — сервери Excalidraw ніколи їх не побачать."
},
"stats": {
"angle": "Кут",
+5 -8
View File
@@ -25,7 +25,7 @@
"strokeStyle_dashed": "虚线",
"strokeStyle_dotted": "点虚线",
"sloppiness": "边框",
"opacity": "透明度",
"opacity": "透明度",
"textAlign": "文本对齐",
"edges": "边角",
"sharp": "尖锐",
@@ -38,7 +38,7 @@
"fontSize": "字体大小",
"fontFamily": "字体",
"onlySelected": "仅被选中",
"withBackground": "包括背景",
"withBackground": "使用背景",
"exportEmbedScene": "将画布数据嵌入到导出的文件",
"exportEmbedScene_details": "画布数据将被保存到导出的 PNG/SVG 文件,以便恢复。\n将会增加导出的文件大小。",
"addWatermark": "添加 “使用 Excalidraw 创建” 水印",
@@ -61,7 +61,7 @@
"architect": "朴素",
"artist": "艺术",
"cartoonist": "漫画家",
"fileTitle": "文件名",
"fileTitle": "",
"colorPicker": "调色盘",
"canvasBackground": "画布背景",
"drawingCanvas": "绘制 Canvas",
@@ -92,8 +92,6 @@
"centerHorizontally": "水平居中",
"distributeHorizontally": "水平等距分布",
"distributeVertically": "垂直等距分布",
"flipHorizontal": "水平翻转",
"flipVertical": "垂直翻转",
"viewMode": "查看模式",
"toggleExportColorScheme": "切换导出配色方案",
"share": "分享"
@@ -130,7 +128,7 @@
"exitZenMode": "退出禅模式"
},
"alerts": {
"clearReset": "这将会清除整个画布。您是否要继续?",
"clearReset": "这将会清除整个 画板。您是否要继续?",
"couldNotCreateShareableLink": "无法创建共享链接",
"couldNotCreateShareableLinkTooBig": "无法创建可共享链接:画布过大",
"couldNotLoadInvalidFile": "无法加载无效的文件",
@@ -230,8 +228,7 @@
"zoomToSelection": "缩放到选区"
},
"encrypted": {
"tooltip": "您的绘图采用的端到端加密,其内容对于Excalidraw服务器是不可见的。",
"link": ""
"tooltip": "您的绘图采用的端到端加密,其内容对于Excalidraw服务器是不可见的。"
},
"stats": {
"angle": "角度",
+1 -4
View File
@@ -92,8 +92,6 @@
"centerHorizontally": "水平置中",
"distributeHorizontally": "水平分布",
"distributeVertically": "垂直分布",
"flipHorizontal": "水平翻轉",
"flipVertical": "垂直翻轉",
"viewMode": "檢視模式",
"toggleExportColorScheme": "切換輸出配色",
"share": "共享"
@@ -230,8 +228,7 @@
"zoomToSelection": "縮放至選取區"
},
"encrypted": {
"tooltip": "你的作品已使用 end-to-end 方式加密,Excalidraw 的伺服器也無法取得其內容。",
"link": ""
"tooltip": "你的作品已使用 end-to-end 方式加密,Excalidraw 的伺服器也無法取得其內容。"
},
"stats": {
"angle": "角度",
+1 -73
View File
@@ -6,84 +6,12 @@ The change should be grouped under one of the below section and must contain PR
- Features: For new features.
- Fixes: For bug fixes.
- Chore: Changes for non src files example package.json.
- Improvements: For any improvements.
- Refactor: For any refactoring.
Please add the latest change on the top under the correct section.
-->
## Unreleased
## Excalidraw API
- Support detecting position of the component and recompute offsets when the position changes [#3428](https://github.com/excalidraw/excalidraw/pull/3428). Disabled by default. You can enable this by setting [`detectPosition`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#detectPosition) to `true`.
- Recompute offsets on `scroll` of the nearest scrollable container [#3408](https://github.com/excalidraw/excalidraw/pull/3408). This can be disabled by setting [`detectScroll`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#detectScroll) to `false`.
- Add `onPaste` prop to handle custom clipboard behaviours [#3420](https://github.com/excalidraw/excalidraw/pull/3420).
## 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)
## Excalidraw API
### Features
- Add `UIOptions` prop to customise `canvas actions` which includes customising `background color picker`, `clear canvas`, `export`, `load`, `save`, `save as` & `theme toggle` [#3364](https://github.com/excalidraw/excalidraw/pull/3364). Check the [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#uioptions) for more details.
- Calculate `width/height` of canvas based on excalidraw component (".excalidraw" selector) & also resize and update offsets whenever the dimensions of excalidraw component gets updated [#3379](https://github.com/excalidraw/excalidraw/pull/3379). You also don't need to add a resize handler anymore for excalidraw as its handled now in excalidraw itself.
#### BREAKING CHANGE
- `width/height` props have been removed. Instead now it takes `100%` of `width` and `height` of the container so you need to make sure the container in which you are rendering Excalidraw has non zero dimensions (It should have non zero width and height so Excalidraw can match the dimensions of containing block)
- Calculate offsets when excalidraw container resizes using resize observer api [#3374](https://github.com/excalidraw/excalidraw/pull/3374).
- Export types for the package so now it can be used with typescript [#3337](https://github.com/excalidraw/excalidraw/pull/3337). The types are available at `@excalidraw/excalidraw/types`.
- Add `renderCustomStats` prop to render extra stats on host, and expose `setToastMessage` API via refs which can be used to show toast with custom message [#3360](https://github.com/excalidraw/excalidraw/pull/3360).
- Support passing a CSRF token when importing libraries to prevent prompting before installation. The token is passed from [https://libraries.excalidraw.com](https://libraries.excalidraw.com/) using the `token` URL key [#3329](https://github.com/excalidraw/excalidraw/pull/3329).
- #### BREAKING CHANGE
Use `location.hash` when importing libraries to fix installation issues. This will require host apps to add a `hashchange` listener and call the newly exposed `excalidrawAPI.importLibrary(url)` API when applicable [#3320](https://github.com/excalidraw/excalidraw/pull/3320). Check the [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#importlibrary) for more details.
- Append `location.pathname` to `libraryReturnUrl` default url [#3325](https://github.com/excalidraw/excalidraw/pull/3325).
### Build
- Expose separate builds for dev and prod and support source maps in dev build [#3330](https://github.com/excalidraw/excalidraw/pull/3330). Check the [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#installation) for more details.
#### BREAKING CHANGE
- If you are using script tag to embed excalidraw then the name of the file will have to be updated to `excalidraw.production.min.js` instead of `excalidraw.min.js`. If you want to use dev build you can use `excalidraw.development.js`
### Refactor
#### BREAKING CHANGE
- Rename the API `setCanvasOffsets` exposed via [`ref`](https://github.com/excalidraw/excalidraw/blob/master/src/components/App.tsx#L265) to `refresh` [#3398](https://github.com/excalidraw/excalidraw/pull/3398).
## Excalidraw Library
### Features
- Reopen library menu on import from file [#3383](https://github.com/excalidraw/excalidraw/pull/3383).
- Don't unnecessarily prompt when installing libraries [#3329](https://github.com/excalidraw/excalidraw/pull/3329).
- Add option to flip single element on the context menu [#2520](https://github.com/excalidraw/excalidraw/pull/2520).
- Replace fontSize and fontFamily text with icons [#2857](https://github.com/excalidraw/excalidraw/pull/2857).
### Fixes
- Export dialog canvas positioning [#3397](https://github.com/excalidraw/excalidraw/pull/3397).
- Don't share collab types with core [#3353](https://github.com/excalidraw/excalidraw/pull/3353).
- Support d&d of files without extension [#3168](https://github.com/excalidraw/excalidraw/pull/3168).
- Positions stats for linear elements [#3331](https://github.com/excalidraw/excalidraw/pull/3331).
- Debounce.flush invokes func even if never queued before [#3326](https://github.com/excalidraw/excalidraw/pull/3326).
- State selection state on opening contextMenu [#3333](https://github.com/excalidraw/excalidraw/pull/3333).
- Add unique key for library header to resolve dev warnings [#3316](https://github.com/excalidraw/excalidraw/pull/3316).
- disallow create text in viewMode on mobile [#3219](https://github.com/excalidraw/excalidraw/pull/3219).
- Make help toggle tabbable [#3310](https://github.com/excalidraw/excalidraw/pull/3310)
- Show Windows share icon for Windows users [#3306](https://github.com/excalidraw/excalidraw/pull/3306).
- Don't scroll to content on INIT websocket message [#3291](https://github.com/excalidraw/excalidraw/pull/3291).
### Refactor
- Use arrow function where possible [#3315](https://github.com/excalidraw/excalidraw/pull/3315).
---
## 0.5.0 (2021-03-21)
## Excalidraw API

Some files were not shown because too many files have changed in this diff Show More