Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe973e3513 | |||
| ade2565f49 | |||
| c35d983fef | |||
| 69878167c2 | |||
| eb1f717d35 | |||
| 8e9af5c51b | |||
| afe0c760f6 | |||
| a231cefac0 | |||
| cb4c9d16fc | |||
| 7366f089ba | |||
| 7c3513b9df | |||
| aef3644c93 | |||
| 0cf58adb4c | |||
| 3aab81bc35 | |||
| 3b0fb1562d | |||
| 0488b7b5c6 | |||
| b8d13c98b5 | |||
| 6f82a88b79 | |||
| 022f349dc6 | |||
| c1e2146d78 | |||
| 8091ac6c08 |
@@ -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://excalidraw-socket.herokuapp.com
|
||||
REACT_APP_SOCKET_SERVER_URL=https://portal.excalidraw.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"}'
|
||||
|
||||
Generated
+9
-9
@@ -3030,9 +3030,9 @@
|
||||
}
|
||||
},
|
||||
"@testing-library/jest-dom": {
|
||||
"version": "5.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.6.tgz",
|
||||
"integrity": "sha512-cVZyUNRWwUKI0++yepYpYX7uhrP398I+tGz4zOlLVlUYnZS+Svuxv4fwLeCIy7TnBYKXUaOlQr3vopxL8ZfEnA==",
|
||||
"version": "5.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.8.tgz",
|
||||
"integrity": "sha512-ScyKrWQM5xNcr79PkSewnA79CLaoxVskE+f7knTOhDD9ftZSA1Jw8mj+pneqhEu3x37ncNfW84NUr7lqK+mXjA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@types/testing-library__jest-dom": "^5.9.1",
|
||||
@@ -9039,9 +9039,9 @@
|
||||
}
|
||||
},
|
||||
"firebase-tools": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.0.1.tgz",
|
||||
"integrity": "sha512-OktyHgjIBJR/JPNU4Xv4NsRLWu5gDnTmYd88VUsMwzUMCkbao2NNpeBi6+0rn6U1zNhwP2WW9PngccJWg/wvSA==",
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.1.0.tgz",
|
||||
"integrity": "sha512-hTfxL2meJSl5WuwAS6bEJ5nay7tu3MNYb4ZL2KmPL7yLM3IeT+Qd2z1evHhW1VvbDXKR6RTwlBxzdWPs4l75kA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@google-cloud/pubsub": "^2.7.0",
|
||||
@@ -10085,9 +10085,9 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.9.tgz",
|
||||
"integrity": "sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q==",
|
||||
"version": "12.19.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.11.tgz",
|
||||
"integrity": "sha512-bwVfNTFZOrGXyiQ6t4B9sZerMSShWNsGRw8tC5DY1qImUNczS9SjT4G6PnzjCnxsu5Ubj6xjL2lgwddkxtQl5w==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@
|
||||
"dependencies": {
|
||||
"@sentry/browser": "5.29.2",
|
||||
"@sentry/integrations": "5.29.2",
|
||||
"@testing-library/jest-dom": "5.11.6",
|
||||
"@testing-library/jest-dom": "5.11.8",
|
||||
"@testing-library/react": "11.2.2",
|
||||
"@types/jest": "26.0.19",
|
||||
"@types/nanoid": "2.1.0",
|
||||
@@ -54,7 +54,7 @@
|
||||
"@types/pako": "1.0.1",
|
||||
"eslint-config-prettier": "7.1.0",
|
||||
"eslint-plugin-prettier": "3.3.0",
|
||||
"firebase-tools": "9.0.1",
|
||||
"firebase-tools": "9.1.0",
|
||||
"husky": "4.3.6",
|
||||
"jest-canvas-mock": "2.3.0",
|
||||
"lint-staged": "10.5.3",
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import React from "react";
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
|
||||
import colors from "../colors";
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { t } from "../i18n";
|
||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { getShortcutKey } from "../utils";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { register } from "./register";
|
||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState, NormalizedZoomValue } from "../types";
|
||||
import { getCommonBounds } from "../element";
|
||||
import { getNewZoom } from "../scene/zoom";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { getNormalizedZoom, getSelectedElements } from "../scene";
|
||||
import { centerScrollOn } from "../scene/scroll";
|
||||
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
|
||||
import colors from "../colors";
|
||||
import { getNewZoom } from "../scene/zoom";
|
||||
import { AppState, NormalizedZoomValue } from "../types";
|
||||
import { getNewSceneName, getShortcutKey } from "../utils";
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionChangeViewBackgroundColor = register({
|
||||
name: "changeViewBackgroundColor",
|
||||
@@ -60,6 +59,7 @@ export const actionClearCanvas = register({
|
||||
),
|
||||
appState: {
|
||||
...getDefaultAppState(),
|
||||
name: getNewSceneName(),
|
||||
appearance: appState.appearance,
|
||||
elementLocked: appState.elementLocked,
|
||||
exportBackground: appState.exportBackground,
|
||||
@@ -67,6 +67,7 @@ export const actionClearCanvas = register({
|
||||
gridSize: appState.gridSize,
|
||||
shouldAddWatermark: appState.shouldAddWatermark,
|
||||
showStats: appState.showStats,
|
||||
pasteDialog: appState.pasteDialog,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { KEYS } from "../keys";
|
||||
import { muteFSAbortError } from "../utils";
|
||||
import { register } from "./register";
|
||||
import "../components/ToolIcon.scss";
|
||||
import { SCENE_NAME_FALLBACK } from "../constants";
|
||||
|
||||
export const actionChangeProjectName = register({
|
||||
name: "changeProjectName",
|
||||
@@ -22,7 +23,7 @@ export const actionChangeProjectName = register({
|
||||
PanelComponent: ({ appState, updateData }) => (
|
||||
<ProjectName
|
||||
label={t("labels.fileTitle")}
|
||||
value={appState.name || "Unnamed"}
|
||||
value={appState.name || SCENE_NAME_FALLBACK}
|
||||
onChange={(name: string) => updateData(name)}
|
||||
/>
|
||||
),
|
||||
|
||||
+71
-62
@@ -1,77 +1,84 @@
|
||||
import oc from "open-color";
|
||||
import { AppState, FlooredNumber, NormalizedZoomValue } from "./types";
|
||||
import { getDateTime } from "./utils";
|
||||
import { t } from "./i18n";
|
||||
import {
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_FONT_SIZE,
|
||||
SCENE_NAME_FALLBACK,
|
||||
DEFAULT_TEXT_ALIGN,
|
||||
} from "./constants";
|
||||
import { AppState, FlooredNumber, NormalizedZoomValue } from "./types";
|
||||
|
||||
export const getDefaultAppState = (): Omit<
|
||||
AppState,
|
||||
"offsetTop" | "offsetLeft"
|
||||
> => {
|
||||
type DefaultAppState = Omit<AppState, "offsetTop" | "offsetLeft" | "name"> & {
|
||||
/**
|
||||
* You should override this with current appState.name, or whatever is
|
||||
* applicable at a given place where you get default appState.
|
||||
*/
|
||||
name: undefined;
|
||||
};
|
||||
|
||||
export const getDefaultAppState = (): DefaultAppState => {
|
||||
return {
|
||||
appearance: "light",
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
collaborators: new Map(),
|
||||
currentChartType: "bar",
|
||||
currentItemBackgroundColor: "transparent",
|
||||
currentItemEndArrowhead: "arrow",
|
||||
currentItemFillStyle: "hachure",
|
||||
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
||||
currentItemFontSize: DEFAULT_FONT_SIZE,
|
||||
currentItemLinearStrokeSharpness: "round",
|
||||
currentItemOpacity: 100,
|
||||
currentItemRoughness: 1,
|
||||
currentItemStartArrowhead: null,
|
||||
currentItemStrokeColor: oc.black,
|
||||
currentItemStrokeSharpness: "sharp",
|
||||
currentItemStrokeStyle: "solid",
|
||||
currentItemStrokeWidth: 1,
|
||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||
cursorButton: "up",
|
||||
draggingElement: null,
|
||||
resizingElement: null,
|
||||
multiElement: null,
|
||||
editingElement: null,
|
||||
startBoundElement: null,
|
||||
editingGroupId: null,
|
||||
editingLinearElement: null,
|
||||
elementType: "selection",
|
||||
elementLocked: false,
|
||||
elementType: "selection",
|
||||
errorMessage: null,
|
||||
exportBackground: true,
|
||||
exportEmbedScene: false,
|
||||
shouldAddWatermark: false,
|
||||
currentItemStrokeColor: oc.black,
|
||||
currentItemBackgroundColor: "transparent",
|
||||
currentItemFillStyle: "hachure",
|
||||
currentItemStrokeWidth: 1,
|
||||
currentItemStrokeStyle: "solid",
|
||||
currentItemRoughness: 1,
|
||||
currentItemOpacity: 100,
|
||||
currentItemFontSize: DEFAULT_FONT_SIZE,
|
||||
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||
currentItemStrokeSharpness: "sharp",
|
||||
currentItemLinearStrokeSharpness: "round",
|
||||
currentItemStartArrowhead: null,
|
||||
currentItemEndArrowhead: "arrow",
|
||||
viewBackgroundColor: oc.white,
|
||||
scrollX: 0 as FlooredNumber,
|
||||
scrollY: 0 as FlooredNumber,
|
||||
cursorButton: "up",
|
||||
scrolledOutside: false,
|
||||
name: `${t("labels.untitled")}-${getDateTime()}`,
|
||||
fileHandle: null,
|
||||
gridSize: null,
|
||||
height: window.innerHeight,
|
||||
isBindingEnabled: true,
|
||||
isLibraryOpen: false,
|
||||
isLoading: false,
|
||||
isResizing: false,
|
||||
isRotating: false,
|
||||
selectionElement: null,
|
||||
zoom: {
|
||||
value: 1 as NormalizedZoomValue,
|
||||
translation: { x: 0, y: 0 },
|
||||
},
|
||||
openMenu: null,
|
||||
lastPointerDownWith: "mouse",
|
||||
selectedElementIds: {},
|
||||
multiElement: null,
|
||||
// for safety (because TS mostly doesn't distinguish optional types and
|
||||
// undefined values), we set `name` to the fallback name, but we cast it to
|
||||
// `undefined` so that TS forces us to explicitly specify it wherever
|
||||
// possible
|
||||
name: (SCENE_NAME_FALLBACK as unknown) as undefined,
|
||||
openMenu: null,
|
||||
pasteDialog: { shown: false, data: null },
|
||||
previousSelectedElementIds: {},
|
||||
resizingElement: null,
|
||||
scrolledOutside: false,
|
||||
scrollX: 0 as FlooredNumber,
|
||||
scrollY: 0 as FlooredNumber,
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
selectionElement: null,
|
||||
shouldAddWatermark: false,
|
||||
shouldCacheIgnoreZoom: false,
|
||||
showShortcutsDialog: false,
|
||||
suggestedBindings: [],
|
||||
zenModeEnabled: false,
|
||||
gridSize: null,
|
||||
editingGroupId: null,
|
||||
selectedGroupIds: {},
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
isLibraryOpen: false,
|
||||
fileHandle: null,
|
||||
collaborators: new Map(),
|
||||
showStats: false,
|
||||
startBoundElement: null,
|
||||
suggestedBindings: [],
|
||||
viewBackgroundColor: oc.white,
|
||||
width: window.innerWidth,
|
||||
zenModeEnabled: false,
|
||||
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -91,24 +98,25 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
config: { [K in keyof T]: K extends keyof AppState ? T[K] : never },
|
||||
) => config)({
|
||||
appearance: { browser: true, export: false },
|
||||
collaborators: { browser: false, export: false },
|
||||
currentChartType: { browser: true, export: false },
|
||||
currentItemBackgroundColor: { browser: true, export: false },
|
||||
currentItemEndArrowhead: { browser: true, export: false },
|
||||
currentItemFillStyle: { browser: true, export: false },
|
||||
currentItemFontFamily: { browser: true, export: false },
|
||||
currentItemFontSize: { browser: true, export: false },
|
||||
currentItemLinearStrokeSharpness: { browser: true, export: false },
|
||||
currentItemOpacity: { browser: true, export: false },
|
||||
currentItemRoughness: { browser: true, export: false },
|
||||
currentItemStartArrowhead: { browser: true, export: false },
|
||||
currentItemStrokeColor: { browser: true, export: false },
|
||||
currentItemStrokeSharpness: { browser: true, export: false },
|
||||
currentItemStrokeStyle: { browser: true, export: false },
|
||||
currentItemStrokeWidth: { browser: true, export: false },
|
||||
currentItemTextAlign: { browser: true, export: false },
|
||||
currentItemStrokeSharpness: { browser: true, export: false },
|
||||
currentItemLinearStrokeSharpness: { browser: true, export: false },
|
||||
currentItemStartArrowhead: { browser: true, export: false },
|
||||
currentItemEndArrowhead: { browser: true, export: false },
|
||||
cursorButton: { browser: true, export: false },
|
||||
draggingElement: { browser: false, export: false },
|
||||
editingElement: { browser: false, export: false },
|
||||
startBoundElement: { browser: false, export: false },
|
||||
editingGroupId: { browser: true, export: false },
|
||||
editingLinearElement: { browser: false, export: false },
|
||||
elementLocked: { browser: true, export: false },
|
||||
@@ -116,6 +124,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
errorMessage: { browser: false, export: false },
|
||||
exportBackground: { browser: true, export: false },
|
||||
exportEmbedScene: { browser: true, export: false },
|
||||
fileHandle: { browser: false, export: false },
|
||||
gridSize: { browser: true, export: true },
|
||||
height: { browser: false, export: false },
|
||||
isBindingEnabled: { browser: false, export: false },
|
||||
@@ -126,7 +135,10 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
lastPointerDownWith: { browser: true, export: false },
|
||||
multiElement: { browser: false, export: false },
|
||||
name: { browser: true, export: false },
|
||||
offsetLeft: { browser: false, export: false },
|
||||
offsetTop: { browser: false, export: false },
|
||||
openMenu: { browser: true, export: false },
|
||||
pasteDialog: { browser: false, export: false },
|
||||
previousSelectedElementIds: { browser: true, export: false },
|
||||
resizingElement: { browser: false, export: false },
|
||||
scrolledOutside: { browser: true, export: false },
|
||||
@@ -138,16 +150,13 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
shouldAddWatermark: { browser: true, export: false },
|
||||
shouldCacheIgnoreZoom: { browser: true, export: false },
|
||||
showShortcutsDialog: { browser: false, export: false },
|
||||
showStats: { browser: true, export: false },
|
||||
startBoundElement: { browser: false, export: false },
|
||||
suggestedBindings: { browser: false, export: false },
|
||||
viewBackgroundColor: { browser: true, export: true },
|
||||
width: { browser: false, export: false },
|
||||
zenModeEnabled: { browser: true, export: false },
|
||||
zoom: { browser: true, export: false },
|
||||
offsetTop: { browser: false, export: false },
|
||||
offsetLeft: { browser: false, export: false },
|
||||
fileHandle: { browser: false, export: false },
|
||||
collaborators: { browser: false, export: false },
|
||||
showStats: { browser: true, export: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
|
||||
|
||||
+311
-115
@@ -1,13 +1,16 @@
|
||||
import { EVENT_MAGIC, trackEvent } from "./analytics";
|
||||
import colors from "./colors";
|
||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "./constants";
|
||||
import { newElement, newTextElement, newLinearElement } from "./element";
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, ENV } from "./constants";
|
||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||
import { NonDeletedExcalidrawElement } from "./element/types";
|
||||
import { randomId } from "./random";
|
||||
|
||||
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
||||
|
||||
const BAR_WIDTH = 32;
|
||||
const BAR_GAP = 12;
|
||||
const BAR_HEIGHT = 256;
|
||||
const GRID_OPACITY = 50;
|
||||
|
||||
export interface Spreadsheet {
|
||||
title: string | null;
|
||||
@@ -139,114 +142,48 @@ export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
|
||||
return transposedResults;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// For the maths behind it https://excalidraw.com/#json=6320864370884608,O_5xfD-Agh32tytHpRJx1g
|
||||
export const renderSpreadsheet = (
|
||||
const bgColors = colors.elementBackground.slice(
|
||||
2,
|
||||
colors.elementBackground.length,
|
||||
);
|
||||
|
||||
// Put all the common properties here so when the whole chart is selected
|
||||
// the properties dialog shows the correct selected values
|
||||
const commonProps = {
|
||||
fillStyle: "hachure",
|
||||
fontFamily: DEFAULT_FONT_FAMILY,
|
||||
fontSize: DEFAULT_FONT_SIZE,
|
||||
opacity: 100,
|
||||
roughness: 1,
|
||||
strokeColor: colors.elementStroke[0],
|
||||
strokeSharpness: "sharp",
|
||||
strokeStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
verticalAlign: "middle",
|
||||
} as const;
|
||||
|
||||
const getChartDimentions = (spreadsheet: Spreadsheet) => {
|
||||
const chartWidth =
|
||||
(BAR_WIDTH + BAR_GAP) * spreadsheet.values.length + BAR_GAP;
|
||||
const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
|
||||
return { chartWidth, chartHeight };
|
||||
};
|
||||
|
||||
const chartXLabels = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
): ExcalidrawElement[] => {
|
||||
const values = spreadsheet.values;
|
||||
const max = Math.max(...values);
|
||||
const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
|
||||
const chartWidth = (BAR_WIDTH + BAR_GAP) * values.length + BAR_GAP;
|
||||
const maxColors = colors.elementBackground.length;
|
||||
const bgColors = colors.elementBackground.slice(2, maxColors);
|
||||
|
||||
// Put all the common properties here so when the whole chart is selected
|
||||
// the properties dialog shows the correct selected values
|
||||
const commonProps = {
|
||||
backgroundColor: bgColors[Math.floor(Math.random() * bgColors.length)],
|
||||
fillStyle: "hachure",
|
||||
fontFamily: DEFAULT_FONT_FAMILY,
|
||||
fontSize: DEFAULT_FONT_SIZE,
|
||||
groupIds: [randomId()],
|
||||
opacity: 100,
|
||||
roughness: 1,
|
||||
strokeColor: colors.elementStroke[0],
|
||||
strokeSharpness: "sharp",
|
||||
strokeStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
verticalAlign: "middle",
|
||||
} as const;
|
||||
|
||||
const minYLabel = newTextElement({
|
||||
...commonProps,
|
||||
x: x - BAR_GAP,
|
||||
y: y - BAR_GAP,
|
||||
text: "0",
|
||||
textAlign: "right",
|
||||
});
|
||||
|
||||
const maxYLabel = newTextElement({
|
||||
...commonProps,
|
||||
x: x - BAR_GAP,
|
||||
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
||||
text: max.toLocaleString(),
|
||||
textAlign: "right",
|
||||
});
|
||||
|
||||
const xAxisLine = newLinearElement({
|
||||
type: "line",
|
||||
x,
|
||||
y,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
width: chartWidth,
|
||||
points: [
|
||||
[0, 0],
|
||||
[chartWidth, 0],
|
||||
],
|
||||
...commonProps,
|
||||
});
|
||||
|
||||
const yAxisLine = newLinearElement({
|
||||
type: "line",
|
||||
x,
|
||||
y,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
height: chartHeight,
|
||||
points: [
|
||||
[0, 0],
|
||||
[0, -chartHeight],
|
||||
],
|
||||
...commonProps,
|
||||
});
|
||||
|
||||
const maxValueLine = newLinearElement({
|
||||
type: "line",
|
||||
x,
|
||||
y: y - BAR_HEIGHT - BAR_GAP,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
...commonProps,
|
||||
strokeStyle: "dotted",
|
||||
width: chartWidth,
|
||||
points: [
|
||||
[0, 0],
|
||||
[chartWidth, 0],
|
||||
],
|
||||
});
|
||||
|
||||
const bars = values.map((value, index) => {
|
||||
const barHeight = (value / max) * BAR_HEIGHT;
|
||||
return newElement({
|
||||
...commonProps,
|
||||
type: "rectangle",
|
||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP,
|
||||
y: y - barHeight - BAR_GAP,
|
||||
width: BAR_WIDTH,
|
||||
height: barHeight,
|
||||
});
|
||||
});
|
||||
|
||||
const xLabels =
|
||||
groupId: string,
|
||||
backgroundColor: string,
|
||||
): ChartElements => {
|
||||
return (
|
||||
spreadsheet.labels?.map((label, index) => {
|
||||
return newTextElement({
|
||||
groupIds: [groupId],
|
||||
backgroundColor,
|
||||
...commonProps,
|
||||
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
|
||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
|
||||
@@ -257,29 +194,288 @@ export const renderSpreadsheet = (
|
||||
textAlign: "center",
|
||||
verticalAlign: "top",
|
||||
});
|
||||
}) || [];
|
||||
}) || []
|
||||
);
|
||||
};
|
||||
|
||||
const chartYLabels = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
groupId: string,
|
||||
backgroundColor: string,
|
||||
): ChartElements => {
|
||||
const minYLabel = newTextElement({
|
||||
groupIds: [groupId],
|
||||
backgroundColor,
|
||||
...commonProps,
|
||||
x: x - BAR_GAP,
|
||||
y: y - BAR_GAP,
|
||||
text: "0",
|
||||
textAlign: "right",
|
||||
});
|
||||
|
||||
const maxYLabel = newTextElement({
|
||||
groupIds: [groupId],
|
||||
backgroundColor,
|
||||
...commonProps,
|
||||
x: x - BAR_GAP,
|
||||
y: y - BAR_HEIGHT - minYLabel.height / 2,
|
||||
text: Math.max(...spreadsheet.values).toLocaleString(),
|
||||
textAlign: "right",
|
||||
});
|
||||
|
||||
return [minYLabel, maxYLabel];
|
||||
};
|
||||
|
||||
const chartLines = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
groupId: string,
|
||||
backgroundColor: string,
|
||||
): ChartElements => {
|
||||
const { chartWidth, chartHeight } = getChartDimentions(spreadsheet);
|
||||
const xLine = newLinearElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "line",
|
||||
x,
|
||||
y,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
width: chartWidth,
|
||||
points: [
|
||||
[0, 0],
|
||||
[chartWidth, 0],
|
||||
],
|
||||
});
|
||||
|
||||
const yLine = newLinearElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "line",
|
||||
x,
|
||||
y,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
height: chartHeight,
|
||||
points: [
|
||||
[0, 0],
|
||||
[0, -chartHeight],
|
||||
],
|
||||
});
|
||||
|
||||
const maxLine = newLinearElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "line",
|
||||
x,
|
||||
y: y - BAR_HEIGHT - BAR_GAP,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
strokeStyle: "dotted",
|
||||
width: chartWidth,
|
||||
opacity: GRID_OPACITY,
|
||||
points: [
|
||||
[0, 0],
|
||||
[chartWidth, 0],
|
||||
],
|
||||
});
|
||||
|
||||
return [xLine, yLine, maxLine];
|
||||
};
|
||||
|
||||
// For the maths behind it https://excalidraw.com/#json=6320864370884608,O_5xfD-Agh32tytHpRJx1g
|
||||
const chartBaseElements = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
groupId: string,
|
||||
backgroundColor: string,
|
||||
debug?: boolean,
|
||||
): ChartElements => {
|
||||
const { chartWidth, chartHeight } = getChartDimentions(spreadsheet);
|
||||
|
||||
const title = spreadsheet.title
|
||||
? newTextElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
text: spreadsheet.title,
|
||||
x: x + chartWidth / 2,
|
||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - maxYLabel.height,
|
||||
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
|
||||
strokeSharpness: "sharp",
|
||||
strokeStyle: "solid",
|
||||
textAlign: "center",
|
||||
})
|
||||
: null;
|
||||
|
||||
trackEvent(EVENT_MAGIC, "chart", "bars", bars.length);
|
||||
const debugRect = debug
|
||||
? newElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "rectangle",
|
||||
x,
|
||||
y: y - chartHeight,
|
||||
width: chartWidth,
|
||||
height: chartHeight,
|
||||
strokeColor: colors.elementStroke[0],
|
||||
fillStyle: "solid",
|
||||
opacity: 6,
|
||||
})
|
||||
: null;
|
||||
|
||||
return [
|
||||
title,
|
||||
...bars,
|
||||
...xLabels,
|
||||
xAxisLine,
|
||||
yAxisLine,
|
||||
maxValueLine,
|
||||
minYLabel,
|
||||
maxYLabel,
|
||||
].filter((element) => element !== null) as ExcalidrawElement[];
|
||||
...(debugRect ? [debugRect] : []),
|
||||
...(title ? [title] : []),
|
||||
...chartXLabels(spreadsheet, x, y, groupId, backgroundColor),
|
||||
...chartYLabels(spreadsheet, x, y, groupId, backgroundColor),
|
||||
...chartLines(spreadsheet, x, y, groupId, backgroundColor),
|
||||
];
|
||||
};
|
||||
|
||||
const chartTypeBar = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
): ChartElements => {
|
||||
const max = Math.max(...spreadsheet.values);
|
||||
const groupId = randomId();
|
||||
const backgroundColor = bgColors[Math.floor(Math.random() * bgColors.length)];
|
||||
|
||||
const bars = spreadsheet.values.map((value, index) => {
|
||||
const barHeight = (value / max) * BAR_HEIGHT;
|
||||
return newElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "rectangle",
|
||||
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP,
|
||||
y: y - barHeight - BAR_GAP,
|
||||
width: BAR_WIDTH,
|
||||
height: barHeight,
|
||||
});
|
||||
});
|
||||
|
||||
return [
|
||||
...bars,
|
||||
...chartBaseElements(
|
||||
spreadsheet,
|
||||
x,
|
||||
y,
|
||||
groupId,
|
||||
backgroundColor,
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const chartTypeLine = (
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
): ChartElements => {
|
||||
const max = Math.max(...spreadsheet.values);
|
||||
const groupId = randomId();
|
||||
const backgroundColor = bgColors[Math.floor(Math.random() * bgColors.length)];
|
||||
|
||||
let index = 0;
|
||||
const points = [];
|
||||
for (const value of spreadsheet.values) {
|
||||
const cx = index * (BAR_WIDTH + BAR_GAP);
|
||||
const cy = -(value / max) * BAR_HEIGHT;
|
||||
points.push([cx, cy]);
|
||||
index++;
|
||||
}
|
||||
|
||||
const maxX = Math.max(...points.map((element) => element[0]));
|
||||
const maxY = Math.max(...points.map((element) => element[1]));
|
||||
const minX = Math.min(...points.map((element) => element[0]));
|
||||
const minY = Math.min(...points.map((element) => element[1]));
|
||||
|
||||
const line = newLinearElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "line",
|
||||
x: x + BAR_GAP + BAR_WIDTH / 2,
|
||||
y: y - BAR_GAP,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
height: maxY - minY,
|
||||
width: maxX - minX,
|
||||
strokeWidth: 2,
|
||||
points: points as any,
|
||||
});
|
||||
|
||||
const dots = spreadsheet.values.map((value, index) => {
|
||||
const cx = index * (BAR_WIDTH + BAR_GAP) + BAR_GAP / 2;
|
||||
const cy = -(value / max) * BAR_HEIGHT + BAR_GAP / 2;
|
||||
return newElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 2,
|
||||
type: "ellipse",
|
||||
x: x + cx + BAR_WIDTH / 2,
|
||||
y: y + cy - BAR_GAP * 2,
|
||||
width: BAR_GAP,
|
||||
height: BAR_GAP,
|
||||
});
|
||||
});
|
||||
|
||||
const lines = spreadsheet.values.map((value, index) => {
|
||||
const cx = index * (BAR_WIDTH + BAR_GAP) + BAR_GAP / 2;
|
||||
const cy = (value / max) * BAR_HEIGHT + BAR_GAP / 2 + BAR_GAP;
|
||||
return newLinearElement({
|
||||
backgroundColor,
|
||||
groupIds: [groupId],
|
||||
...commonProps,
|
||||
type: "line",
|
||||
x: x + cx + BAR_WIDTH / 2 + BAR_GAP / 2,
|
||||
y: y - cy,
|
||||
startArrowhead: null,
|
||||
endArrowhead: null,
|
||||
height: cy,
|
||||
strokeStyle: "dotted",
|
||||
opacity: GRID_OPACITY,
|
||||
points: [
|
||||
[0, 0],
|
||||
[0, cy],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
return [
|
||||
...chartBaseElements(
|
||||
spreadsheet,
|
||||
x,
|
||||
y,
|
||||
groupId,
|
||||
backgroundColor,
|
||||
process.env.NODE_ENV === ENV.DEVELOPMENT,
|
||||
),
|
||||
line,
|
||||
...lines,
|
||||
...dots,
|
||||
];
|
||||
};
|
||||
|
||||
export const renderSpreadsheet = (
|
||||
chartType: string,
|
||||
spreadsheet: Spreadsheet,
|
||||
x: number,
|
||||
y: number,
|
||||
): ChartElements => {
|
||||
trackEvent(EVENT_MAGIC, "chart", chartType, spreadsheet.values.length);
|
||||
if (chartType === "line") {
|
||||
return chartTypeLine(spreadsheet, x, y);
|
||||
}
|
||||
return chartTypeBar(spreadsheet, x, y);
|
||||
};
|
||||
|
||||
+188
-174
@@ -1,181 +1,168 @@
|
||||
import { Point, simplify } from "points-on-curve";
|
||||
import React from "react";
|
||||
|
||||
import rough from "roughjs/bin/rough";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import { simplify, Point } from "points-on-curve";
|
||||
|
||||
import {
|
||||
newElement,
|
||||
newTextElement,
|
||||
duplicateElement,
|
||||
isInvisiblySmallElement,
|
||||
isTextElement,
|
||||
textWysiwyg,
|
||||
getCommonBounds,
|
||||
getCursorForResizingElement,
|
||||
getPerfectElementSize,
|
||||
getNormalizedDimensions,
|
||||
newLinearElement,
|
||||
transformElements,
|
||||
getElementWithTransformHandleType,
|
||||
getResizeOffsetXY,
|
||||
getResizeArrowDirection,
|
||||
getTransformHandleTypeFromCoords,
|
||||
isNonDeletedElement,
|
||||
updateTextElement,
|
||||
dragSelectedElements,
|
||||
getDragOffsetXY,
|
||||
dragNewElement,
|
||||
hitTest,
|
||||
isHittingElementBoundingBoxWithoutHittingElement,
|
||||
getNonDeletedElements,
|
||||
} from "../element";
|
||||
import {
|
||||
getElementsWithinSelection,
|
||||
isOverScrollBars,
|
||||
getElementsAtPosition,
|
||||
getElementContainingPosition,
|
||||
getNormalizedZoom,
|
||||
getSelectedElements,
|
||||
isSomeElementSelected,
|
||||
calculateScrollCenter,
|
||||
} from "../scene";
|
||||
import { loadFromBlob, exportCanvas } from "../data";
|
||||
|
||||
import { renderScene } from "../renderer";
|
||||
import {
|
||||
AppState,
|
||||
GestureEvent,
|
||||
Gesture,
|
||||
ExcalidrawProps,
|
||||
SceneData,
|
||||
} from "../types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
NonDeleted,
|
||||
ExcalidrawGenericElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawBindableElement,
|
||||
} from "../element/types";
|
||||
|
||||
import { distance2d, isPathALoop, getGridPoint } from "../math";
|
||||
|
||||
import {
|
||||
isWritableElement,
|
||||
isInputLike,
|
||||
isToolIcon,
|
||||
debounce,
|
||||
distance,
|
||||
resetCursor,
|
||||
viewportCoordsToSceneCoords,
|
||||
sceneCoordsToViewportCoords,
|
||||
setCursorForShape,
|
||||
tupleToCoors,
|
||||
ResolvablePromise,
|
||||
resolvablePromise,
|
||||
withBatchedUpdates,
|
||||
} from "../utils";
|
||||
import {
|
||||
KEYS,
|
||||
isArrowKey,
|
||||
getResizeCenterPointKey,
|
||||
getResizeWithSidesSameLengthKey,
|
||||
getRotateWithDiscreteAngleKey,
|
||||
CODES,
|
||||
} from "../keys";
|
||||
|
||||
import { findShapeByKey } from "../shapes";
|
||||
import { createHistory, SceneHistory } from "../history";
|
||||
|
||||
import ContextMenu from "./ContextMenu";
|
||||
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import "../actions";
|
||||
import { actionDeleteSelected, actionFinalize } from "../actions";
|
||||
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { actions } from "../actions/register";
|
||||
|
||||
import { ActionResult } from "../actions/types";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { t, getLanguage } from "../i18n";
|
||||
|
||||
import {
|
||||
copyToClipboard,
|
||||
parseClipboard,
|
||||
probablySupportsClipboardBlob,
|
||||
probablySupportsClipboardWriteText,
|
||||
} from "../clipboard";
|
||||
import { normalizeScroll } from "../scene";
|
||||
import { getCenter, getDistance } from "../gesture";
|
||||
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
|
||||
|
||||
import {
|
||||
CURSOR_TYPE,
|
||||
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
||||
ELEMENT_TRANSLATE_AMOUNT,
|
||||
POINTER_BUTTON,
|
||||
DRAGGING_THRESHOLD,
|
||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||
LINE_CONFIRM_THRESHOLD,
|
||||
EVENT,
|
||||
ENV,
|
||||
CANVAS_ONLY_ACTIONS,
|
||||
DEFAULT_VERTICAL_ALIGN,
|
||||
GRID_SIZE,
|
||||
MIME_TYPES,
|
||||
TAP_TWICE_TIMEOUT,
|
||||
TOUCH_CTX_MENU_TIMEOUT,
|
||||
APP_NAME,
|
||||
} from "../constants";
|
||||
|
||||
import LayerUI from "./LayerUI";
|
||||
import { ScrollBars, SceneState } from "../scene/types";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import {
|
||||
isLinearElement,
|
||||
isLinearElementType,
|
||||
isBindingElement,
|
||||
isBindingElementType,
|
||||
} from "../element/typeChecks";
|
||||
import { actionFinalize, actionDeleteSelected } from "../actions";
|
||||
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import {
|
||||
getSelectedGroupIds,
|
||||
isSelectedViaGroup,
|
||||
selectGroupsForSelectedElements,
|
||||
isElementInGroup,
|
||||
getSelectedGroupIdForElement,
|
||||
getElementsInGroup,
|
||||
editGroupForSelectedElement,
|
||||
} from "../groups";
|
||||
import { Library } from "../data/library";
|
||||
import Scene from "../scene/Scene";
|
||||
import {
|
||||
getHoveredElementForBinding,
|
||||
maybeBindLinearElement,
|
||||
getEligibleElementsForBinding,
|
||||
bindOrUnbindSelectedElements,
|
||||
unbindLinearElements,
|
||||
fixBindingsAfterDuplication,
|
||||
fixBindingsAfterDeletion,
|
||||
isLinearElementSimpleAndAlreadyBound,
|
||||
isBindingEnabled,
|
||||
updateBoundElements,
|
||||
shouldEnableBindingForPointerEvent,
|
||||
} from "../element/binding";
|
||||
import { MaybeTransformHandleType } from "../element/transformHandles";
|
||||
import { deepCopyElement } from "../element/newElement";
|
||||
import { renderSpreadsheet } from "../charts";
|
||||
import { isValidLibrary } from "../data/json";
|
||||
import { getNewZoom } from "../scene/zoom";
|
||||
import { restore } from "../data/restore";
|
||||
import {
|
||||
EVENT_DIALOG,
|
||||
EVENT_LIBRARY,
|
||||
EVENT_SHAPE,
|
||||
trackEvent,
|
||||
} from "../analytics";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import {
|
||||
copyToClipboard,
|
||||
parseClipboard,
|
||||
probablySupportsClipboardBlob,
|
||||
probablySupportsClipboardWriteText,
|
||||
} from "../clipboard";
|
||||
import {
|
||||
APP_NAME,
|
||||
CANVAS_ONLY_ACTIONS,
|
||||
CURSOR_TYPE,
|
||||
DEFAULT_VERTICAL_ALIGN,
|
||||
DRAGGING_THRESHOLD,
|
||||
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
|
||||
ELEMENT_TRANSLATE_AMOUNT,
|
||||
ENV,
|
||||
EVENT,
|
||||
GRID_SIZE,
|
||||
LINE_CONFIRM_THRESHOLD,
|
||||
MIME_TYPES,
|
||||
POINTER_BUTTON,
|
||||
TAP_TWICE_TIMEOUT,
|
||||
TEXT_TO_CENTER_SNAP_THRESHOLD,
|
||||
TOUCH_CTX_MENU_TIMEOUT,
|
||||
} from "../constants";
|
||||
import { exportCanvas, loadFromBlob } from "../data";
|
||||
import { isValidLibrary } from "../data/json";
|
||||
import { Library } from "../data/library";
|
||||
import { restore } from "../data/restore";
|
||||
import {
|
||||
dragNewElement,
|
||||
dragSelectedElements,
|
||||
duplicateElement,
|
||||
getCommonBounds,
|
||||
getCursorForResizingElement,
|
||||
getDragOffsetXY,
|
||||
getElementWithTransformHandleType,
|
||||
getNonDeletedElements,
|
||||
getNormalizedDimensions,
|
||||
getPerfectElementSize,
|
||||
getResizeArrowDirection,
|
||||
getResizeOffsetXY,
|
||||
getTransformHandleTypeFromCoords,
|
||||
hitTest,
|
||||
isHittingElementBoundingBoxWithoutHittingElement,
|
||||
isInvisiblySmallElement,
|
||||
isNonDeletedElement,
|
||||
isTextElement,
|
||||
newElement,
|
||||
newLinearElement,
|
||||
newTextElement,
|
||||
textWysiwyg,
|
||||
transformElements,
|
||||
updateTextElement,
|
||||
} from "../element";
|
||||
import {
|
||||
bindOrUnbindSelectedElements,
|
||||
fixBindingsAfterDeletion,
|
||||
fixBindingsAfterDuplication,
|
||||
getEligibleElementsForBinding,
|
||||
getHoveredElementForBinding,
|
||||
isBindingEnabled,
|
||||
isLinearElementSimpleAndAlreadyBound,
|
||||
maybeBindLinearElement,
|
||||
shouldEnableBindingForPointerEvent,
|
||||
unbindLinearElements,
|
||||
updateBoundElements,
|
||||
} from "../element/binding";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import { deepCopyElement } from "../element/newElement";
|
||||
import { MaybeTransformHandleType } from "../element/transformHandles";
|
||||
import {
|
||||
isBindingElement,
|
||||
isBindingElementType,
|
||||
isLinearElement,
|
||||
isLinearElementType,
|
||||
} from "../element/typeChecks";
|
||||
import {
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawGenericElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElement,
|
||||
NonDeleted,
|
||||
} from "../element/types";
|
||||
import { getCenter, getDistance } from "../gesture";
|
||||
import {
|
||||
editGroupForSelectedElement,
|
||||
getElementsInGroup,
|
||||
getSelectedGroupIdForElement,
|
||||
getSelectedGroupIds,
|
||||
isElementInGroup,
|
||||
isSelectedViaGroup,
|
||||
selectGroupsForSelectedElements,
|
||||
} from "../groups";
|
||||
import { createHistory, SceneHistory } from "../history";
|
||||
import { t, getLanguage, setLanguage, languages, defaultLang } from "../i18n";
|
||||
import {
|
||||
CODES,
|
||||
getResizeCenterPointKey,
|
||||
getResizeWithSidesSameLengthKey,
|
||||
getRotateWithDiscreteAngleKey,
|
||||
isArrowKey,
|
||||
KEYS,
|
||||
} from "../keys";
|
||||
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||
import { renderScene } from "../renderer";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import {
|
||||
calculateScrollCenter,
|
||||
getElementContainingPosition,
|
||||
getElementsAtPosition,
|
||||
getElementsWithinSelection,
|
||||
getNormalizedZoom,
|
||||
getSelectedElements,
|
||||
isOverScrollBars,
|
||||
isSomeElementSelected,
|
||||
normalizeScroll,
|
||||
} from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
import { SceneState, ScrollBars } from "../scene/types";
|
||||
import { getNewZoom } from "../scene/zoom";
|
||||
import { findShapeByKey } from "../shapes";
|
||||
import {
|
||||
AppState,
|
||||
ExcalidrawProps,
|
||||
Gesture,
|
||||
GestureEvent,
|
||||
SceneData,
|
||||
} from "../types";
|
||||
import {
|
||||
debounce,
|
||||
distance,
|
||||
getNewSceneName,
|
||||
isInputLike,
|
||||
isToolIcon,
|
||||
isWritableElement,
|
||||
resetCursor,
|
||||
ResolvablePromise,
|
||||
resolvablePromise,
|
||||
sceneCoordsToViewportCoords,
|
||||
setCursorForShape,
|
||||
tupleToCoors,
|
||||
viewportCoordsToSceneCoords,
|
||||
withBatchedUpdates,
|
||||
} from "../utils";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import LayerUI from "./LayerUI";
|
||||
import { Stats } from "./Stats";
|
||||
|
||||
const { history } = createHistory();
|
||||
@@ -295,6 +282,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
} = props;
|
||||
this.state = {
|
||||
...defaultAppState,
|
||||
name: getNewSceneName(),
|
||||
isLoading: true,
|
||||
width,
|
||||
height,
|
||||
@@ -346,7 +334,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
offsetLeft,
|
||||
} = this.state;
|
||||
|
||||
const { onCollabButtonClick, onExportToBackend } = this.props;
|
||||
const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props;
|
||||
const canvasScale = window.devicePixelRatio;
|
||||
|
||||
const canvasWidth = canvasDOMWidth * canvasScale;
|
||||
@@ -374,7 +362,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
elements={this.scene.getElements()}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={this.toggleLock}
|
||||
onInsertShape={(elements) =>
|
||||
onInsertElements={(elements) =>
|
||||
this.addElementsFromPasteOrLibrary(
|
||||
elements,
|
||||
DEFAULT_PASTE_X,
|
||||
@@ -383,9 +371,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
}
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
toggleZenMode={this.toggleZenMode}
|
||||
lng={getLanguage().lng}
|
||||
langCode={getLanguage().code}
|
||||
isCollaborating={this.props.isCollaborating || false}
|
||||
onExportToBackend={onExportToBackend}
|
||||
renderCustomFooter={renderFooter}
|
||||
/>
|
||||
{this.state.showStats && (
|
||||
<Stats
|
||||
@@ -541,6 +530,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
this.scene.replaceAllElements([]);
|
||||
this.setState((state) => ({
|
||||
...getDefaultAppState(),
|
||||
name: getNewSceneName(),
|
||||
isLoading: opts?.resetLoadingState ? false : state.isLoading,
|
||||
appearance: this.state.appearance,
|
||||
}));
|
||||
@@ -752,6 +742,10 @@ class App extends React.Component<ExcalidrawProps, 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 ||
|
||||
@@ -875,7 +869,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
|
||||
history.record(this.state, this.scene.getElementsIncludingDeleted());
|
||||
|
||||
this.props.onChange?.(this.scene.getElementsIncludingDeleted(), this.state);
|
||||
// Do not notify consumers if we're still loading the scene. Among other
|
||||
// potential issues, this fixes a case where the tab isn't focused during
|
||||
// init, which would trigger onChange with empty elements, which would then
|
||||
// override whatever is in localStorage currently.
|
||||
if (!this.state.isLoading) {
|
||||
this.props.onChange?.(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
this.state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy/paste
|
||||
@@ -1004,9 +1007,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
if (data.errorMessage) {
|
||||
this.setState({ errorMessage: data.errorMessage });
|
||||
} else if (data.spreadsheet) {
|
||||
this.addElementsFromPasteOrLibrary(
|
||||
renderSpreadsheet(data.spreadsheet, cursorX, cursorY),
|
||||
);
|
||||
this.setState({
|
||||
pasteDialog: {
|
||||
data: data.spreadsheet,
|
||||
shown: true,
|
||||
},
|
||||
});
|
||||
} else if (data.elements) {
|
||||
this.addElementsFromPasteOrLibrary(data.elements);
|
||||
} else if (data.text) {
|
||||
@@ -3851,6 +3857,14 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
||||
offsetTop: typeof offsets?.offsetTop === "number" ? offsets.offsetTop : 0,
|
||||
};
|
||||
}
|
||||
|
||||
private async updateLanguage() {
|
||||
const currentLang =
|
||||
languages.find((lang) => lang.code === this.props.langCode) ||
|
||||
defaultLang;
|
||||
await setLanguage(currentLang);
|
||||
this.setAppState({});
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -218,7 +218,7 @@
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
@media #{$media-query} {
|
||||
@media #{$is-mobile-query} {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "open-color/open-color.scss";
|
||||
@import "../css/_variables";
|
||||
|
||||
.excalidraw {
|
||||
.context-menu {
|
||||
@@ -42,16 +42,16 @@
|
||||
}
|
||||
|
||||
&.dangerous {
|
||||
div:nth-child(1) {
|
||||
.context-menu-option__label {
|
||||
color: $oc-red-7;
|
||||
}
|
||||
}
|
||||
|
||||
div:nth-child(1) {
|
||||
.context-menu-option__label {
|
||||
justify-self: start;
|
||||
margin-inline-end: 20px;
|
||||
}
|
||||
div:nth-child(2) {
|
||||
.context-menu-option__shortcut {
|
||||
justify-self: end;
|
||||
opacity: 0.6;
|
||||
font-size: 0.7rem;
|
||||
@@ -63,7 +63,7 @@
|
||||
background-color: var(--select-highlight-color);
|
||||
|
||||
&.dangerous {
|
||||
div:nth-child(1) {
|
||||
.context-menu-option__label {
|
||||
color: var(--popup-background-color);
|
||||
}
|
||||
background-color: $oc-red-6;
|
||||
@@ -73,4 +73,18 @@
|
||||
.context-menu-option:focus {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
.context-menu-option {
|
||||
display: block;
|
||||
|
||||
.context-menu-option__label {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
.context-menu-option__shortcut {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
|
||||
${checked ? "checkmark" : ""}`}
|
||||
onClick={action}
|
||||
>
|
||||
<div>{label}</div>
|
||||
<div>
|
||||
<div className="context-menu-option__label">{label}</div>
|
||||
<div className="context-menu-option__shortcut">
|
||||
{shortcutName
|
||||
? getShortcutFromShortcutName(shortcutName)
|
||||
: ""}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
margin-top: 0;
|
||||
grid-template-columns: 1fr calc(var(--space-factor) * 7);
|
||||
grid-gap: var(--metric);
|
||||
padding: calc(var(--space-factor) * 2);
|
||||
text-align: center;
|
||||
font-variant: small-caps;
|
||||
}
|
||||
|
||||
.Dialog__titleContent {
|
||||
@@ -18,7 +21,11 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media #{$media-query} {
|
||||
.Dialog__content {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
.Dialog {
|
||||
--metric: calc(var(--space-factor) * 4);
|
||||
--inset-left: #{"max(var(--metric), var(--sal))"};
|
||||
@@ -30,13 +37,8 @@
|
||||
var(--space-factor) * 7
|
||||
);
|
||||
position: sticky;
|
||||
top: calc(-1 * var(--metric));
|
||||
margin: calc(-1 * var(--inset-right));
|
||||
margin-top: calc(-1 * var(--metric));
|
||||
margin-bottom: var(--metric);
|
||||
top: 0;
|
||||
padding: calc(var(--space-factor) * 2);
|
||||
padding-left: var(--inset-left);
|
||||
padding-right: var(--inset-right);
|
||||
background: var(--bg-color-island);
|
||||
font-size: 1.25em;
|
||||
|
||||
|
||||
+13
-13
@@ -1,13 +1,12 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { Modal } from "./Modal";
|
||||
import { Island } from "./Island";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { back, close } from "./icons";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
import "./Dialog.scss";
|
||||
import { back, close } from "./icons";
|
||||
import { Island } from "./Island";
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
const useRefState = <T,>() => {
|
||||
const [refValue, setRefValue] = useState<T | null>(null);
|
||||
@@ -20,9 +19,10 @@ const useRefState = <T,>() => {
|
||||
export const Dialog = (props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
maxWidth?: number;
|
||||
small?: boolean;
|
||||
onCloseRequest(): void;
|
||||
title: React.ReactNode;
|
||||
autofocus?: boolean;
|
||||
}) => {
|
||||
const [islandNode, setIslandNode] = useRefState<HTMLDivElement>();
|
||||
|
||||
@@ -33,7 +33,7 @@ export const Dialog = (props: {
|
||||
|
||||
const focusableElements = queryFocusableElements(islandNode);
|
||||
|
||||
if (focusableElements.length > 0) {
|
||||
if (focusableElements.length > 0 && props.autofocus !== false) {
|
||||
// If there's an element other than close, focus it.
|
||||
(focusableElements[1] || focusableElements[0]).focus();
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export const Dialog = (props: {
|
||||
islandNode.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => islandNode.removeEventListener("keydown", handleKeyDown);
|
||||
}, [islandNode]);
|
||||
}, [islandNode, props.autofocus]);
|
||||
|
||||
const queryFocusableElements = (node: HTMLElement) => {
|
||||
const focusableElements = node.querySelectorAll<HTMLElement>(
|
||||
@@ -76,11 +76,11 @@ export const Dialog = (props: {
|
||||
<Modal
|
||||
className={clsx("Dialog", props.className)}
|
||||
labelledBy="dialog-title"
|
||||
maxWidth={props.maxWidth}
|
||||
maxWidth={props.small ? 550 : 800}
|
||||
onCloseRequest={props.onCloseRequest}
|
||||
>
|
||||
<Island padding={4} ref={setIslandNode}>
|
||||
<h2 id="dialog-title" className="Dialog__title">
|
||||
<Island ref={setIslandNode}>
|
||||
<h3 id="dialog-title" className="Dialog__title">
|
||||
<span className="Dialog__titleContent">{props.title}</span>
|
||||
<button
|
||||
className="Modal__close"
|
||||
@@ -89,8 +89,8 @@ export const Dialog = (props: {
|
||||
>
|
||||
{useIsMobile() ? back : close}
|
||||
</button>
|
||||
</h2>
|
||||
{props.children}
|
||||
</h3>
|
||||
<div className="Dialog__content">{props.children}</div>
|
||||
</Island>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ export const ErrorDialog = ({
|
||||
<>
|
||||
{modalIsShown && (
|
||||
<Dialog
|
||||
maxWidth={500}
|
||||
small
|
||||
onCloseRequest={handleClose}
|
||||
title={t("errorDialog.title")}
|
||||
>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
@media #{$is-mobile-query} {
|
||||
.ExportDialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -51,9 +51,7 @@
|
||||
.ExportDialog__actions > * {
|
||||
margin-bottom: calc(var(--space-factor) * 3);
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$media-query} {
|
||||
.ExportDialog__preview canvas {
|
||||
max-height: 30vh;
|
||||
}
|
||||
|
||||
@@ -262,11 +262,7 @@ export const ExportDialog = ({
|
||||
ref={triggerButton}
|
||||
/>
|
||||
{modalIsShown && (
|
||||
<Dialog
|
||||
maxWidth={800}
|
||||
onCloseRequest={handleClose}
|
||||
title={t("buttons.export")}
|
||||
>
|
||||
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
|
||||
<ExportModal
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
@import "../css/_variables";
|
||||
|
||||
// this is loosely based on the longest hint text
|
||||
$wide-viewport-width: 1000px;
|
||||
|
||||
.excalidraw {
|
||||
.HintViewer {
|
||||
pointer-events: none;
|
||||
@@ -16,12 +19,9 @@
|
||||
color: $oc-gray-6;
|
||||
font-size: 0.8rem;
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@media #{$media-query} {
|
||||
@media #{$is-mobile-query} {
|
||||
position: static;
|
||||
padding-right: 2em;
|
||||
}
|
||||
|
||||
> span {
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
:root[dir="rtl"] & {
|
||||
left: 2px;
|
||||
}
|
||||
@media #{$media-query} {
|
||||
@media #{$is-mobile-query} {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import React from "react";
|
||||
|
||||
import { LoadingMessage } from "./LoadingMessage";
|
||||
import { setLanguageFirstTime } from "../i18n";
|
||||
import {
|
||||
defaultLang,
|
||||
Language,
|
||||
languages,
|
||||
setLanguageFirstTime,
|
||||
} from "../i18n";
|
||||
|
||||
export class InitializeApp extends React.Component<
|
||||
any,
|
||||
{ isLoading: boolean }
|
||||
> {
|
||||
interface Props {
|
||||
langCode: Language["code"];
|
||||
}
|
||||
interface State {
|
||||
isLoading: boolean;
|
||||
}
|
||||
export class InitializeApp extends React.Component<Props, State> {
|
||||
public state: { isLoading: boolean } = {
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
await setLanguageFirstTime();
|
||||
const currentLang =
|
||||
languages.find((lang) => lang.code === this.props.langCode) ||
|
||||
defaultLang;
|
||||
await setLanguageFirstTime(currentLang);
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
background-color: var(--bg-color-island);
|
||||
backdrop-filter: saturate(100%) blur(10px);
|
||||
box-shadow: var(--shadow-island);
|
||||
border-radius: var(--border-radius-m);
|
||||
border-radius: 4px;
|
||||
padding: calc(var(--padding) * var(--space-factor));
|
||||
position: relative;
|
||||
transition: box-shadow 0.5s ease-in-out;
|
||||
|
||||
@@ -7,11 +7,23 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.browse-libraries {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 16px;
|
||||
white-space: nowrap;
|
||||
.layer-ui__library-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 2px 0;
|
||||
|
||||
button {
|
||||
// 2px from the left to account for focus border of left-most button
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
a {
|
||||
margin-left: auto;
|
||||
// 17px for scrollbar (needed for overlay scrollbars on Big Sur?) + 1px extra
|
||||
padding-right: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+83
-75
@@ -19,8 +19,7 @@ import { FixedSideContainer } from "./FixedSideContainer";
|
||||
import { UserList } from "./UserList";
|
||||
import { LockIcon } from "./LockIcon";
|
||||
import { ExportDialog, ExportCB } from "./ExportDialog";
|
||||
import { LanguageList } from "./LanguageList";
|
||||
import { t, languages, setLanguage } from "../i18n";
|
||||
import { Language, t } from "../i18n";
|
||||
import { HintViewer } from "./HintViewer";
|
||||
import useIsMobile from "../is-mobile";
|
||||
|
||||
@@ -51,6 +50,7 @@ import {
|
||||
EVENT_LIBRARY,
|
||||
trackEvent,
|
||||
} from "../analytics";
|
||||
import { PasteChartDialog } from "./PasteChartDialog";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
@@ -60,16 +60,17 @@ interface LayerUIProps {
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onCollabButtonClick?: () => void;
|
||||
onLockToggle: () => void;
|
||||
onInsertShape: (elements: LibraryItem) => void;
|
||||
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
||||
zenModeEnabled: boolean;
|
||||
toggleZenMode: () => void;
|
||||
lng: string;
|
||||
langCode: Language["code"];
|
||||
isCollaborating: boolean;
|
||||
onExportToBackend?: (
|
||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
) => void;
|
||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||
}
|
||||
|
||||
const useOnClickOutside = (
|
||||
@@ -123,9 +124,42 @@ const LibraryMenuItems = ({
|
||||
let addedPendingElements = false;
|
||||
|
||||
rows.push(
|
||||
<>
|
||||
<div className="layer-ui__library-header">
|
||||
<ToolButton
|
||||
key="import"
|
||||
type="button"
|
||||
title={t("buttons.load")}
|
||||
aria-label={t("buttons.load")}
|
||||
icon={load}
|
||||
onClick={() => {
|
||||
importLibraryFromJSON()
|
||||
.then(() => {
|
||||
// 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 });
|
||||
})
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
setAppState({ errorMessage: error.message });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ToolButton
|
||||
key="export"
|
||||
type="button"
|
||||
title={t("buttons.export")}
|
||||
aria-label={t("buttons.export")}
|
||||
icon={exportFile}
|
||||
onClick={() => {
|
||||
saveLibraryAsJSON()
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
setAppState({ errorMessage: error.message });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<a
|
||||
className="browse-libraries"
|
||||
href="https://libraries.excalidraw.com"
|
||||
target="_excalidraw_libraries"
|
||||
onClick={() => {
|
||||
@@ -134,48 +168,7 @@ const LibraryMenuItems = ({
|
||||
>
|
||||
{t("labels.libraries")}
|
||||
</a>
|
||||
|
||||
<Stack.Row
|
||||
align="center"
|
||||
gap={1}
|
||||
key={"actions"}
|
||||
style={{ padding: "2px" }}
|
||||
>
|
||||
<ToolButton
|
||||
key="import"
|
||||
type="button"
|
||||
title={t("buttons.load")}
|
||||
aria-label={t("buttons.load")}
|
||||
icon={load}
|
||||
onClick={() => {
|
||||
importLibraryFromJSON()
|
||||
.then(() => {
|
||||
// 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 });
|
||||
})
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
setAppState({ errorMessage: error.message });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ToolButton
|
||||
key="export"
|
||||
type="button"
|
||||
title={t("buttons.export")}
|
||||
aria-label={t("buttons.export")}
|
||||
icon={exportFile}
|
||||
onClick={() => {
|
||||
saveLibraryAsJSON()
|
||||
.catch(muteFSAbortError)
|
||||
.catch((error) => {
|
||||
setAppState({ errorMessage: error.message });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack.Row>
|
||||
</>,
|
||||
</div>,
|
||||
);
|
||||
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
@@ -318,11 +311,12 @@ const LayerUI = ({
|
||||
elements,
|
||||
onCollabButtonClick,
|
||||
onLockToggle,
|
||||
onInsertShape,
|
||||
onInsertElements,
|
||||
zenModeEnabled,
|
||||
toggleZenMode,
|
||||
isCollaborating,
|
||||
onExportToBackend,
|
||||
renderCustomFooter,
|
||||
}: LayerUIProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@@ -456,7 +450,7 @@ const LayerUI = ({
|
||||
<LibraryMenu
|
||||
pendingElements={getSelectedElements(elements, appState)}
|
||||
onClickOutside={closeLibrary}
|
||||
onInsertShape={onInsertShape}
|
||||
onInsertShape={onInsertElements}
|
||||
onAddToLibrary={deselectItems}
|
||||
setAppState={setAppState}
|
||||
/>
|
||||
@@ -558,14 +552,7 @@ const LayerUI = ({
|
||||
"transition-right disable-pointerEvents": zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<LanguageList
|
||||
onChange={async (lng) => {
|
||||
await setLanguage(lng);
|
||||
setAppState({});
|
||||
}}
|
||||
languages={languages}
|
||||
floating
|
||||
/>
|
||||
{renderCustomFooter?.(false)}
|
||||
{actionManager.renderAction("toggleShortcuts")}
|
||||
</div>
|
||||
<button
|
||||
@@ -592,21 +579,8 @@ const LayerUI = ({
|
||||
</footer>
|
||||
);
|
||||
|
||||
return isMobile ? (
|
||||
<MobileMenu
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
libraryMenu={libraryMenu}
|
||||
exportButton={renderExportDialog()}
|
||||
setAppState={setAppState}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={onLockToggle}
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
/>
|
||||
) : (
|
||||
<div className="layer-ui__wrapper">
|
||||
const dialogs = (
|
||||
<>
|
||||
{appState.isLoading && <LoadingMessage />}
|
||||
{appState.errorMessage && (
|
||||
<ErrorDialog
|
||||
@@ -619,6 +593,41 @@ const LayerUI = ({
|
||||
onClose={() => setAppState({ showShortcutsDialog: false })}
|
||||
/>
|
||||
)}
|
||||
{appState.pasteDialog.shown && (
|
||||
<PasteChartDialog
|
||||
setAppState={setAppState}
|
||||
appState={appState}
|
||||
onInsertChart={onInsertElements}
|
||||
onClose={() =>
|
||||
setAppState({
|
||||
pasteDialog: { shown: false, data: null },
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return isMobile ? (
|
||||
<>
|
||||
{dialogs}
|
||||
<MobileMenu
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
actionManager={actionManager}
|
||||
libraryMenu={libraryMenu}
|
||||
exportButton={renderExportDialog()}
|
||||
setAppState={setAppState}
|
||||
onCollabButtonClick={onCollabButtonClick}
|
||||
onLockToggle={onLockToggle}
|
||||
canvas={canvas}
|
||||
isCollaborating={isCollaborating}
|
||||
renderCustomFooter={renderCustomFooter}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="layer-ui__wrapper">
|
||||
{dialogs}
|
||||
{renderFixedSideContainer()}
|
||||
{renderBottomAppMenu()}
|
||||
{
|
||||
@@ -651,9 +660,8 @@ const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
|
||||
const nextAppState = getNecessaryObj(next.appState);
|
||||
|
||||
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
||||
|
||||
return (
|
||||
prev.lng === next.lng &&
|
||||
prev.langCode === next.langCode &&
|
||||
prev.elements === next.elements &&
|
||||
keys.every((key) => prevAppState[key] === nextAppState[key])
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import oc from "open-color";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { close } from "../components/icons";
|
||||
|
||||
import "./LibraryUnit.scss";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import useIsMobile from "../is-mobile";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import { LibraryItem } from "../types";
|
||||
import { MIME_TYPES } from "../constants";
|
||||
import "./LibraryUnit.scss";
|
||||
|
||||
// fa-plus
|
||||
const PLUS_ICON = (
|
||||
@@ -38,7 +38,7 @@ export const LibraryUnit = ({
|
||||
}
|
||||
const svg = exportToSvg(elementsToRender, {
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: "#fff",
|
||||
viewBackgroundColor: oc.white,
|
||||
shouldAddWatermark: false,
|
||||
});
|
||||
for (const child of ref.current!.children) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from "react";
|
||||
import { AppState } from "../types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { t, setLanguage } from "../i18n";
|
||||
import { t } from "../i18n";
|
||||
import Stack from "./Stack";
|
||||
import { LanguageList } from "./LanguageList";
|
||||
import { showSelectedShapeActions } from "../element";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { FixedSideContainer } from "./FixedSideContainer";
|
||||
@@ -15,7 +14,6 @@ import { Section } from "./Section";
|
||||
import CollabButton from "./CollabButton";
|
||||
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
|
||||
import { LockIcon } from "./LockIcon";
|
||||
import { LoadingMessage } from "./LoadingMessage";
|
||||
import { UserList } from "./UserList";
|
||||
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
||||
import { EVENT_ACTION, trackEvent } from "../analytics";
|
||||
@@ -31,6 +29,7 @@ type MobileMenuProps = {
|
||||
onLockToggle: () => void;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
isCollaborating: boolean;
|
||||
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
||||
};
|
||||
|
||||
export const MobileMenu = ({
|
||||
@@ -44,9 +43,9 @@ export const MobileMenu = ({
|
||||
onLockToggle,
|
||||
canvas,
|
||||
isCollaborating,
|
||||
renderCustomFooter,
|
||||
}: MobileMenuProps) => (
|
||||
<>
|
||||
{appState.isLoading && <LoadingMessage />}
|
||||
<FixedSideContainer side="top">
|
||||
<Section heading="shapes">
|
||||
{(heading) => (
|
||||
@@ -104,15 +103,7 @@ export const MobileMenu = ({
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
/>
|
||||
<fieldset>
|
||||
<legend>{t("labels.language")}</legend>
|
||||
<LanguageList
|
||||
onChange={async (lng) => {
|
||||
await setLanguage(lng);
|
||||
setAppState({});
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
{renderCustomFooter?.(true)}
|
||||
<fieldset>
|
||||
<legend>{t("labels.collaborators")}</legend>
|
||||
<UserList mobile>
|
||||
|
||||
@@ -30,18 +30,26 @@
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
max-height: 100%;
|
||||
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
animation: Modal__content_fade-in 0.1s ease-out 0.05s forwards;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
// for modals, reset blurry bg
|
||||
background: var(--bg-color-island);
|
||||
backdrop-filter: none;
|
||||
|
||||
@media #{$media-query} {
|
||||
border: 1px solid var(--dialog-border);
|
||||
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
|
||||
border-radius: 6px;
|
||||
|
||||
@media #{$is-mobile-query} {
|
||||
max-width: 100%;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,13 +76,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.Modal__close--floating {
|
||||
position: absolute;
|
||||
right: calc(var(--space-factor) * 5);
|
||||
top: calc(var(--space-factor) * 5);
|
||||
}
|
||||
|
||||
@media #{$media-query} {
|
||||
@media #{$is-mobile-query} {
|
||||
.Modal {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -36,11 +36,7 @@ export const Modal = (props: {
|
||||
<div className="Modal__background" onClick={props.onCloseRequest}></div>
|
||||
<div
|
||||
className="Modal__content"
|
||||
style={{
|
||||
"--max-width": `${props.maxWidth}px`,
|
||||
maxHeight: "100%",
|
||||
overflowY: "scroll",
|
||||
}}
|
||||
style={{ "--max-width": `${props.maxWidth}px` }}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
@import "../css/_variables";
|
||||
|
||||
.excalidraw {
|
||||
.PasteChartDialog {
|
||||
@media #{$is-mobile-query} {
|
||||
.Island {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
@media #{$is-mobile-query} {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.ChartPreview {
|
||||
margin: 8px;
|
||||
text-align: center;
|
||||
width: 192px;
|
||||
height: 128px;
|
||||
border-radius: 2px;
|
||||
padding: 1px;
|
||||
border: 1px solid $oc-gray-4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
svg {
|
||||
max-height: 120px;
|
||||
max-width: 186px;
|
||||
}
|
||||
&:hover {
|
||||
padding: 0;
|
||||
border: 2px solid $oc-blue-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import oc from "open-color";
|
||||
import React, { useLayoutEffect, useRef, useState } from "react";
|
||||
import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts";
|
||||
import { ChartType } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import { AppState, LibraryItem } from "../types";
|
||||
import { Dialog } from "./Dialog";
|
||||
import "./PasteChartDialog.scss";
|
||||
|
||||
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
|
||||
|
||||
const ChartPreviewBtn = (props: {
|
||||
spreadsheet: Spreadsheet | null;
|
||||
chartType: ChartType;
|
||||
selected: boolean;
|
||||
onClick: OnInsertChart;
|
||||
}) => {
|
||||
const previewRef = useRef<HTMLDivElement | null>(null);
|
||||
const [chartElements, setChartElements] = useState<ChartElements | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!props.spreadsheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = renderSpreadsheet(
|
||||
props.chartType,
|
||||
props.spreadsheet,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
setChartElements(elements);
|
||||
|
||||
const svg = exportToSvg(elements, {
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: oc.white,
|
||||
shouldAddWatermark: false,
|
||||
});
|
||||
|
||||
const previewNode = previewRef.current!;
|
||||
|
||||
previewNode.appendChild(svg);
|
||||
|
||||
if (props.selected) {
|
||||
(previewNode.parentNode as HTMLDivElement).focus();
|
||||
}
|
||||
|
||||
return () => {
|
||||
previewNode.removeChild(svg);
|
||||
};
|
||||
}, [props.spreadsheet, props.chartType, props.selected]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className="ChartPreview"
|
||||
onClick={() => {
|
||||
if (chartElements) {
|
||||
props.onClick(props.chartType, chartElements);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div ref={previewRef} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const PasteChartDialog = ({
|
||||
setAppState,
|
||||
appState,
|
||||
onClose,
|
||||
onInsertChart,
|
||||
}: {
|
||||
appState: AppState;
|
||||
onClose: () => void;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
onInsertChart: (elements: LibraryItem) => void;
|
||||
}) => {
|
||||
const handleClose = React.useCallback(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}, [onClose]);
|
||||
|
||||
const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
|
||||
onInsertChart(elements);
|
||||
setAppState({
|
||||
currentChartType: chartType,
|
||||
pasteDialog: {
|
||||
shown: false,
|
||||
data: null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
small
|
||||
onCloseRequest={handleClose}
|
||||
title={t("labels.pasteCharts")}
|
||||
className={"PasteChartDialog"}
|
||||
autofocus={false}
|
||||
>
|
||||
<div className={"container"}>
|
||||
<ChartPreviewBtn
|
||||
chartType="bar"
|
||||
spreadsheet={appState.pasteDialog.data}
|
||||
selected={appState.currentChartType === "bar"}
|
||||
onClick={handleChartClick}
|
||||
/>
|
||||
<ChartPreviewBtn
|
||||
chartType="line"
|
||||
spreadsheet={appState.pasteDialog.data}
|
||||
selected={appState.currentChartType === "line"}
|
||||
onClick={handleChartClick}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -130,11 +130,7 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
maxWidth={900}
|
||||
onCloseRequest={handleClose}
|
||||
title={t("shortcutsDialog.title")}
|
||||
>
|
||||
<Dialog onCloseRequest={handleClose} title={t("shortcutsDialog.title")}>
|
||||
<Columns>
|
||||
<Column>
|
||||
<ShortcutIsland caption={t("shortcutsDialog.shapes")}>
|
||||
|
||||
@@ -85,7 +85,6 @@ export const Stats = (props: {
|
||||
<td>{t("stats.total")}</td>
|
||||
<td>{nFormatter(storageSizes.total, 1)}</td>
|
||||
</tr>
|
||||
|
||||
{selectedElements.length === 1 && (
|
||||
<tr>
|
||||
<th colSpan={2}>{t("stats.element")}</th>
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// shrink shape icons on small viewports to make them fit
|
||||
@media (max-width: 425px) {
|
||||
.Shape .ToolIcon__icon {
|
||||
width: 2rem;
|
||||
@@ -153,6 +154,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// move the lock button out of the way on small viewports
|
||||
// it begins to collide with the GitHub icon before we switch to mobile mode
|
||||
@media (max-width: 760px) {
|
||||
.ToolIcon.ToolIcon__lock {
|
||||
display: inline-block;
|
||||
@@ -162,6 +165,7 @@
|
||||
|
||||
margin-left: 0;
|
||||
border-radius: 20px 0 0 20px;
|
||||
z-index: 1;
|
||||
|
||||
background-color: var(--button-gray-1);
|
||||
|
||||
@@ -189,7 +193,7 @@
|
||||
margin-left: 5px;
|
||||
margin-top: 1px;
|
||||
|
||||
@media #{$media-query} {
|
||||
@media #{$is-mobile-query} {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,3 +88,5 @@ export const STORAGE_KEYS = {
|
||||
export const TAP_TWICE_TIMEOUT = 300;
|
||||
export const TOUCH_CTX_MENU_TIMEOUT = 500;
|
||||
export const TITLE_TIMEOUT = 10000;
|
||||
|
||||
export const SCENE_NAME_FALLBACK = "Untitled";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import "open-color/open-color.scss";
|
||||
|
||||
$media-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
|
||||
// keep up to date with is-mobile.tsx
|
||||
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
|
||||
|
||||
+1
-1
@@ -441,7 +441,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$media-query} {
|
||||
@media #{$is-mobile-query} {
|
||||
aside {
|
||||
display: none;
|
||||
}
|
||||
|
||||
+2
-6
@@ -3,7 +3,6 @@
|
||||
:root {
|
||||
--bg-color-island: rgba(255, 255, 255, 0.9);
|
||||
--popup-background-color: #{$oc-white};
|
||||
--border-radius-m: 4px;
|
||||
--space-factor: 0.25rem;
|
||||
--button-gray-1: #{$oc-gray-2};
|
||||
--button-gray-2: #{$oc-gray-4};
|
||||
@@ -15,7 +14,6 @@
|
||||
--icon-fill-color: #{$oc-black};
|
||||
--icon-green-fill-color: #{$oc-green-9};
|
||||
--keybinding-color: #{$oc-gray-5};
|
||||
--color-overlay-text-color: #ccc;
|
||||
--sat: env(safe-area-inset-top);
|
||||
--sab: env(safe-area-inset-bottom);
|
||||
--sal: env(safe-area-inset-left);
|
||||
@@ -23,8 +21,6 @@
|
||||
--text-color-primary: #{$oc-gray-8};
|
||||
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.85)};
|
||||
--overlay-background-color: #{transparentize($oc-white, 0.12)};
|
||||
--border-radius-m: 4px;
|
||||
--space-factor: 0.25rem;
|
||||
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
|
||||
--focus-highlight-color: #{$oc-blue-2};
|
||||
--select-highlight-color: #{$oc-blue-5};
|
||||
@@ -35,6 +31,7 @@
|
||||
--popup-secondary-background-color: #{$oc-gray-1};
|
||||
--popup-text-color: #{$oc-black};
|
||||
--popup-text-inverted-color: #{$oc-white};
|
||||
--dialog-border: #{$oc-gray-6};
|
||||
}
|
||||
|
||||
.excalidraw {
|
||||
@@ -60,10 +57,8 @@
|
||||
--icon-fill-color: #{$oc-gray-4};
|
||||
--icon-green-fill-color: #{$oc-green-4};
|
||||
--keybinding-color: #{$oc-gray-6};
|
||||
--color-overlay-text-color: #bbb;
|
||||
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.7)};
|
||||
--overlay-background-color: rgba(30, 30, 30, 0.88);
|
||||
// #{$oc-gray-4}; inlined
|
||||
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path fill="%23ced4da" d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
|
||||
--focus-highlight-color: #{$oc-blue-6};
|
||||
--select-highlight-color: #{$oc-blue-4};
|
||||
@@ -74,5 +69,6 @@
|
||||
--popup-secondary-background-color: #222;
|
||||
--popup-text-color: #{$oc-gray-4};
|
||||
--popup-text-inverted-color: #2c2c2c;
|
||||
--dialog-border: #{$oc-gray-9};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
DEFAULT_VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { getNewSceneName } from "../utils";
|
||||
|
||||
const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
|
||||
for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) {
|
||||
@@ -166,6 +167,7 @@ const restoreAppState = (
|
||||
|
||||
return {
|
||||
...nextAppState,
|
||||
name: appState.name ?? localAppState?.name ?? getNewSceneName(),
|
||||
offsetLeft: appState.offsetLeft || 0,
|
||||
offsetTop: appState.offsetTop || 0,
|
||||
// Migrates from previous version where appState.zoom was a number
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Point } from "../types";
|
||||
import { FONT_FAMILY } from "../constants";
|
||||
|
||||
export type ChartType = "bar" | "line";
|
||||
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||
export type FontFamily = keyof typeof FONT_FAMILY;
|
||||
export type FontString = string & { _brand: "fontString" };
|
||||
@@ -26,11 +27,21 @@ type _ExcalidrawElementBase = Readonly<{
|
||||
width: number;
|
||||
height: number;
|
||||
angle: number;
|
||||
/** Random integer used to seed shape generation so that the roughjs shape
|
||||
doesn't differ across renders. */
|
||||
seed: number;
|
||||
/** Integer that is sequentially incremented on each change. Used to reconcile
|
||||
elements during collaboration or when saving to server. */
|
||||
version: number;
|
||||
/** Random integer that is regenerated on each change.
|
||||
Used for deterministic reconciliation of updates during collaboration,
|
||||
in case the versions (see above) are identical. */
|
||||
versionNonce: number;
|
||||
isDeleted: boolean;
|
||||
/** List of groups the element belongs to.
|
||||
Ordered from deepest to shallowest. */
|
||||
groupIds: readonly GroupId[];
|
||||
/** Ids of (linear) elements that are bound to this element. */
|
||||
boundElementIds: readonly ExcalidrawLinearElement["id"][] | null;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -123,11 +123,7 @@ const RoomDialog = ({
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Dialog
|
||||
maxWidth={800}
|
||||
onCloseRequest={handleClose}
|
||||
title={t("labels.createRoom")}
|
||||
>
|
||||
<Dialog small onCloseRequest={handleClose} title={t("labels.createRoom")}>
|
||||
{renderRoomDialog()}
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import * as i18n from "../i18n";
|
||||
import * as i18n from "../../i18n";
|
||||
|
||||
export const LanguageList = ({
|
||||
onChange,
|
||||
languages = i18n.languages,
|
||||
currentLanguage = i18n.getLanguage().lng,
|
||||
currentLangCode = i18n.getLanguage().code,
|
||||
floating,
|
||||
}: {
|
||||
languages?: { lng: string; label: string }[];
|
||||
onChange: (value: string) => void;
|
||||
currentLanguage?: string;
|
||||
languages?: { code: string; label: string }[];
|
||||
onChange: (langCode: i18n.Language["code"]) => void;
|
||||
currentLangCode?: i18n.Language["code"];
|
||||
floating?: boolean;
|
||||
}) => (
|
||||
<React.Fragment>
|
||||
@@ -19,12 +19,12 @@ export const LanguageList = ({
|
||||
"dropdown-select--floating": floating,
|
||||
})}
|
||||
onChange={({ target }) => onChange(target.value)}
|
||||
value={currentLanguage}
|
||||
value={currentLangCode}
|
||||
aria-label={i18n.t("buttons.selectLanguage")}
|
||||
>
|
||||
{languages.map((language) => (
|
||||
<option key={language.lng} value={language.lng}>
|
||||
{language.label}
|
||||
{languages.map((lang) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
{lang.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "../../appState";
|
||||
import { clearElementsForLocalStorage } from "../../element";
|
||||
import { STORAGE_KEYS as APP_STORAGE_KEYS } from "../../constants";
|
||||
import { ImportedDataState } from "../../data/types";
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
LOCAL_STORAGE_ELEMENTS: "excalidraw",
|
||||
@@ -81,7 +82,7 @@ export const importFromLocalStorage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
let appState = null;
|
||||
let appState: ImportedDataState["appState"] = null;
|
||||
if (savedState) {
|
||||
try {
|
||||
appState = {
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import React, { useState, useLayoutEffect, useEffect, useRef } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
import Excalidraw from "../packages/excalidraw/index";
|
||||
import Excalidraw, {
|
||||
languages,
|
||||
defaultLang,
|
||||
} from "../packages/excalidraw/index";
|
||||
|
||||
import {
|
||||
getTotalStorageSize,
|
||||
@@ -12,7 +22,7 @@ import {
|
||||
import { ImportedDataState } from "../data/types";
|
||||
import CollabWrapper, { CollabAPI } from "./collab/CollabWrapper";
|
||||
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
||||
import { t } from "../i18n";
|
||||
import { Language, t } from "../i18n";
|
||||
import { exportToBackend, loadScene } from "./data";
|
||||
import { getCollaborationLinkData } from "./data";
|
||||
import { EVENT } from "../constants";
|
||||
@@ -29,6 +39,16 @@ import { EVENT_LOAD, EVENT_SHARE, trackEvent } from "../analytics";
|
||||
import { ErrorDialog } from "../components/ErrorDialog";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { APP_NAME, TITLE_TIMEOUT } from "../constants";
|
||||
import { LanguageList } from "./components/LanguageList";
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.init({
|
||||
languageUtils: {
|
||||
formatLanguageCode: (langCode: Language["code"]) => langCode,
|
||||
isWhitelisted: () => true,
|
||||
},
|
||||
checkWhitelist: false,
|
||||
});
|
||||
|
||||
const excalidrawRef: React.MutableRefObject<
|
||||
MarkRequired<ExcalidrawAPIRefValue, "ready" | "readyPromise">
|
||||
@@ -93,7 +113,6 @@ type Scene = ImportedDataState & { commitToHistory: boolean };
|
||||
const initializeScene = async (opts: {
|
||||
resetScene: ExcalidrawImperativeAPI["resetScene"];
|
||||
initializeSocketClient: CollabAPI["initializeSocketClient"];
|
||||
onLateInitialization?: (scene: Scene) => void;
|
||||
}): Promise<Scene | null> => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const id = searchParams.get("id");
|
||||
@@ -124,17 +143,15 @@ const initializeScene = async (opts: {
|
||||
} else {
|
||||
// https://github.com/excalidraw/excalidraw/issues/1919
|
||||
if (document.hidden) {
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() =>
|
||||
initializeScene(opts).then((_scene) => {
|
||||
opts?.onLateInitialization?.(_scene || scene);
|
||||
}),
|
||||
{
|
||||
once: true,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
return new Promise((resolve, reject) => {
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => initializeScene(opts).then(resolve).catch(reject),
|
||||
{
|
||||
once: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
isCollabScene = false;
|
||||
@@ -185,6 +202,8 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
||||
height: window.innerHeight,
|
||||
});
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const currentLangCode = languageDetector.detect() || defaultLang.code;
|
||||
const [langCode, setLangCode] = useState(currentLangCode);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const onResize = () => {
|
||||
@@ -222,9 +241,6 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
||||
initializeScene({
|
||||
resetScene: excalidrawApi.resetScene,
|
||||
initializeSocketClient: collab.initializeSocketClient,
|
||||
onLateInitialization: (scene) => {
|
||||
initialStatePromiseRef.current.promise.resolve(scene);
|
||||
},
|
||||
}).then((scene) => {
|
||||
initialStatePromiseRef.current.promise.resolve(scene);
|
||||
});
|
||||
@@ -262,6 +278,10 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
||||
};
|
||||
}, [collab.initializeSocketClient]);
|
||||
|
||||
useEffect(() => {
|
||||
languageDetector.cacheUserLanguage(langCode);
|
||||
}, [langCode]);
|
||||
|
||||
const onChange = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
@@ -297,6 +317,32 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderFooter = useCallback(
|
||||
(isMobile: boolean) => {
|
||||
const renderLanguageList = () => (
|
||||
<LanguageList
|
||||
onChange={(langCode) => {
|
||||
setLangCode(langCode);
|
||||
}}
|
||||
languages={languages}
|
||||
floating={!isMobile}
|
||||
currentLangCode={langCode}
|
||||
/>
|
||||
);
|
||||
if (isMobile) {
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("labels.language")}</legend>
|
||||
{renderLanguageList()}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
return renderLanguageList();
|
||||
},
|
||||
[langCode],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Excalidraw
|
||||
@@ -310,6 +356,8 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
|
||||
isCollaborating={collab.isCollaborating}
|
||||
onPointerUpdate={collab.onPointerUpdate}
|
||||
onExportToBackend={onExportToBackend}
|
||||
renderFooter={renderFooter}
|
||||
langCode={langCode}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<ErrorDialog
|
||||
|
||||
+55
-72
@@ -1,93 +1,85 @@
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { EVENT_CHANGE, trackEvent } from "./analytics";
|
||||
|
||||
import fallbackLanguageData from "./locales/en.json";
|
||||
import fallbackLangData from "./locales/en.json";
|
||||
import percentages from "./locales/percentages.json";
|
||||
|
||||
const COMPLETION_THRESHOLD = 85;
|
||||
|
||||
interface Language {
|
||||
lng: string;
|
||||
export interface Language {
|
||||
code: string;
|
||||
label: string;
|
||||
rtl?: boolean;
|
||||
}
|
||||
|
||||
const allLanguages: Language[] = [
|
||||
{ lng: "ar-SA", label: "العربية", rtl: true },
|
||||
{ lng: "bg-BG", label: "Български" },
|
||||
{ lng: "ca-ES", label: "Catalan" },
|
||||
{ lng: "de-DE", label: "Deutsch" },
|
||||
{ lng: "el-GR", label: "Ελληνικά" },
|
||||
{ lng: "es-ES", label: "Español" },
|
||||
{ lng: "fa-IR", label: "فارسی", rtl: true },
|
||||
{ lng: "fi-FI", label: "Suomi" },
|
||||
{ lng: "fr-FR", label: "Français" },
|
||||
{ lng: "he-IL", label: "עברית", rtl: true },
|
||||
{ lng: "hi-IN", label: "हिन्दी" },
|
||||
{ lng: "hu-HU", label: "Magyar" },
|
||||
{ lng: "id-ID", label: "Bahasa Indonesia" },
|
||||
{ lng: "it-IT", label: "Italiano" },
|
||||
{ lng: "ja-JP", label: "日本語" },
|
||||
{ lng: "ko-KR", label: "한국어" },
|
||||
{ lng: "my-MM", label: "Burmese" },
|
||||
{ lng: "nb-NO", label: "Norsk bokmål" },
|
||||
{ lng: "nl-NL", label: "Nederlands" },
|
||||
{ lng: "nn-NO", label: "Norsk nynorsk" },
|
||||
{ lng: "pl-PL", label: "Polski" },
|
||||
{ lng: "pt-BR", label: "Português Brasileiro" },
|
||||
{ lng: "pt-PT", label: "Português" },
|
||||
{ lng: "ro-RO", label: "Română" },
|
||||
{ lng: "ru-RU", label: "Русский" },
|
||||
{ lng: "sk-SK", label: "Slovenčina" },
|
||||
{ lng: "sv-SE", label: "Svenska" },
|
||||
{ lng: "tr-TR", label: "Türkçe" },
|
||||
{ lng: "uk-UA", label: "Українська" },
|
||||
{ lng: "zh-CN", label: "简体中文" },
|
||||
{ lng: "zh-TW", label: "繁體中文" },
|
||||
{ code: "ar-SA", label: "العربية", rtl: true },
|
||||
{ code: "bg-BG", label: "Български" },
|
||||
{ code: "ca-ES", label: "Catalan" },
|
||||
{ code: "de-DE", label: "Deutsch" },
|
||||
{ code: "el-GR", label: "Ελληνικά" },
|
||||
{ code: "es-ES", label: "Español" },
|
||||
{ code: "fa-IR", label: "فارسی", rtl: true },
|
||||
{ code: "fi-FI", label: "Suomi" },
|
||||
{ code: "fr-FR", label: "Français" },
|
||||
{ code: "he-IL", label: "עברית", rtl: true },
|
||||
{ code: "hi-IN", label: "हिन्दी" },
|
||||
{ code: "hu-HU", label: "Magyar" },
|
||||
{ code: "id-ID", label: "Bahasa Indonesia" },
|
||||
{ code: "it-IT", label: "Italiano" },
|
||||
{ code: "ja-JP", label: "日本語" },
|
||||
{ code: "ko-KR", label: "한국어" },
|
||||
{ code: "my-MM", label: "Burmese" },
|
||||
{ code: "nb-NO", label: "Norsk bokmål" },
|
||||
{ code: "nl-NL", label: "Nederlands" },
|
||||
{ code: "nn-NO", label: "Norsk nynorsk" },
|
||||
{ code: "pl-PL", label: "Polski" },
|
||||
{ code: "pt-BR", label: "Português Brasileiro" },
|
||||
{ code: "pt-PT", label: "Português" },
|
||||
{ code: "ro-RO", label: "Română" },
|
||||
{ code: "ru-RU", label: "Русский" },
|
||||
{ code: "sk-SK", label: "Slovenčina" },
|
||||
{ code: "sv-SE", label: "Svenska" },
|
||||
{ code: "tr-TR", label: "Türkçe" },
|
||||
{ code: "uk-UA", label: "Українська" },
|
||||
{ code: "zh-CN", label: "简体中文" },
|
||||
{ code: "zh-TW", label: "繁體中文" },
|
||||
];
|
||||
|
||||
export const languages: Language[] = [{ lng: "en", label: "English" }]
|
||||
export const defaultLang = { code: "en", label: "English" };
|
||||
|
||||
export const languages: Language[] = [defaultLang]
|
||||
.concat(
|
||||
allLanguages.sort((left, right) => (left.label > right.label ? 1 : -1)),
|
||||
)
|
||||
.filter(
|
||||
(lang) =>
|
||||
(percentages as Record<string, number>)[lang.lng] >= COMPLETION_THRESHOLD,
|
||||
(percentages as Record<string, number>)[lang.code] >=
|
||||
COMPLETION_THRESHOLD,
|
||||
);
|
||||
|
||||
let currentLanguage = languages[0];
|
||||
let currentLanguageData = {};
|
||||
const fallbackLanguage = languages[0];
|
||||
let currentLang: Language = defaultLang;
|
||||
let currentLangData = {};
|
||||
|
||||
export const setLanguage = async (newLng: string | undefined) => {
|
||||
currentLanguage =
|
||||
languages.find((language) => language.lng === newLng) || fallbackLanguage;
|
||||
export const setLanguage = async (lang: Language) => {
|
||||
currentLang = lang;
|
||||
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
|
||||
|
||||
document.documentElement.dir = currentLanguage.rtl ? "rtl" : "ltr";
|
||||
|
||||
currentLanguageData = await import(
|
||||
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLanguage.lng}.json`
|
||||
currentLangData = await import(
|
||||
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json`
|
||||
);
|
||||
languageDetector.cacheUserLanguage(currentLanguage.lng);
|
||||
trackEvent(EVENT_CHANGE, "language", currentLanguage.lng);
|
||||
trackEvent(EVENT_CHANGE, "language", currentLang.code);
|
||||
};
|
||||
|
||||
export const setLanguageFirstTime = async () => {
|
||||
const newLng: string | undefined = languageDetector.detect();
|
||||
export const setLanguageFirstTime = async (lang: Language) => {
|
||||
currentLang = lang;
|
||||
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
|
||||
|
||||
currentLanguage =
|
||||
languages.find((language) => language.lng === newLng) || fallbackLanguage;
|
||||
|
||||
document.documentElement.dir = currentLanguage.rtl ? "rtl" : "ltr";
|
||||
|
||||
currentLanguageData = await import(
|
||||
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLanguage.lng}.json`
|
||||
currentLangData = await import(
|
||||
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json`
|
||||
);
|
||||
|
||||
languageDetector.cacheUserLanguage(currentLanguage.lng);
|
||||
};
|
||||
|
||||
export const getLanguage = () => currentLanguage;
|
||||
export const getLanguage = () => currentLang;
|
||||
|
||||
const findPartsForData = (data: any, parts: string[]) => {
|
||||
for (let index = 0; index < parts.length; ++index) {
|
||||
@@ -106,8 +98,8 @@ const findPartsForData = (data: any, parts: string[]) => {
|
||||
export const t = (path: string, replacement?: { [key: string]: string }) => {
|
||||
const parts = path.split(".");
|
||||
let translation =
|
||||
findPartsForData(currentLanguageData, parts) ||
|
||||
findPartsForData(fallbackLanguageData, parts);
|
||||
findPartsForData(currentLangData, parts) ||
|
||||
findPartsForData(fallbackLangData, parts);
|
||||
if (translation === undefined) {
|
||||
throw new Error(`Can't find translation for ${path}`);
|
||||
}
|
||||
@@ -119,12 +111,3 @@ export const t = (path: string, replacement?: { [key: string]: string }) => {
|
||||
}
|
||||
return translation;
|
||||
};
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.init({
|
||||
languageUtils: {
|
||||
formatLanguageCode: (lng: string) => lng,
|
||||
isWhitelisted: () => true,
|
||||
},
|
||||
checkWhitelist: false,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { exportToCanvas } from "./scene/export";
|
||||
import { getDefaultAppState } from "./appState";
|
||||
import { SCENE_NAME_FALLBACK } from "./constants";
|
||||
|
||||
const { registerFont, createCanvas } = require("canvas");
|
||||
|
||||
@@ -61,6 +62,7 @@ const canvas = exportToCanvas(
|
||||
elements as any,
|
||||
{
|
||||
...getDefaultAppState(),
|
||||
name: SCENE_NAME_FALLBACK,
|
||||
offsetTop: 0,
|
||||
offsetLeft: 0,
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ export const IsMobileProvider = ({
|
||||
if (!query.current) {
|
||||
query.current = window.matchMedia
|
||||
? window.matchMedia(
|
||||
// keep up to date with _variables.scss
|
||||
"(max-width: 640px), (max-height: 500px) and (max-width: 1000px)",
|
||||
)
|
||||
: (({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Paste",
|
||||
"pasteCharts": "Paste charts",
|
||||
"selectAll": "Select all",
|
||||
"multiSelect": "Add element to selection",
|
||||
"moveCanvas": "Move canvas",
|
||||
|
||||
@@ -16,6 +16,9 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### Features
|
||||
|
||||
- Remove language picker, and add `langCode`, `renderFooter` [#2644](https://github.com/excalidraw/excalidraw/pull/2644):
|
||||
- BREAKING: removed the language picker from UI. It is now the host app's responsibility to implement a language picker if desirable, using the newly added [`renderFooter`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#renderFooter) prop. The reasoning is that the i18n should be controlled by the app itself, not by the nested Excalidraw component.
|
||||
- Added [`langCode`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#langCode) prop to control the UI language.
|
||||
- Add support for `exportToBackend` prop to allow host apps to implement shareable links [#2612](https://github.com/excalidraw/excalidraw/pull/2612/files)
|
||||
- Add zoom to selection [#2522](https://github.com/excalidraw/excalidraw/pull/2522)
|
||||
- Insert Library items in the middle of the screen [#2527](https://github.com/excalidraw/excalidraw/pull/2527)
|
||||
@@ -27,6 +30,7 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix initialization when browser tab not focused [#2677](https://github.com/excalidraw/excalidraw/pull/2677)
|
||||
- Consistent case for export locale strings [#2622](https://github.com/excalidraw/excalidraw/pull/2622)
|
||||
- Remove unnecessary console.error as it was polluting Sentry [#2637](https://github.com/excalidraw/excalidraw/pull/2637)
|
||||
- Fix scroll-to-center on init for non-zero canvas offsets [#2445](https://github.com/excalidraw/excalidraw/pull/2445)
|
||||
@@ -51,6 +55,10 @@ Please add the latest change on the top under the correct section.
|
||||
|
||||
- Bump ini from 1.3.5 to 1.3.7 in /src/packages/excalidraw [#2500](https://github.com/excalidraw/excalidraw/pull/2500)
|
||||
|
||||
### Docs
|
||||
|
||||
- Document some of the more exotic element props [#2673](https://github.com/excalidraw/excalidraw/pull/2673)
|
||||
|
||||
## 0.1.1
|
||||
|
||||
#### Fix
|
||||
|
||||
@@ -139,6 +139,8 @@ export default function App() {
|
||||
| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode |
|
||||
| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. |
|
||||
| [`onExportToBackend`](#onExportToBackend) | Function | | Callback triggered when link button is clicked on export dialog |
|
||||
| [`langCode`](#langCode) | string | `en` | Language code string |
|
||||
| [`renderFooter `](#renderFooter) | Function | | Function that renders custom UI footer |
|
||||
|
||||
#### `width`
|
||||
|
||||
@@ -270,3 +272,21 @@ This callback is triggered when the shareable-link button is clicked in the expo
|
||||
1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/6e45cb95dbd7a8be1859c7055b06957298e3097c/src/element/types.ts#L76) which needs to be exported.
|
||||
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/4c90ea5667d29effe8ec4a115e49efc7c340cdb3/src/types.ts#L33) of the scene.
|
||||
3. `canvas`: The `HTMLCanvasElement` of the scene.
|
||||
|
||||
#### `langCode`
|
||||
|
||||
Determines the language of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/d337c8b15f6c1085287b12ecbe59c96e2c4e0ff4/src/i18n.ts#L14). Defaults to `en` (English).
|
||||
We also export default language and supported languages which you can import as shown below.
|
||||
|
||||
```js
|
||||
import { defaultLang, languages } from "@excalidraw/excalidraw";
|
||||
```
|
||||
|
||||
| name | type |
|
||||
| ----------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| defaultLang | string |
|
||||
| languages | [Language []](https://github.com/excalidraw/excalidraw/blob/c35d983fef8a83ba842dd892c0f461111a3e8589/src/i18n.ts#L9) |
|
||||
|
||||
#### `renderFooter`
|
||||
|
||||
A function that renders (returns JSX) custom UI footer. For example, you can use this to render a language picker that was previously being rendered by Excalidraw itself (for now, you'll need to implement your own language picker).
|
||||
|
||||
@@ -8,6 +8,7 @@ import "../../css/styles.scss";
|
||||
|
||||
import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
|
||||
import { IsMobileProvider } from "../../is-mobile";
|
||||
import { defaultLang } from "../../i18n";
|
||||
|
||||
const Excalidraw = (props: ExcalidrawProps) => {
|
||||
const {
|
||||
@@ -23,6 +24,8 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
isCollaborating,
|
||||
onPointerUpdate,
|
||||
onExportToBackend,
|
||||
renderFooter,
|
||||
langCode = defaultLang.code,
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,7 +47,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InitializeApp>
|
||||
<InitializeApp langCode={langCode}>
|
||||
<IsMobileProvider>
|
||||
<App
|
||||
width={width}
|
||||
@@ -59,6 +62,8 @@ const Excalidraw = (props: ExcalidrawProps) => {
|
||||
isCollaborating={isCollaborating}
|
||||
onPointerUpdate={onPointerUpdate}
|
||||
onExportToBackend={onExportToBackend}
|
||||
renderFooter={renderFooter}
|
||||
langCode={langCode}
|
||||
/>
|
||||
</IsMobileProvider>
|
||||
</InitializeApp>
|
||||
@@ -94,3 +99,4 @@ export {
|
||||
getSyncableElements,
|
||||
getElementMap,
|
||||
} from "../../element";
|
||||
export { defaultLang, languages } from "../../i18n";
|
||||
|
||||
+75
-199
@@ -1149,6 +1149,12 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@discoveryjs/json-ext": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz",
|
||||
"integrity": "sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==",
|
||||
"dev": true
|
||||
},
|
||||
"@polka/url": {
|
||||
"version": "1.0.0-next.11",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.11.tgz",
|
||||
@@ -1369,18 +1375,18 @@
|
||||
}
|
||||
},
|
||||
"@webpack-cli/info": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.1.0.tgz",
|
||||
"integrity": "sha512-uNWSdaYHc+f3LdIZNwhdhkjjLDDl3jP2+XBqAq9H8DjrJUvlOKdP8TNruy1yEaDfgpAIgbSAN7pye4FEHg9tYQ==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.2.1.tgz",
|
||||
"integrity": "sha512-fLnDML5HZ5AEKzHul8xLAksoKN2cibu6MgonkUj8R9V7bbeVRkd1XbGEGWrAUNYHbX1jcqCsDEpBviE5StPMzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"envinfo": "^7.7.3"
|
||||
}
|
||||
},
|
||||
"@webpack-cli/serve": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.1.0.tgz",
|
||||
"integrity": "sha512-7RfnMXCpJ/NThrhq4gYQYILB18xWyoQcBey81oIyVbmgbc6m5ZHHyFK+DyH7pLHJf0p14MxL4mTsoPAgBSTpIg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.2.1.tgz",
|
||||
"integrity": "sha512-Zj1z6AyS+vqV6Hfi7ngCjFGdHV5EwZNIHo6QfFTNe9PyW+zBU1zJ9BiOW1pmUEq950RC4+Dym6flyA/61/vhyw==",
|
||||
"dev": true
|
||||
},
|
||||
"@xtuc/ieee754": {
|
||||
@@ -1446,12 +1452,6 @@
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"array-back": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.1.tgz",
|
||||
"integrity": "sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==",
|
||||
"dev": true
|
||||
},
|
||||
"babel-code-frame": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
|
||||
@@ -1761,18 +1761,6 @@
|
||||
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"command-line-usage": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.1.tgz",
|
||||
"integrity": "sha512-F59pEuAR9o1SF/bD0dQBDluhpT4jJQNWUHEuVBqpDmCUo6gPjCi+m9fCWnWZVR/oG6cMTUms4h+3NPl74wGXvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-back": "^4.0.1",
|
||||
"chalk": "^2.4.2",
|
||||
"table-layout": "^1.0.1",
|
||||
"typical": "^5.2.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
@@ -1912,12 +1900,6 @@
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"dev": true
|
||||
},
|
||||
"define-properties": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||
@@ -1945,15 +1927,6 @@
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"enhanced-resolve": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz",
|
||||
@@ -1981,9 +1954,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"errno": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
|
||||
"integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
|
||||
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prr": "~1.0.1"
|
||||
@@ -2047,19 +2020,19 @@
|
||||
"dev": true
|
||||
},
|
||||
"execa": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
|
||||
"integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz",
|
||||
"integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^7.0.0",
|
||||
"get-stream": "^5.0.0",
|
||||
"human-signals": "^1.1.1",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"get-stream": "^6.0.0",
|
||||
"human-signals": "^2.1.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^4.0.0",
|
||||
"onetime": "^5.1.0",
|
||||
"signal-exit": "^3.0.2",
|
||||
"npm-run-path": "^4.0.1",
|
||||
"onetime": "^5.1.2",
|
||||
"signal-exit": "^3.0.3",
|
||||
"strip-final-newline": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -2075,6 +2048,12 @@
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true
|
||||
},
|
||||
"fastest-levenshtein": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
|
||||
"integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==",
|
||||
"dev": true
|
||||
},
|
||||
"file-loader": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
|
||||
@@ -2163,13 +2142,10 @@
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz",
|
||||
"integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==",
|
||||
"dev": true
|
||||
},
|
||||
"glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
@@ -2229,9 +2205,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"human-signals": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
|
||||
"integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
|
||||
"dev": true
|
||||
},
|
||||
"icss-utils": {
|
||||
@@ -2248,51 +2224,6 @@
|
||||
"requires": {
|
||||
"pkg-dir": "^4.2.0",
|
||||
"resolve-cwd": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-locate": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-limit": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true
|
||||
},
|
||||
"pkg-dir": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
|
||||
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"find-up": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"indexes-of": {
|
||||
@@ -2322,6 +2253,15 @@
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-core-module": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz",
|
||||
"integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -2413,12 +2353,6 @@
|
||||
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==",
|
||||
"dev": true
|
||||
},
|
||||
"leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
|
||||
"dev": true
|
||||
},
|
||||
"loader-runner": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.1.0.tgz",
|
||||
@@ -2530,18 +2464,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
|
||||
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==",
|
||||
"version": "1.45.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
|
||||
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
|
||||
"dev": true
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.27",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
|
||||
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
|
||||
"version": "2.1.28",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
|
||||
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "1.44.0"
|
||||
"mime-db": "1.45.0"
|
||||
}
|
||||
},
|
||||
"mimic-fn": {
|
||||
@@ -2642,15 +2576,6 @@
|
||||
"object-keys": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
@@ -2807,16 +2732,6 @@
|
||||
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
|
||||
"dev": true
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
@@ -2856,12 +2771,6 @@
|
||||
"resolve": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"reduce-flatten": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz",
|
||||
"integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==",
|
||||
"dev": true
|
||||
},
|
||||
"regenerate": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||
@@ -2930,11 +2839,12 @@
|
||||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
|
||||
"integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
|
||||
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-core-module": "^2.1.0",
|
||||
"path-parse": "^1.0.6"
|
||||
}
|
||||
},
|
||||
@@ -3126,18 +3036,6 @@
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"table-layout": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.1.tgz",
|
||||
"integrity": "sha512-dEquqYNJiGwY7iPfZ3wbXDI944iqanTSchrACLL2nOB+1r+h1Nzu2eH+DuPPvWvm5Ry7iAPeFlgEtP5bIp5U7Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-back": "^4.0.1",
|
||||
"deep-extend": "~0.6.0",
|
||||
"typical": "^5.2.0",
|
||||
"wordwrapjs": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"tapable": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
|
||||
@@ -3227,9 +3125,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"ts-loader": {
|
||||
"version": "8.0.12",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.12.tgz",
|
||||
"integrity": "sha512-UIivVfGVJDdwwjgSrbtcL9Nf10c1BWnL1mxAQUVcnhNIn/P9W3nP5v60Z0aBMtc7ZrE11lMmU6+5jSgAXmGaYw==",
|
||||
"version": "8.0.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.13.tgz",
|
||||
"integrity": "sha512-1o1nO6aqouA23d2nlcMSEyPMAWRhnYUU0EQUJSc60E0TUyBNX792RHFYUN1ZM29vhMUNayrsbj2UVdZwKhXCDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.3.0",
|
||||
@@ -3253,12 +3151,6 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"typical": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz",
|
||||
"integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==",
|
||||
"dev": true
|
||||
},
|
||||
"unicode-canonical-property-names-ecmascript": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
|
||||
@@ -3325,9 +3217,9 @@
|
||||
}
|
||||
},
|
||||
"webpack": {
|
||||
"version": "5.11.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.11.0.tgz",
|
||||
"integrity": "sha512-ubWv7iP54RqAC/VjixgpnLLogCFbAfSOREcSWnnOlZEU8GICC5eKmJSu6YEnph2N2amKqY9rvxSwgyHxVqpaRw==",
|
||||
"version": "5.11.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.11.1.tgz",
|
||||
"integrity": "sha512-tNUIdAmYJv+nupRs/U/gqmADm6fgrf5xE+rSlSsf2PgsGO7j2WG7ccU6AWNlOJlHFl+HnmXlBmHIkiLf+XA9mQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/eslint-scope": "^3.7.0",
|
||||
@@ -3357,13 +3249,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"enhanced-resolve": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.4.0.tgz",
|
||||
"integrity": "sha512-ZmqfWURB2lConOBM1JdCVfPyMRv5RdKWktLXO6123p97ovVm2CLBgw9t5MBj3jJWA6eHyOeIws9iJQoGFR4euQ==",
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.4.1.tgz",
|
||||
"integrity": "sha512-4GbyIMzYktTFoRSmkbgZ1LU+RXwf4AQ8Z+rSuuh1dC8plp0PPeaWvx6+G4hh4KnUJ48VoxKbNyA1QQQIUpXjYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.0.0"
|
||||
"tapable": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
@@ -3522,21 +3414,21 @@
|
||||
}
|
||||
},
|
||||
"webpack-cli": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.2.0.tgz",
|
||||
"integrity": "sha512-EIl3k88vaF4fSxWSgtAQR+VwicfLMTZ9amQtqS4o+TDPW9HGaEpbFBbAZ4A3ZOT5SOnMxNOzROsSTPiE8tBJPA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.3.1.tgz",
|
||||
"integrity": "sha512-/F4+9QNZM/qKzzL9/06Am8NXIkGV+/NqQ62Dx7DSqudxxpAgBqYn6V7+zp+0Y7JuWksKUbczRY3wMTd+7Uj6OA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@webpack-cli/info": "^1.1.0",
|
||||
"@webpack-cli/serve": "^1.1.0",
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
"@webpack-cli/info": "^1.2.1",
|
||||
"@webpack-cli/serve": "^1.2.1",
|
||||
"colorette": "^1.2.1",
|
||||
"command-line-usage": "^6.1.0",
|
||||
"commander": "^6.2.0",
|
||||
"enquirer": "^2.3.6",
|
||||
"execa": "^4.1.0",
|
||||
"execa": "^5.0.0",
|
||||
"fastest-levenshtein": "^1.0.12",
|
||||
"import-local": "^3.0.2",
|
||||
"interpret": "^2.2.0",
|
||||
"leven": "^3.1.0",
|
||||
"rechoir": "^0.7.0",
|
||||
"v8-compile-cache": "^2.2.0",
|
||||
"webpack-merge": "^4.2.2"
|
||||
@@ -3586,22 +3478,6 @@
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"wordwrapjs": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.0.tgz",
|
||||
"integrity": "sha512-Svqw723a3R34KvsMgpjFBYCgNOSdcW3mQFK4wIfhGQhtaFVOJmdYoXgi63ne3dTlWgatVcUc7t4HtQ/+bUVIzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"reduce-flatten": "^2.0.0",
|
||||
"typical": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",
|
||||
|
||||
@@ -57,10 +57,10 @@
|
||||
"mini-css-extract-plugin": "1.3.3",
|
||||
"sass-loader": "10.1.0",
|
||||
"terser-webpack-plugin": "5.0.3",
|
||||
"ts-loader": "8.0.12",
|
||||
"webpack": "5.11.0",
|
||||
"ts-loader": "8.0.13",
|
||||
"webpack": "5.11.1",
|
||||
"webpack-bundle-analyzer": "4.3.0",
|
||||
"webpack-cli": "4.2.0"
|
||||
"webpack-cli": "4.3.1"
|
||||
},
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getDefaultAppState } from "../appState";
|
||||
import { AppState } from "../types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { SCENE_NAME_FALLBACK } from "../constants";
|
||||
|
||||
type ExportOpts = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
@@ -18,7 +19,7 @@ type ExportOpts = {
|
||||
|
||||
export const exportToCanvas = ({
|
||||
elements,
|
||||
appState = getDefaultAppState(),
|
||||
appState = { ...getDefaultAppState(), name: SCENE_NAME_FALLBACK },
|
||||
getDimensions = (width, height) => ({ width, height, scale: 1 }),
|
||||
}: ExportOpts) => {
|
||||
return _exportToCanvas(
|
||||
@@ -74,7 +75,7 @@ export const exportToBlob = (
|
||||
|
||||
export const exportToSvg = ({
|
||||
elements,
|
||||
appState = getDefaultAppState(),
|
||||
appState = { ...getDefaultAppState(), name: SCENE_NAME_FALLBACK },
|
||||
exportPadding,
|
||||
metadata,
|
||||
}: ExportOpts & {
|
||||
|
||||
Generated
+65
-154
@@ -1064,6 +1064,12 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@discoveryjs/json-ext": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz",
|
||||
"integrity": "sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==",
|
||||
"dev": true
|
||||
},
|
||||
"@polka/url": {
|
||||
"version": "1.0.0-next.11",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.11.tgz",
|
||||
@@ -1103,9 +1109,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.14.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.14.tgz",
|
||||
"integrity": "sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ==",
|
||||
"version": "14.14.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.19.tgz",
|
||||
"integrity": "sha512-4nhBPStMK04rruRVtVc6cDqhu7S9GZai0fpXgPXrFpcPX6Xul8xnrjSdGB4KPBVYG/R5+fXWdCM8qBoiULWGPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@webassemblyjs/ast": {
|
||||
@@ -1284,18 +1290,18 @@
|
||||
}
|
||||
},
|
||||
"@webpack-cli/info": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.1.0.tgz",
|
||||
"integrity": "sha512-uNWSdaYHc+f3LdIZNwhdhkjjLDDl3jP2+XBqAq9H8DjrJUvlOKdP8TNruy1yEaDfgpAIgbSAN7pye4FEHg9tYQ==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.2.1.tgz",
|
||||
"integrity": "sha512-fLnDML5HZ5AEKzHul8xLAksoKN2cibu6MgonkUj8R9V7bbeVRkd1XbGEGWrAUNYHbX1jcqCsDEpBviE5StPMzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"envinfo": "^7.7.3"
|
||||
}
|
||||
},
|
||||
"@webpack-cli/serve": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.1.0.tgz",
|
||||
"integrity": "sha512-7RfnMXCpJ/NThrhq4gYQYILB18xWyoQcBey81oIyVbmgbc6m5ZHHyFK+DyH7pLHJf0p14MxL4mTsoPAgBSTpIg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.2.1.tgz",
|
||||
"integrity": "sha512-Zj1z6AyS+vqV6Hfi7ngCjFGdHV5EwZNIHo6QfFTNe9PyW+zBU1zJ9BiOW1pmUEq950RC4+Dym6flyA/61/vhyw==",
|
||||
"dev": true
|
||||
},
|
||||
"@xtuc/ieee754": {
|
||||
@@ -1361,12 +1367,6 @@
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"array-back": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.1.tgz",
|
||||
"integrity": "sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==",
|
||||
"dev": true
|
||||
},
|
||||
"babel-code-frame": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
|
||||
@@ -1670,18 +1670,6 @@
|
||||
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"command-line-usage": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.1.tgz",
|
||||
"integrity": "sha512-F59pEuAR9o1SF/bD0dQBDluhpT4jJQNWUHEuVBqpDmCUo6gPjCi+m9fCWnWZVR/oG6cMTUms4h+3NPl74wGXvA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-back": "^4.0.1",
|
||||
"chalk": "^2.4.2",
|
||||
"table-layout": "^1.0.1",
|
||||
"typical": "^5.2.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
@@ -1762,12 +1750,6 @@
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"dev": true
|
||||
},
|
||||
"define-properties": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||
@@ -1795,15 +1777,6 @@
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"enhanced-resolve": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz",
|
||||
@@ -1831,9 +1804,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"errno": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
|
||||
"integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
|
||||
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prr": "~1.0.1"
|
||||
@@ -1897,19 +1870,19 @@
|
||||
"dev": true
|
||||
},
|
||||
"execa": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
|
||||
"integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz",
|
||||
"integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^7.0.0",
|
||||
"get-stream": "^5.0.0",
|
||||
"human-signals": "^1.1.1",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"get-stream": "^6.0.0",
|
||||
"human-signals": "^2.1.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^4.0.0",
|
||||
"onetime": "^5.1.0",
|
||||
"signal-exit": "^3.0.2",
|
||||
"npm-run-path": "^4.0.1",
|
||||
"onetime": "^5.1.2",
|
||||
"signal-exit": "^3.0.3",
|
||||
"strip-final-newline": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -1925,6 +1898,12 @@
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true
|
||||
},
|
||||
"fastest-levenshtein": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
|
||||
"integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==",
|
||||
"dev": true
|
||||
},
|
||||
"file-loader": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
|
||||
@@ -2013,13 +1992,10 @@
|
||||
}
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz",
|
||||
"integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==",
|
||||
"dev": true
|
||||
},
|
||||
"glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
@@ -2079,9 +2055,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"human-signals": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
|
||||
"integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
|
||||
"dev": true
|
||||
},
|
||||
"import-local": {
|
||||
@@ -2209,12 +2185,6 @@
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
|
||||
"dev": true
|
||||
},
|
||||
"loader-runner": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.1.0.tgz",
|
||||
@@ -2317,18 +2287,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
|
||||
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==",
|
||||
"version": "1.45.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
|
||||
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
|
||||
"dev": true
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.27",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
|
||||
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
|
||||
"version": "2.1.28",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
|
||||
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "1.44.0"
|
||||
"mime-db": "1.45.0"
|
||||
}
|
||||
},
|
||||
"mimic-fn": {
|
||||
@@ -2388,15 +2358,6 @@
|
||||
"object-keys": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"onetime": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
|
||||
@@ -2481,16 +2442,6 @@
|
||||
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
|
||||
"dev": true
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
@@ -2530,12 +2481,6 @@
|
||||
"resolve": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"reduce-flatten": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz",
|
||||
"integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==",
|
||||
"dev": true
|
||||
},
|
||||
"regenerate": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||
@@ -2755,18 +2700,6 @@
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"table-layout": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.1.tgz",
|
||||
"integrity": "sha512-dEquqYNJiGwY7iPfZ3wbXDI944iqanTSchrACLL2nOB+1r+h1Nzu2eH+DuPPvWvm5Ry7iAPeFlgEtP5bIp5U7Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-back": "^4.0.1",
|
||||
"deep-extend": "~0.6.0",
|
||||
"typical": "^5.2.0",
|
||||
"wordwrapjs": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"tapable": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
|
||||
@@ -2856,9 +2789,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"ts-loader": {
|
||||
"version": "8.0.12",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.12.tgz",
|
||||
"integrity": "sha512-UIivVfGVJDdwwjgSrbtcL9Nf10c1BWnL1mxAQUVcnhNIn/P9W3nP5v60Z0aBMtc7ZrE11lMmU6+5jSgAXmGaYw==",
|
||||
"version": "8.0.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.13.tgz",
|
||||
"integrity": "sha512-1o1nO6aqouA23d2nlcMSEyPMAWRhnYUU0EQUJSc60E0TUyBNX792RHFYUN1ZM29vhMUNayrsbj2UVdZwKhXCDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^2.3.0",
|
||||
@@ -2882,12 +2815,6 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"typical": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz",
|
||||
"integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==",
|
||||
"dev": true
|
||||
},
|
||||
"unicode-canonical-property-names-ecmascript": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
|
||||
@@ -2948,9 +2875,9 @@
|
||||
}
|
||||
},
|
||||
"webpack": {
|
||||
"version": "5.11.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.11.0.tgz",
|
||||
"integrity": "sha512-ubWv7iP54RqAC/VjixgpnLLogCFbAfSOREcSWnnOlZEU8GICC5eKmJSu6YEnph2N2amKqY9rvxSwgyHxVqpaRw==",
|
||||
"version": "5.11.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.11.1.tgz",
|
||||
"integrity": "sha512-tNUIdAmYJv+nupRs/U/gqmADm6fgrf5xE+rSlSsf2PgsGO7j2WG7ccU6AWNlOJlHFl+HnmXlBmHIkiLf+XA9mQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/eslint-scope": "^3.7.0",
|
||||
@@ -2980,13 +2907,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"enhanced-resolve": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.4.0.tgz",
|
||||
"integrity": "sha512-ZmqfWURB2lConOBM1JdCVfPyMRv5RdKWktLXO6123p97ovVm2CLBgw9t5MBj3jJWA6eHyOeIws9iJQoGFR4euQ==",
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.4.1.tgz",
|
||||
"integrity": "sha512-4GbyIMzYktTFoRSmkbgZ1LU+RXwf4AQ8Z+rSuuh1dC8plp0PPeaWvx6+G4hh4KnUJ48VoxKbNyA1QQQIUpXjYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.0.0"
|
||||
"tapable": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
@@ -3129,21 +3056,21 @@
|
||||
}
|
||||
},
|
||||
"webpack-cli": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.2.0.tgz",
|
||||
"integrity": "sha512-EIl3k88vaF4fSxWSgtAQR+VwicfLMTZ9amQtqS4o+TDPW9HGaEpbFBbAZ4A3ZOT5SOnMxNOzROsSTPiE8tBJPA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.3.1.tgz",
|
||||
"integrity": "sha512-/F4+9QNZM/qKzzL9/06Am8NXIkGV+/NqQ62Dx7DSqudxxpAgBqYn6V7+zp+0Y7JuWksKUbczRY3wMTd+7Uj6OA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@webpack-cli/info": "^1.1.0",
|
||||
"@webpack-cli/serve": "^1.1.0",
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
"@webpack-cli/info": "^1.2.1",
|
||||
"@webpack-cli/serve": "^1.2.1",
|
||||
"colorette": "^1.2.1",
|
||||
"command-line-usage": "^6.1.0",
|
||||
"commander": "^6.2.0",
|
||||
"enquirer": "^2.3.6",
|
||||
"execa": "^4.1.0",
|
||||
"execa": "^5.0.0",
|
||||
"fastest-levenshtein": "^1.0.12",
|
||||
"import-local": "^3.0.2",
|
||||
"interpret": "^2.2.0",
|
||||
"leven": "^3.1.0",
|
||||
"rechoir": "^0.7.0",
|
||||
"v8-compile-cache": "^2.2.0",
|
||||
"webpack-merge": "^4.2.2"
|
||||
@@ -3193,22 +3120,6 @@
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"wordwrapjs": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.0.tgz",
|
||||
"integrity": "sha512-Svqw723a3R34KvsMgpjFBYCgNOSdcW3mQFK4wIfhGQhtaFVOJmdYoXgi63ne3dTlWgatVcUc7t4HtQ/+bUVIzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"reduce-flatten": "^2.0.0",
|
||||
"typical": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",
|
||||
|
||||
@@ -45,10 +45,10 @@
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
"cross-env": "7.0.3",
|
||||
"file-loader": "6.2.0",
|
||||
"ts-loader": "8.0.12",
|
||||
"webpack": "5.11.0",
|
||||
"ts-loader": "8.0.13",
|
||||
"webpack": "5.11.1",
|
||||
"webpack-bundle-analyzer": "4.3.0",
|
||||
"webpack-cli": "4.2.0"
|
||||
"webpack-cli": "4.3.1"
|
||||
},
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { render } from "./test-utils";
|
||||
import ExcalidrawApp from "../excalidraw-app";
|
||||
import { setLanguage } from "../i18n";
|
||||
import { defaultLang, setLanguage } from "../i18n";
|
||||
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
||||
import { API } from "./helpers/api";
|
||||
import { KEYS } from "../keys";
|
||||
@@ -60,7 +60,7 @@ describe("aligning", () => {
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
mouse.reset();
|
||||
|
||||
await setLanguage("en.json");
|
||||
await setLanguage(defaultLang);
|
||||
await render(<ExcalidrawApp />);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ import ReactDOM from "react-dom";
|
||||
import { copiedStyles } from "../actions/actionStyles";
|
||||
import { ShortcutName } from "../actions/shortcuts";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { setLanguage } from "../i18n";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import Excalidraw from "../packages/excalidraw/index";
|
||||
import ExcalidrawApp from "../excalidraw-app";
|
||||
import { reseed } from "../random";
|
||||
import * as Renderer from "../renderer/renderScene";
|
||||
import { setDateTimeForTests } from "../utils";
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
screen,
|
||||
waitFor,
|
||||
} from "./test-utils";
|
||||
import { defaultLang } from "../i18n";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
@@ -75,8 +75,7 @@ beforeEach(async () => {
|
||||
finger1.reset();
|
||||
finger2.reset();
|
||||
|
||||
await setLanguage("en.json");
|
||||
await render(<Excalidraw offsetLeft={0} offsetTop={0} />);
|
||||
await render(<ExcalidrawApp />);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -439,7 +438,7 @@ describe("regression tests", () => {
|
||||
await waitFor(() => expect(screen.queryByTitle(/thin/i)).toBeNull());
|
||||
// reset language
|
||||
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
|
||||
target: { value: "en" },
|
||||
target: { value: defaultLang.code },
|
||||
});
|
||||
// switching back to English
|
||||
await waitFor(() => expect(screen.queryByTitle(/thin/i)).not.toBeNull());
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
GroupId,
|
||||
ExcalidrawBindableElement,
|
||||
Arrowhead,
|
||||
ChartType,
|
||||
} from "./element/types";
|
||||
import { SHAPES } from "./shapes";
|
||||
import { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
@@ -17,6 +18,8 @@ import { SuggestedBinding } from "./element/binding";
|
||||
import { ImportedDataState } from "./data/types";
|
||||
import { ExcalidrawImperativeAPI } from "./components/App";
|
||||
import type { ResolvablePromise } from "./utils";
|
||||
import { Spreadsheet } from "./charts";
|
||||
import { Language } from "./i18n";
|
||||
|
||||
export type FlooredNumber = number & { _brand: "FlooredNumber" };
|
||||
export type Point = Readonly<RoughPoint>;
|
||||
@@ -97,6 +100,16 @@ export type AppState = {
|
||||
fileHandle: import("browser-nativefs").FileSystemHandle | null;
|
||||
collaborators: Map<string, Collaborator>;
|
||||
showStats: boolean;
|
||||
currentChartType: ChartType;
|
||||
pasteDialog:
|
||||
| {
|
||||
shown: false;
|
||||
data: null;
|
||||
}
|
||||
| {
|
||||
shown: true;
|
||||
data: Spreadsheet;
|
||||
};
|
||||
};
|
||||
|
||||
export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };
|
||||
@@ -169,6 +182,8 @@ export interface ExcalidrawProps {
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
) => void;
|
||||
renderFooter?: (isMobile: boolean) => JSX.Element;
|
||||
langCode?: Language["code"];
|
||||
}
|
||||
|
||||
export type SceneData = {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { FontFamily, FontString } from "./element/types";
|
||||
import { Zoom } from "./types";
|
||||
import { unstable_batchedUpdates } from "react-dom";
|
||||
import { isDarwin } from "./keys";
|
||||
import { t } from "./i18n";
|
||||
|
||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
@@ -32,6 +33,10 @@ export const getDateTime = () => {
|
||||
return `${year}-${month}-${day}-${hr}${min}`;
|
||||
};
|
||||
|
||||
export const getNewSceneName = () => {
|
||||
return `${t("labels.untitled")}-${getDateTime()}`;
|
||||
};
|
||||
|
||||
export const capitalizeString = (str: string) =>
|
||||
str.charAt(0).toUpperCase() + str.slice(1);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user