Compare commits

..

1 Commits

Author SHA1 Message Date
Aakansha Doshi eda35a25b3 remove dependency of i18n from getDefaultAppState and make it pure 2020-12-27 22:12:37 +05:30
65 changed files with 1166 additions and 1771 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
REACT_APP_BACKEND_V1_GET_URL=https://json.excalidraw.com/api/v1/ 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_GET_URL=https://json.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/ REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
REACT_APP_SOCKET_SERVER_URL=https://portal.excalidraw.com REACT_APP_SOCKET_SERVER_URL=https://excalidraw-socket.herokuapp.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"}' 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"}'
+9 -9
View File
@@ -3030,9 +3030,9 @@
} }
}, },
"@testing-library/jest-dom": { "@testing-library/jest-dom": {
"version": "5.11.8", "version": "5.11.6",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.8.tgz", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.6.tgz",
"integrity": "sha512-ScyKrWQM5xNcr79PkSewnA79CLaoxVskE+f7knTOhDD9ftZSA1Jw8mj+pneqhEu3x37ncNfW84NUr7lqK+mXjA==", "integrity": "sha512-cVZyUNRWwUKI0++yepYpYX7uhrP398I+tGz4zOlLVlUYnZS+Svuxv4fwLeCIy7TnBYKXUaOlQr3vopxL8ZfEnA==",
"requires": { "requires": {
"@babel/runtime": "^7.9.2", "@babel/runtime": "^7.9.2",
"@types/testing-library__jest-dom": "^5.9.1", "@types/testing-library__jest-dom": "^5.9.1",
@@ -9039,9 +9039,9 @@
} }
}, },
"firebase-tools": { "firebase-tools": {
"version": "9.1.0", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.1.0.tgz", "resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-9.0.1.tgz",
"integrity": "sha512-hTfxL2meJSl5WuwAS6bEJ5nay7tu3MNYb4ZL2KmPL7yLM3IeT+Qd2z1evHhW1VvbDXKR6RTwlBxzdWPs4l75kA==", "integrity": "sha512-OktyHgjIBJR/JPNU4Xv4NsRLWu5gDnTmYd88VUsMwzUMCkbao2NNpeBi6+0rn6U1zNhwP2WW9PngccJWg/wvSA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@google-cloud/pubsub": "^2.7.0", "@google-cloud/pubsub": "^2.7.0",
@@ -10085,9 +10085,9 @@
} }
}, },
"@types/node": { "@types/node": {
"version": "12.19.11", "version": "12.19.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.11.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.9.tgz",
"integrity": "sha512-bwVfNTFZOrGXyiQ6t4B9sZerMSShWNsGRw8tC5DY1qImUNczS9SjT4G6PnzjCnxsu5Ubj6xjL2lgwddkxtQl5w==", "integrity": "sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q==",
"dev": true "dev": true
}, },
"ansi-styles": { "ansi-styles": {
+2 -2
View File
@@ -21,7 +21,7 @@
"dependencies": { "dependencies": {
"@sentry/browser": "5.29.2", "@sentry/browser": "5.29.2",
"@sentry/integrations": "5.29.2", "@sentry/integrations": "5.29.2",
"@testing-library/jest-dom": "5.11.8", "@testing-library/jest-dom": "5.11.6",
"@testing-library/react": "11.2.2", "@testing-library/react": "11.2.2",
"@types/jest": "26.0.19", "@types/jest": "26.0.19",
"@types/nanoid": "2.1.0", "@types/nanoid": "2.1.0",
@@ -54,7 +54,7 @@
"@types/pako": "1.0.1", "@types/pako": "1.0.1",
"eslint-config-prettier": "7.1.0", "eslint-config-prettier": "7.1.0",
"eslint-plugin-prettier": "3.3.0", "eslint-plugin-prettier": "3.3.0",
"firebase-tools": "9.1.0", "firebase-tools": "9.0.1",
"husky": "4.3.6", "husky": "4.3.6",
"jest-canvas-mock": "2.3.0", "jest-canvas-mock": "2.3.0",
"lint-staged": "10.5.3", "lint-staged": "10.5.3",
+14 -15
View File
@@ -1,22 +1,23 @@
import React from "react"; import React from "react";
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
import { getDefaultAppState } from "../appState";
import colors from "../colors";
import { ColorPicker } from "../components/ColorPicker"; import { ColorPicker } from "../components/ColorPicker";
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons"; import { getDefaultAppState } from "../appState";
import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { getCommonBounds, getNonDeletedElements } from "../element"; 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 { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
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 { getNewZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types"; import { AppState, NormalizedZoomValue } from "../types";
import { getNewSceneName, getShortcutKey } from "../utils"; import { getCommonBounds } from "../element";
import { register } from "./register"; import { getNewZoom } from "../scene/zoom";
import { centerScrollOn } from "../scene/scroll";
import { EVENT_ACTION, EVENT_CHANGE, trackEvent } from "../analytics";
import colors from "../colors";
export const actionChangeViewBackgroundColor = register({ export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor", name: "changeViewBackgroundColor",
@@ -59,7 +60,6 @@ export const actionClearCanvas = register({
), ),
appState: { appState: {
...getDefaultAppState(), ...getDefaultAppState(),
name: getNewSceneName(),
appearance: appState.appearance, appearance: appState.appearance,
elementLocked: appState.elementLocked, elementLocked: appState.elementLocked,
exportBackground: appState.exportBackground, exportBackground: appState.exportBackground,
@@ -67,7 +67,6 @@ export const actionClearCanvas = register({
gridSize: appState.gridSize, gridSize: appState.gridSize,
shouldAddWatermark: appState.shouldAddWatermark, shouldAddWatermark: appState.shouldAddWatermark,
showStats: appState.showStats, showStats: appState.showStats,
pasteDialog: appState.pasteDialog,
}, },
commitToHistory: true, commitToHistory: true,
}; };
+2 -3
View File
@@ -9,10 +9,9 @@ import { loadFromJSON, saveAsJSON } from "../data";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import useIsMobile from "../is-mobile";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { muteFSAbortError } from "../utils"; import { getDateTime, muteFSAbortError } from "../utils";
import { register } from "./register"; import { register } from "./register";
import "../components/ToolIcon.scss"; import "../components/ToolIcon.scss";
import { SCENE_NAME_FALLBACK } from "../constants";
export const actionChangeProjectName = register({ export const actionChangeProjectName = register({
name: "changeProjectName", name: "changeProjectName",
@@ -23,7 +22,7 @@ export const actionChangeProjectName = register({
PanelComponent: ({ appState, updateData }) => ( PanelComponent: ({ appState, updateData }) => (
<ProjectName <ProjectName
label={t("labels.fileTitle")} label={t("labels.fileTitle")}
value={appState.name || SCENE_NAME_FALLBACK} value={appState.name || `${t("labels.untitled")}-${getDateTime()}`}
onChange={(name: string) => updateData(name)} onChange={(name: string) => updateData(name)}
/> />
), ),
+64 -75
View File
@@ -1,84 +1,75 @@
import oc from "open-color"; import oc from "open-color";
import { AppState, FlooredNumber, NormalizedZoomValue } from "./types";
import { import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
SCENE_NAME_FALLBACK, DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
} from "./constants"; } from "./constants";
import { AppState, FlooredNumber, NormalizedZoomValue } from "./types";
type DefaultAppState = Omit<AppState, "offsetTop" | "offsetLeft" | "name"> & { export const getDefaultAppState = (): Omit<
/** AppState,
* You should override this with current appState.name, or whatever is "offsetTop" | "offsetLeft"
* applicable at a given place where you get default appState. > => {
*/
name: undefined;
};
export const getDefaultAppState = (): DefaultAppState => {
return { return {
appearance: "light", appearance: "light",
collaborators: new Map(), isLoading: false,
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,
editingElement: null,
editingGroupId: null,
editingLinearElement: null,
elementLocked: false,
elementType: "selection",
errorMessage: null, errorMessage: null,
draggingElement: null,
resizingElement: null,
multiElement: null,
editingElement: null,
startBoundElement: null,
editingLinearElement: null,
elementType: "selection",
elementLocked: false,
exportBackground: true, exportBackground: true,
exportEmbedScene: false, exportEmbedScene: false,
fileHandle: null, shouldAddWatermark: false,
gridSize: null, currentItemStrokeColor: oc.black,
height: window.innerHeight, currentItemBackgroundColor: "transparent",
isBindingEnabled: true, currentItemFillStyle: "hachure",
isLibraryOpen: false, currentItemStrokeWidth: 1,
isLoading: false, currentItemStrokeStyle: "solid",
isResizing: false, currentItemRoughness: 1,
isRotating: false, currentItemOpacity: 100,
lastPointerDownWith: "mouse", currentItemFontSize: DEFAULT_FONT_SIZE,
multiElement: null, currentItemFontFamily: DEFAULT_FONT_FAMILY,
// for safety (because TS mostly doesn't distinguish optional types and currentItemTextAlign: DEFAULT_TEXT_ALIGN,
// undefined values), we set `name` to the fallback name, but we cast it to currentItemStrokeSharpness: "sharp",
// `undefined` so that TS forces us to explicitly specify it wherever currentItemLinearStrokeSharpness: "round",
// possible currentItemStartArrowhead: null,
name: (SCENE_NAME_FALLBACK as unknown) as undefined, currentItemEndArrowhead: "arrow",
openMenu: null, viewBackgroundColor: oc.white,
pasteDialog: { shown: false, data: null },
previousSelectedElementIds: {},
resizingElement: null,
scrolledOutside: false,
scrollX: 0 as FlooredNumber, scrollX: 0 as FlooredNumber,
scrollY: 0 as FlooredNumber, scrollY: 0 as FlooredNumber,
selectedElementIds: {}, cursorButton: "up",
selectedGroupIds: {}, scrolledOutside: false,
name: "",
isBindingEnabled: true,
isResizing: false,
isRotating: false,
selectionElement: null, selectionElement: null,
shouldAddWatermark: false, zoom: {
value: 1 as NormalizedZoomValue,
translation: { x: 0, y: 0 },
},
openMenu: null,
lastPointerDownWith: "mouse",
selectedElementIds: {},
previousSelectedElementIds: {},
shouldCacheIgnoreZoom: false, shouldCacheIgnoreZoom: false,
showShortcutsDialog: false, showShortcutsDialog: false,
showStats: false,
startBoundElement: null,
suggestedBindings: [], suggestedBindings: [],
viewBackgroundColor: oc.white,
width: window.innerWidth,
zenModeEnabled: false, zenModeEnabled: false,
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } }, gridSize: null,
editingGroupId: null,
selectedGroupIds: {},
width: window.innerWidth,
height: window.innerHeight,
isLibraryOpen: false,
fileHandle: null,
collaborators: new Map(),
showStats: false,
}; };
}; };
@@ -98,25 +89,24 @@ const APP_STATE_STORAGE_CONF = (<
config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }, config: { [K in keyof T]: K extends keyof AppState ? T[K] : never },
) => config)({ ) => config)({
appearance: { browser: true, export: false }, appearance: { browser: true, export: false },
collaborators: { browser: false, export: false },
currentChartType: { browser: true, export: false },
currentItemBackgroundColor: { browser: true, export: false }, currentItemBackgroundColor: { browser: true, export: false },
currentItemEndArrowhead: { browser: true, export: false },
currentItemFillStyle: { browser: true, export: false }, currentItemFillStyle: { browser: true, export: false },
currentItemFontFamily: { browser: true, export: false }, currentItemFontFamily: { browser: true, export: false },
currentItemFontSize: { browser: true, export: false }, currentItemFontSize: { browser: true, export: false },
currentItemLinearStrokeSharpness: { browser: true, export: false },
currentItemOpacity: { browser: true, export: false }, currentItemOpacity: { browser: true, export: false },
currentItemRoughness: { browser: true, export: false }, currentItemRoughness: { browser: true, export: false },
currentItemStartArrowhead: { browser: true, export: false },
currentItemStrokeColor: { browser: true, export: false }, currentItemStrokeColor: { browser: true, export: false },
currentItemStrokeSharpness: { browser: true, export: false },
currentItemStrokeStyle: { browser: true, export: false }, currentItemStrokeStyle: { browser: true, export: false },
currentItemStrokeWidth: { browser: true, export: false }, currentItemStrokeWidth: { browser: true, export: false },
currentItemTextAlign: { 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 }, cursorButton: { browser: true, export: false },
draggingElement: { browser: false, export: false }, draggingElement: { browser: false, export: false },
editingElement: { browser: false, export: false }, editingElement: { browser: false, export: false },
startBoundElement: { browser: false, export: false },
editingGroupId: { browser: true, export: false }, editingGroupId: { browser: true, export: false },
editingLinearElement: { browser: false, export: false }, editingLinearElement: { browser: false, export: false },
elementLocked: { browser: true, export: false }, elementLocked: { browser: true, export: false },
@@ -124,7 +114,6 @@ const APP_STATE_STORAGE_CONF = (<
errorMessage: { browser: false, export: false }, errorMessage: { browser: false, export: false },
exportBackground: { browser: true, export: false }, exportBackground: { browser: true, export: false },
exportEmbedScene: { browser: true, export: false }, exportEmbedScene: { browser: true, export: false },
fileHandle: { browser: false, export: false },
gridSize: { browser: true, export: true }, gridSize: { browser: true, export: true },
height: { browser: false, export: false }, height: { browser: false, export: false },
isBindingEnabled: { browser: false, export: false }, isBindingEnabled: { browser: false, export: false },
@@ -135,10 +124,7 @@ const APP_STATE_STORAGE_CONF = (<
lastPointerDownWith: { browser: true, export: false }, lastPointerDownWith: { browser: true, export: false },
multiElement: { browser: false, export: false }, multiElement: { browser: false, export: false },
name: { browser: true, export: false }, name: { browser: true, export: false },
offsetLeft: { browser: false, export: false },
offsetTop: { browser: false, export: false },
openMenu: { browser: true, export: false }, openMenu: { browser: true, export: false },
pasteDialog: { browser: false, export: false },
previousSelectedElementIds: { browser: true, export: false }, previousSelectedElementIds: { browser: true, export: false },
resizingElement: { browser: false, export: false }, resizingElement: { browser: false, export: false },
scrolledOutside: { browser: true, export: false }, scrolledOutside: { browser: true, export: false },
@@ -150,13 +136,16 @@ const APP_STATE_STORAGE_CONF = (<
shouldAddWatermark: { browser: true, export: false }, shouldAddWatermark: { browser: true, export: false },
shouldCacheIgnoreZoom: { browser: true, export: false }, shouldCacheIgnoreZoom: { browser: true, export: false },
showShortcutsDialog: { browser: false, export: false }, showShortcutsDialog: { browser: false, export: false },
showStats: { browser: true, export: false },
startBoundElement: { browser: false, export: false },
suggestedBindings: { browser: false, export: false }, suggestedBindings: { browser: false, export: false },
viewBackgroundColor: { browser: true, export: true }, viewBackgroundColor: { browser: true, export: true },
width: { browser: false, export: false }, width: { browser: false, export: false },
zenModeEnabled: { browser: true, export: false }, zenModeEnabled: { browser: true, export: false },
zoom: { 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">( const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
+75 -271
View File
@@ -1,16 +1,13 @@
import { EVENT_MAGIC, trackEvent } from "./analytics"; import { EVENT_MAGIC, trackEvent } from "./analytics";
import colors from "./colors"; import colors from "./colors";
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, ENV } from "./constants"; import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "./constants";
import { newElement, newLinearElement, newTextElement } from "./element"; import { newElement, newTextElement, newLinearElement } from "./element";
import { NonDeletedExcalidrawElement } from "./element/types"; import { ExcalidrawElement } from "./element/types";
import { randomId } from "./random"; import { randomId } from "./random";
export type ChartElements = readonly NonDeletedExcalidrawElement[];
const BAR_WIDTH = 32; const BAR_WIDTH = 32;
const BAR_GAP = 12; const BAR_GAP = 12;
const BAR_HEIGHT = 256; const BAR_HEIGHT = 256;
const GRID_OPACITY = 50;
export interface Spreadsheet { export interface Spreadsheet {
title: string | null; title: string | null;
@@ -142,72 +139,41 @@ export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
return transposedResults; return transposedResults;
} }
} }
return result; return result;
}; };
const bgColors = colors.elementBackground.slice( // For the maths behind it https://excalidraw.com/#json=6320864370884608,O_5xfD-Agh32tytHpRJx1g
2, export const renderSpreadsheet = (
colors.elementBackground.length, spreadsheet: Spreadsheet,
); x: number,
y: number,
// Put all the common properties here so when the whole chart is selected ): ExcalidrawElement[] => {
// the properties dialog shows the correct selected values const values = spreadsheet.values;
const commonProps = { const max = Math.max(...values);
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; const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
return { chartWidth, chartHeight }; const chartWidth = (BAR_WIDTH + BAR_GAP) * values.length + BAR_GAP;
}; const maxColors = colors.elementBackground.length;
const bgColors = colors.elementBackground.slice(2, maxColors);
const chartXLabels = ( // Put all the common properties here so when the whole chart is selected
spreadsheet: Spreadsheet, // the properties dialog shows the correct selected values
x: number, const commonProps = {
y: number, backgroundColor: bgColors[Math.floor(Math.random() * bgColors.length)],
groupId: string, fillStyle: "hachure",
backgroundColor: string, fontFamily: DEFAULT_FONT_FAMILY,
): ChartElements => { fontSize: DEFAULT_FONT_SIZE,
return ( groupIds: [randomId()],
spreadsheet.labels?.map((label, index) => { opacity: 100,
return newTextElement({ roughness: 1,
groupIds: [groupId], strokeColor: colors.elementStroke[0],
backgroundColor, strokeSharpness: "sharp",
...commonProps, strokeStyle: "solid",
text: label.length > 8 ? `${label.slice(0, 5)}...` : label, strokeWidth: 1,
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2, verticalAlign: "middle",
y: y + BAR_GAP / 2, } as const;
width: BAR_WIDTH,
angle: 5.87,
fontSize: 16,
textAlign: "center",
verticalAlign: "top",
});
}) || []
);
};
const chartYLabels = (
spreadsheet: Spreadsheet,
x: number,
y: number,
groupId: string,
backgroundColor: string,
): ChartElements => {
const minYLabel = newTextElement({ const minYLabel = newTextElement({
groupIds: [groupId],
backgroundColor,
...commonProps, ...commonProps,
x: x - BAR_GAP, x: x - BAR_GAP,
y: y - BAR_GAP, y: y - BAR_GAP,
@@ -216,30 +182,14 @@ const chartYLabels = (
}); });
const maxYLabel = newTextElement({ const maxYLabel = newTextElement({
groupIds: [groupId],
backgroundColor,
...commonProps, ...commonProps,
x: x - BAR_GAP, x: x - BAR_GAP,
y: y - BAR_HEIGHT - minYLabel.height / 2, y: y - BAR_HEIGHT - minYLabel.height / 2,
text: Math.max(...spreadsheet.values).toLocaleString(), text: max.toLocaleString(),
textAlign: "right", textAlign: "right",
}); });
return [minYLabel, maxYLabel]; const xAxisLine = newLinearElement({
};
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", type: "line",
x, x,
y, y,
@@ -250,12 +200,10 @@ const chartLines = (
[0, 0], [0, 0],
[chartWidth, 0], [chartWidth, 0],
], ],
...commonProps,
}); });
const yLine = newLinearElement({ const yAxisLine = newLinearElement({
backgroundColor,
groupIds: [groupId],
...commonProps,
type: "line", type: "line",
x, x,
y, y,
@@ -266,93 +214,27 @@ const chartLines = (
[0, 0], [0, 0],
[0, -chartHeight], [0, -chartHeight],
], ],
...commonProps,
}); });
const maxLine = newLinearElement({ const maxValueLine = newLinearElement({
backgroundColor,
groupIds: [groupId],
...commonProps,
type: "line", type: "line",
x, x,
y: y - BAR_HEIGHT - BAR_GAP, y: y - BAR_HEIGHT - BAR_GAP,
startArrowhead: null, startArrowhead: null,
endArrowhead: null, endArrowhead: null,
...commonProps,
strokeStyle: "dotted", strokeStyle: "dotted",
width: chartWidth, width: chartWidth,
opacity: GRID_OPACITY,
points: [ points: [
[0, 0], [0, 0],
[chartWidth, 0], [chartWidth, 0],
], ],
}); });
return [xLine, yLine, maxLine]; const bars = values.map((value, index) => {
};
// 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 - DEFAULT_FONT_SIZE,
strokeSharpness: "sharp",
strokeStyle: "solid",
textAlign: "center",
})
: null;
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 [
...(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; const barHeight = (value / max) * BAR_HEIGHT;
return newElement({ return newElement({
backgroundColor,
groupIds: [groupId],
...commonProps, ...commonProps,
type: "rectangle", type: "rectangle",
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP, x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP,
@@ -362,120 +244,42 @@ const chartTypeBar = (
}); });
}); });
const xLabels =
spreadsheet.labels?.map((label, index) => {
return newTextElement({
...commonProps,
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
y: y + BAR_GAP / 2,
width: BAR_WIDTH,
angle: 5.87,
fontSize: 16,
textAlign: "center",
verticalAlign: "top",
});
}) || [];
const title = spreadsheet.title
? newTextElement({
...commonProps,
text: spreadsheet.title,
x: x + chartWidth / 2,
y: y - BAR_HEIGHT - BAR_GAP * 2 - maxYLabel.height,
strokeSharpness: "sharp",
strokeStyle: "solid",
textAlign: "center",
})
: null;
trackEvent(EVENT_MAGIC, "chart", "bars", bars.length);
return [ return [
title,
...bars, ...bars,
...chartBaseElements( ...xLabels,
spreadsheet, xAxisLine,
x, yAxisLine,
y, maxValueLine,
groupId, minYLabel,
backgroundColor, maxYLabel,
process.env.NODE_ENV === ENV.DEVELOPMENT, ].filter((element) => element !== null) as ExcalidrawElement[];
),
];
};
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);
}; };
+161 -175
View File
@@ -1,169 +1,182 @@
import { Point, simplify } from "points-on-curve";
import React from "react"; import React from "react";
import { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough"; import rough from "roughjs/bin/rough";
import "../actions"; import { RoughCanvas } from "roughjs/bin/canvas";
import { actionDeleteSelected, actionFinalize } from "../actions"; import { simplify, Point } from "points-on-curve";
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
import { ActionManager } from "../actions/manager";
import { actions } from "../actions/register";
import { ActionResult } from "../actions/types";
import { import {
EVENT_DIALOG, newElement,
EVENT_LIBRARY, newTextElement,
EVENT_SHAPE, duplicateElement,
trackEvent, isInvisiblySmallElement,
} from "../analytics"; isTextElement,
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 "../actions";
import { actions } from "../actions/register";
import { ActionResult } from "../actions/types";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { t, getLanguage } from "../i18n";
import { import {
copyToClipboard, copyToClipboard,
parseClipboard, parseClipboard,
probablySupportsClipboardBlob, probablySupportsClipboardBlob,
probablySupportsClipboardWriteText, probablySupportsClipboardWriteText,
} from "../clipboard"; } from "../clipboard";
import { normalizeScroll } from "../scene";
import { getCenter, getDistance } from "../gesture";
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
import { import {
APP_NAME,
CANVAS_ONLY_ACTIONS,
CURSOR_TYPE, CURSOR_TYPE,
DEFAULT_VERTICAL_ALIGN,
DRAGGING_THRESHOLD,
ELEMENT_SHIFT_TRANSLATE_AMOUNT, ELEMENT_SHIFT_TRANSLATE_AMOUNT,
ELEMENT_TRANSLATE_AMOUNT, ELEMENT_TRANSLATE_AMOUNT,
ENV,
EVENT,
GRID_SIZE,
LINE_CONFIRM_THRESHOLD,
MIME_TYPES,
POINTER_BUTTON, POINTER_BUTTON,
TAP_TWICE_TIMEOUT, DRAGGING_THRESHOLD,
TEXT_TO_CENTER_SNAP_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, TOUCH_CTX_MENU_TIMEOUT,
APP_NAME,
} from "../constants"; } from "../constants";
import { exportCanvas, loadFromBlob } from "../data";
import { isValidLibrary } from "../data/json"; import LayerUI from "./LayerUI";
import { Library } from "../data/library"; import { ScrollBars, SceneState } from "../scene/types";
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 { 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 { invalidateShapeForElement } from "../renderer/renderElement";
import { import {
calculateScrollCenter, isLinearElement,
getElementContainingPosition, isLinearElementType,
getElementsAtPosition, isBindingElement,
getElementsWithinSelection, isBindingElementType,
getNormalizedZoom, } from "../element/typeChecks";
getSelectedElements, import { actionFinalize, actionDeleteSelected } from "../actions";
isOverScrollBars,
isSomeElementSelected, import { LinearElementEditor } from "../element/linearElementEditor";
normalizeScroll, import {
} from "../scene"; getSelectedGroupIds,
isSelectedViaGroup,
selectGroupsForSelectedElements,
isElementInGroup,
getSelectedGroupIdForElement,
getElementsInGroup,
editGroupForSelectedElement,
} from "../groups";
import { Library } from "../data/library";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { SceneState, ScrollBars } from "../scene/types"; 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 { getNewZoom } from "../scene/zoom";
import { findShapeByKey } from "../shapes"; import { restore } from "../data/restore";
import { import {
AppState, EVENT_DIALOG,
ExcalidrawProps, EVENT_LIBRARY,
Gesture, EVENT_SHAPE,
GestureEvent, trackEvent,
SceneData, } from "../analytics";
} 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"; import { Stats } from "./Stats";
import { textWysiwyg } from "../element/textWysiwyg";
const { history } = createHistory(); const { history } = createHistory();
@@ -282,7 +295,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} = props; } = props;
this.state = { this.state = {
...defaultAppState, ...defaultAppState,
name: getNewSceneName(),
isLoading: true, isLoading: true,
width, width,
height, height,
@@ -334,7 +346,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
offsetLeft, offsetLeft,
} = this.state; } = this.state;
const { onCollabButtonClick, onExportToBackend, renderFooter } = this.props; const { onCollabButtonClick, onExportToBackend } = this.props;
const canvasScale = window.devicePixelRatio; const canvasScale = window.devicePixelRatio;
const canvasWidth = canvasDOMWidth * canvasScale; const canvasWidth = canvasDOMWidth * canvasScale;
@@ -362,7 +374,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
elements={this.scene.getElements()} elements={this.scene.getElements()}
onCollabButtonClick={onCollabButtonClick} onCollabButtonClick={onCollabButtonClick}
onLockToggle={this.toggleLock} onLockToggle={this.toggleLock}
onInsertElements={(elements) => onInsertShape={(elements) =>
this.addElementsFromPasteOrLibrary( this.addElementsFromPasteOrLibrary(
elements, elements,
DEFAULT_PASTE_X, DEFAULT_PASTE_X,
@@ -371,10 +383,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
zenModeEnabled={zenModeEnabled} zenModeEnabled={zenModeEnabled}
toggleZenMode={this.toggleZenMode} toggleZenMode={this.toggleZenMode}
langCode={getLanguage().code} lng={getLanguage().lng}
isCollaborating={this.props.isCollaborating || false} isCollaborating={this.props.isCollaborating || false}
onExportToBackend={onExportToBackend} onExportToBackend={onExportToBackend}
renderCustomFooter={renderFooter}
/> />
{this.state.showStats && ( {this.state.showStats && (
<Stats <Stats
@@ -530,7 +541,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.scene.replaceAllElements([]); this.scene.replaceAllElements([]);
this.setState((state) => ({ this.setState((state) => ({
...getDefaultAppState(), ...getDefaultAppState(),
name: getNewSceneName(),
isLoading: opts?.resetLoadingState ? false : state.isLoading, isLoading: opts?.resetLoadingState ? false : state.isLoading,
appearance: this.state.appearance, appearance: this.state.appearance,
})); }));
@@ -742,10 +752,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
} }
componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) { componentDidUpdate(prevProps: ExcalidrawProps, prevState: AppState) {
if (prevProps.langCode !== this.props.langCode) {
this.updateLanguage();
}
if ( if (
prevProps.width !== this.props.width || prevProps.width !== this.props.width ||
prevProps.height !== this.props.height || prevProps.height !== this.props.height ||
@@ -869,16 +875,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
history.record(this.state, this.scene.getElementsIncludingDeleted()); history.record(this.state, this.scene.getElementsIncludingDeleted());
// Do not notify consumers if we're still loading the scene. Among other this.props.onChange?.(this.scene.getElementsIncludingDeleted(), this.state);
// 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 // Copy/paste
@@ -1007,12 +1004,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (data.errorMessage) { if (data.errorMessage) {
this.setState({ errorMessage: data.errorMessage }); this.setState({ errorMessage: data.errorMessage });
} else if (data.spreadsheet) { } else if (data.spreadsheet) {
this.setState({ this.addElementsFromPasteOrLibrary(
pasteDialog: { renderSpreadsheet(data.spreadsheet, cursorX, cursorY),
data: data.spreadsheet, );
shown: true,
},
});
} else if (data.elements) { } else if (data.elements) {
this.addElementsFromPasteOrLibrary(data.elements); this.addElementsFromPasteOrLibrary(data.elements);
} else if (data.text) { } else if (data.text) {
@@ -3857,14 +3851,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
offsetTop: typeof offsets?.offsetTop === "number" ? offsets.offsetTop : 0, 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({});
}
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
+1 -1
View File
@@ -218,7 +218,7 @@
left: 2px; left: 2px;
} }
@media #{$is-mobile-query} { @media #{$media-query} {
display: none; display: none;
} }
} }
+5 -19
View File
@@ -1,4 +1,4 @@
@import "../css/_variables"; @import "open-color/open-color.scss";
.excalidraw { .excalidraw {
.context-menu { .context-menu {
@@ -42,16 +42,16 @@
} }
&.dangerous { &.dangerous {
.context-menu-option__label { div:nth-child(1) {
color: $oc-red-7; color: $oc-red-7;
} }
} }
.context-menu-option__label { div:nth-child(1) {
justify-self: start; justify-self: start;
margin-inline-end: 20px; margin-inline-end: 20px;
} }
.context-menu-option__shortcut { div:nth-child(2) {
justify-self: end; justify-self: end;
opacity: 0.6; opacity: 0.6;
font-size: 0.7rem; font-size: 0.7rem;
@@ -63,7 +63,7 @@
background-color: var(--select-highlight-color); background-color: var(--select-highlight-color);
&.dangerous { &.dangerous {
.context-menu-option__label { div:nth-child(1) {
color: var(--popup-background-color); color: var(--popup-background-color);
} }
background-color: $oc-red-6; background-color: $oc-red-6;
@@ -73,18 +73,4 @@
.context-menu-option:focus { .context-menu-option:focus {
z-index: 1; 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;
}
}
}
} }
+2 -2
View File
@@ -51,8 +51,8 @@ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
${checked ? "checkmark" : ""}`} ${checked ? "checkmark" : ""}`}
onClick={action} onClick={action}
> >
<div className="context-menu-option__label">{label}</div> <div>{label}</div>
<div className="context-menu-option__shortcut"> <div>
{shortcutName {shortcutName
? getShortcutFromShortcutName(shortcutName) ? getShortcutFromShortcutName(shortcutName)
: ""} : ""}
+7 -9
View File
@@ -7,9 +7,6 @@
margin-top: 0; margin-top: 0;
grid-template-columns: 1fr calc(var(--space-factor) * 7); grid-template-columns: 1fr calc(var(--space-factor) * 7);
grid-gap: var(--metric); grid-gap: var(--metric);
padding: calc(var(--space-factor) * 2);
text-align: center;
font-variant: small-caps;
} }
.Dialog__titleContent { .Dialog__titleContent {
@@ -21,11 +18,7 @@
margin: 0; margin: 0;
} }
.Dialog__content { @media #{$media-query} {
padding: 0 16px 16px;
}
@media #{$is-mobile-query} {
.Dialog { .Dialog {
--metric: calc(var(--space-factor) * 4); --metric: calc(var(--space-factor) * 4);
--inset-left: #{"max(var(--metric), var(--sal))"}; --inset-left: #{"max(var(--metric), var(--sal))"};
@@ -37,8 +30,13 @@
var(--space-factor) * 7 var(--space-factor) * 7
); );
position: sticky; position: sticky;
top: 0; top: calc(-1 * var(--metric));
margin: calc(-1 * var(--inset-right));
margin-top: calc(-1 * var(--metric));
margin-bottom: var(--metric);
padding: calc(var(--space-factor) * 2); padding: calc(var(--space-factor) * 2);
padding-left: var(--inset-left);
padding-right: var(--inset-right);
background: var(--bg-color-island); background: var(--bg-color-island);
font-size: 1.25em; font-size: 1.25em;
+14 -14
View File
@@ -1,12 +1,13 @@
import clsx from "clsx";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import clsx from "clsx";
import { Modal } from "./Modal";
import { Island } from "./Island";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import useIsMobile from "../is-mobile";
import { KEYS } from "../keys";
import "./Dialog.scss";
import { back, close } from "./icons"; import { back, close } from "./icons";
import { Island } from "./Island"; import { KEYS } from "../keys";
import { Modal } from "./Modal";
import "./Dialog.scss";
const useRefState = <T,>() => { const useRefState = <T,>() => {
const [refValue, setRefValue] = useState<T | null>(null); const [refValue, setRefValue] = useState<T | null>(null);
@@ -19,10 +20,9 @@ const useRefState = <T,>() => {
export const Dialog = (props: { export const Dialog = (props: {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
small?: boolean; maxWidth?: number;
onCloseRequest(): void; onCloseRequest(): void;
title: React.ReactNode; title: React.ReactNode;
autofocus?: boolean;
}) => { }) => {
const [islandNode, setIslandNode] = useRefState<HTMLDivElement>(); const [islandNode, setIslandNode] = useRefState<HTMLDivElement>();
@@ -33,7 +33,7 @@ export const Dialog = (props: {
const focusableElements = queryFocusableElements(islandNode); const focusableElements = queryFocusableElements(islandNode);
if (focusableElements.length > 0 && props.autofocus !== false) { if (focusableElements.length > 0) {
// If there's an element other than close, focus it. // If there's an element other than close, focus it.
(focusableElements[1] || focusableElements[0]).focus(); (focusableElements[1] || focusableElements[0]).focus();
} }
@@ -62,7 +62,7 @@ export const Dialog = (props: {
islandNode.addEventListener("keydown", handleKeyDown); islandNode.addEventListener("keydown", handleKeyDown);
return () => islandNode.removeEventListener("keydown", handleKeyDown); return () => islandNode.removeEventListener("keydown", handleKeyDown);
}, [islandNode, props.autofocus]); }, [islandNode]);
const queryFocusableElements = (node: HTMLElement) => { const queryFocusableElements = (node: HTMLElement) => {
const focusableElements = node.querySelectorAll<HTMLElement>( const focusableElements = node.querySelectorAll<HTMLElement>(
@@ -76,11 +76,11 @@ export const Dialog = (props: {
<Modal <Modal
className={clsx("Dialog", props.className)} className={clsx("Dialog", props.className)}
labelledBy="dialog-title" labelledBy="dialog-title"
maxWidth={props.small ? 550 : 800} maxWidth={props.maxWidth}
onCloseRequest={props.onCloseRequest} onCloseRequest={props.onCloseRequest}
> >
<Island ref={setIslandNode}> <Island padding={4} ref={setIslandNode}>
<h3 id="dialog-title" className="Dialog__title"> <h2 id="dialog-title" className="Dialog__title">
<span className="Dialog__titleContent">{props.title}</span> <span className="Dialog__titleContent">{props.title}</span>
<button <button
className="Modal__close" className="Modal__close"
@@ -89,8 +89,8 @@ export const Dialog = (props: {
> >
{useIsMobile() ? back : close} {useIsMobile() ? back : close}
</button> </button>
</h3> </h2>
<div className="Dialog__content">{props.children}</div> {props.children}
</Island> </Island>
</Modal> </Modal>
); );
+1 -1
View File
@@ -24,7 +24,7 @@ export const ErrorDialog = ({
<> <>
{modalIsShown && ( {modalIsShown && (
<Dialog <Dialog
small maxWidth={500}
onCloseRequest={handleClose} onCloseRequest={handleClose}
title={t("errorDialog.title")} title={t("errorDialog.title")}
> >
+3 -1
View File
@@ -37,7 +37,7 @@
} }
} }
@media #{$is-mobile-query} { @media (max-width: 550px) {
.ExportDialog { .ExportDialog {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -51,7 +51,9 @@
.ExportDialog__actions > * { .ExportDialog__actions > * {
margin-bottom: calc(var(--space-factor) * 3); margin-bottom: calc(var(--space-factor) * 3);
} }
}
@media #{$media-query} {
.ExportDialog__preview canvas { .ExportDialog__preview canvas {
max-height: 30vh; max-height: 30vh;
} }
+7 -1
View File
@@ -100,6 +100,7 @@ const ExportModal = ({
exportPadding, exportPadding,
scale, scale,
shouldAddWatermark, shouldAddWatermark,
watermarkText: t("labels.madeWithExcalidraw"),
}); });
// if converting to blob fails, there's some problem that will // if converting to blob fails, there's some problem that will
@@ -175,6 +176,7 @@ const ExportModal = ({
exportPadding, exportPadding,
shouldAddWatermark, shouldAddWatermark,
s, s,
t("labels.madeWithExcalidraw"),
); );
const scaleButtonTitle = `${t( const scaleButtonTitle = `${t(
@@ -262,7 +264,11 @@ export const ExportDialog = ({
ref={triggerButton} ref={triggerButton}
/> />
{modalIsShown && ( {modalIsShown && (
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}> <Dialog
maxWidth={800}
onCloseRequest={handleClose}
title={t("buttons.export")}
>
<ExportModal <ExportModal
elements={elements} elements={elements}
appState={appState} appState={appState}
+5 -5
View File
@@ -1,8 +1,5 @@
@import "../css/_variables"; @import "../css/_variables";
// this is loosely based on the longest hint text
$wide-viewport-width: 1000px;
.excalidraw { .excalidraw {
.HintViewer { .HintViewer {
pointer-events: none; pointer-events: none;
@@ -19,9 +16,12 @@ $wide-viewport-width: 1000px;
color: $oc-gray-6; color: $oc-gray-6;
font-size: 0.8rem; font-size: 0.8rem;
@media #{$is-mobile-query} { @media (min-width: 1200px) {
white-space: pre;
}
@media #{$media-query} {
position: static; position: static;
padding-right: 2em;
} }
> span { > span {
+1 -1
View File
@@ -110,7 +110,7 @@
:root[dir="rtl"] & { :root[dir="rtl"] & {
left: 2px; left: 2px;
} }
@media #{$is-mobile-query} { @media #{$media-query} {
display: none; display: none;
} }
} }
+6 -17
View File
@@ -1,29 +1,18 @@
import React from "react"; import React from "react";
import { LoadingMessage } from "./LoadingMessage"; import { LoadingMessage } from "./LoadingMessage";
import { import { setLanguageFirstTime } from "../i18n";
defaultLang,
Language,
languages,
setLanguageFirstTime,
} from "../i18n";
interface Props { export class InitializeApp extends React.Component<
langCode: Language["code"]; any,
} { isLoading: boolean }
interface State { > {
isLoading: boolean;
}
export class InitializeApp extends React.Component<Props, State> {
public state: { isLoading: boolean } = { public state: { isLoading: boolean } = {
isLoading: true, isLoading: true,
}; };
async componentDidMount() { async componentDidMount() {
const currentLang = await setLanguageFirstTime();
languages.find((lang) => lang.code === this.props.langCode) ||
defaultLang;
await setLanguageFirstTime(currentLang);
this.setState({ this.setState({
isLoading: false, isLoading: false,
}); });
+1 -1
View File
@@ -4,7 +4,7 @@
background-color: var(--bg-color-island); background-color: var(--bg-color-island);
backdrop-filter: saturate(100%) blur(10px); backdrop-filter: saturate(100%) blur(10px);
box-shadow: var(--shadow-island); box-shadow: var(--shadow-island);
border-radius: 4px; border-radius: var(--border-radius-m);
padding: calc(var(--padding) * var(--space-factor)); padding: calc(var(--padding) * var(--space-factor));
position: relative; position: relative;
transition: box-shadow 0.5s ease-in-out; transition: box-shadow 0.5s ease-in-out;
@@ -1,16 +1,16 @@
import React from "react"; import React from "react";
import clsx from "clsx"; import clsx from "clsx";
import * as i18n from "../../i18n"; import * as i18n from "../i18n";
export const LanguageList = ({ export const LanguageList = ({
onChange, onChange,
languages = i18n.languages, languages = i18n.languages,
currentLangCode = i18n.getLanguage().code, currentLanguage = i18n.getLanguage().lng,
floating, floating,
}: { }: {
languages?: { code: string; label: string }[]; languages?: { lng: string; label: string }[];
onChange: (langCode: i18n.Language["code"]) => void; onChange: (value: string) => void;
currentLangCode?: i18n.Language["code"]; currentLanguage?: string;
floating?: boolean; floating?: boolean;
}) => ( }) => (
<React.Fragment> <React.Fragment>
@@ -19,12 +19,12 @@ export const LanguageList = ({
"dropdown-select--floating": floating, "dropdown-select--floating": floating,
})} })}
onChange={({ target }) => onChange(target.value)} onChange={({ target }) => onChange(target.value)}
value={currentLangCode} value={currentLanguage}
aria-label={i18n.t("buttons.selectLanguage")} aria-label={i18n.t("buttons.selectLanguage")}
> >
{languages.map((lang) => ( {languages.map((language) => (
<option key={lang.code} value={lang.code}> <option key={language.lng} value={language.lng}>
{lang.label} {language.label}
</option> </option>
))} ))}
</select> </select>
+5 -17
View File
@@ -7,23 +7,11 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.layer-ui__library-header { .browse-libraries {
display: flex; position: absolute;
align-items: center; right: 12px;
width: 100%; top: 16px;
margin: 2px 0; white-space: nowrap;
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;
}
} }
} }
+77 -85
View File
@@ -19,7 +19,8 @@ import { FixedSideContainer } from "./FixedSideContainer";
import { UserList } from "./UserList"; import { UserList } from "./UserList";
import { LockIcon } from "./LockIcon"; import { LockIcon } from "./LockIcon";
import { ExportDialog, ExportCB } from "./ExportDialog"; import { ExportDialog, ExportCB } from "./ExportDialog";
import { Language, t } from "../i18n"; import { LanguageList } from "./LanguageList";
import { t, languages, setLanguage } from "../i18n";
import { HintViewer } from "./HintViewer"; import { HintViewer } from "./HintViewer";
import useIsMobile from "../is-mobile"; import useIsMobile from "../is-mobile";
@@ -40,7 +41,7 @@ import "./LayerUI.scss";
import { LibraryUnit } from "./LibraryUnit"; import { LibraryUnit } from "./LibraryUnit";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { saveLibraryAsJSON, importLibraryFromJSON } from "../data/json"; import { saveLibraryAsJSON, importLibraryFromJSON } from "../data/json";
import { muteFSAbortError } from "../utils"; import { getDateTime, muteFSAbortError } from "../utils";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle"; import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import clsx from "clsx"; import clsx from "clsx";
import { Library } from "../data/library"; import { Library } from "../data/library";
@@ -50,7 +51,6 @@ import {
EVENT_LIBRARY, EVENT_LIBRARY,
trackEvent, trackEvent,
} from "../analytics"; } from "../analytics";
import { PasteChartDialog } from "./PasteChartDialog";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
@@ -60,17 +60,16 @@ interface LayerUIProps {
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
onCollabButtonClick?: () => void; onCollabButtonClick?: () => void;
onLockToggle: () => void; onLockToggle: () => void;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; onInsertShape: (elements: LibraryItem) => void;
zenModeEnabled: boolean; zenModeEnabled: boolean;
toggleZenMode: () => void; toggleZenMode: () => void;
langCode: Language["code"]; lng: string;
isCollaborating: boolean; isCollaborating: boolean;
onExportToBackend?: ( onExportToBackend?: (
exportedElements: readonly NonDeletedExcalidrawElement[], exportedElements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
canvas: HTMLCanvasElement | null, canvas: HTMLCanvasElement | null,
) => void; ) => void;
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
} }
const useOnClickOutside = ( const useOnClickOutside = (
@@ -124,42 +123,9 @@ const LibraryMenuItems = ({
let addedPendingElements = false; let addedPendingElements = false;
rows.push( 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 <a
className="browse-libraries"
href="https://libraries.excalidraw.com" href="https://libraries.excalidraw.com"
target="_excalidraw_libraries" target="_excalidraw_libraries"
onClick={() => { onClick={() => {
@@ -168,7 +134,48 @@ const LibraryMenuItems = ({
> >
{t("labels.libraries")} {t("labels.libraries")}
</a> </a>
</div>,
<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>
</>,
); );
for (let row = 0; row < numRows; row++) { for (let row = 0; row < numRows; row++) {
@@ -311,12 +318,11 @@ const LayerUI = ({
elements, elements,
onCollabButtonClick, onCollabButtonClick,
onLockToggle, onLockToggle,
onInsertElements, onInsertShape,
zenModeEnabled, zenModeEnabled,
toggleZenMode, toggleZenMode,
isCollaborating, isCollaborating,
onExportToBackend, onExportToBackend,
renderCustomFooter,
}: LayerUIProps) => { }: LayerUIProps) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@@ -346,7 +352,7 @@ const LayerUI = ({
if (canvas) { if (canvas) {
await exportCanvas(type, exportedElements, appState, canvas, { await exportCanvas(type, exportedElements, appState, canvas, {
exportBackground: appState.exportBackground, exportBackground: appState.exportBackground,
name: appState.name, name: appState.name || `${t("labels.untitled")}-${getDateTime()}`,
viewBackgroundColor: appState.viewBackgroundColor, viewBackgroundColor: appState.viewBackgroundColor,
scale, scale,
shouldAddWatermark: appState.shouldAddWatermark, shouldAddWatermark: appState.shouldAddWatermark,
@@ -450,7 +456,7 @@ const LayerUI = ({
<LibraryMenu <LibraryMenu
pendingElements={getSelectedElements(elements, appState)} pendingElements={getSelectedElements(elements, appState)}
onClickOutside={closeLibrary} onClickOutside={closeLibrary}
onInsertShape={onInsertElements} onInsertShape={onInsertShape}
onAddToLibrary={deselectItems} onAddToLibrary={deselectItems}
setAppState={setAppState} setAppState={setAppState}
/> />
@@ -552,7 +558,14 @@ const LayerUI = ({
"transition-right disable-pointerEvents": zenModeEnabled, "transition-right disable-pointerEvents": zenModeEnabled,
})} })}
> >
{renderCustomFooter?.(false)} <LanguageList
onChange={async (lng) => {
await setLanguage(lng);
setAppState({});
}}
languages={languages}
floating
/>
{actionManager.renderAction("toggleShortcuts")} {actionManager.renderAction("toggleShortcuts")}
</div> </div>
<button <button
@@ -579,8 +592,21 @@ const LayerUI = ({
</footer> </footer>
); );
const dialogs = ( 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">
{appState.isLoading && <LoadingMessage />} {appState.isLoading && <LoadingMessage />}
{appState.errorMessage && ( {appState.errorMessage && (
<ErrorDialog <ErrorDialog
@@ -593,41 +619,6 @@ const LayerUI = ({
onClose={() => setAppState({ showShortcutsDialog: false })} 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()} {renderFixedSideContainer()}
{renderBottomAppMenu()} {renderBottomAppMenu()}
{ {
@@ -660,8 +651,9 @@ const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
const nextAppState = getNecessaryObj(next.appState); const nextAppState = getNecessaryObj(next.appState);
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[]; const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
return ( return (
prev.langCode === next.langCode && prev.lng === next.lng &&
prev.elements === next.elements && prev.elements === next.elements &&
keys.every((key) => prevAppState[key] === nextAppState[key]) keys.every((key) => prevAppState[key] === nextAppState[key])
); );
+6 -6
View File
@@ -1,13 +1,13 @@
import React, { useRef, useEffect, useState } from "react";
import clsx from "clsx"; import clsx from "clsx";
import oc from "open-color"; import { exportToSvg } from "../scene/export";
import React, { useEffect, useRef, useState } from "react";
import { close } from "../components/icons"; import { close } from "../components/icons";
import { MIME_TYPES } from "../constants";
import "./LibraryUnit.scss";
import { t } from "../i18n"; import { t } from "../i18n";
import useIsMobile from "../is-mobile"; import useIsMobile from "../is-mobile";
import { exportToSvg } from "../scene/export";
import { LibraryItem } from "../types"; import { LibraryItem } from "../types";
import "./LibraryUnit.scss"; import { MIME_TYPES } from "../constants";
// fa-plus // fa-plus
const PLUS_ICON = ( const PLUS_ICON = (
@@ -38,7 +38,7 @@ export const LibraryUnit = ({
} }
const svg = exportToSvg(elementsToRender, { const svg = exportToSvg(elementsToRender, {
exportBackground: false, exportBackground: false,
viewBackgroundColor: oc.white, viewBackgroundColor: "#fff",
shouldAddWatermark: false, shouldAddWatermark: false,
}); });
for (const child of ref.current!.children) { for (const child of ref.current!.children) {
+13 -4
View File
@@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import { AppState } from "../types"; import { AppState } from "../types";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { t } from "../i18n"; import { t, setLanguage } from "../i18n";
import Stack from "./Stack"; import Stack from "./Stack";
import { LanguageList } from "./LanguageList";
import { showSelectedShapeActions } from "../element"; import { showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { FixedSideContainer } from "./FixedSideContainer"; import { FixedSideContainer } from "./FixedSideContainer";
@@ -14,6 +15,7 @@ import { Section } from "./Section";
import CollabButton from "./CollabButton"; import CollabButton from "./CollabButton";
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars"; import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
import { LockIcon } from "./LockIcon"; import { LockIcon } from "./LockIcon";
import { LoadingMessage } from "./LoadingMessage";
import { UserList } from "./UserList"; import { UserList } from "./UserList";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle"; import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import { EVENT_ACTION, trackEvent } from "../analytics"; import { EVENT_ACTION, trackEvent } from "../analytics";
@@ -29,7 +31,6 @@ type MobileMenuProps = {
onLockToggle: () => void; onLockToggle: () => void;
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
isCollaborating: boolean; isCollaborating: boolean;
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
}; };
export const MobileMenu = ({ export const MobileMenu = ({
@@ -43,9 +44,9 @@ export const MobileMenu = ({
onLockToggle, onLockToggle,
canvas, canvas,
isCollaborating, isCollaborating,
renderCustomFooter,
}: MobileMenuProps) => ( }: MobileMenuProps) => (
<> <>
{appState.isLoading && <LoadingMessage />}
<FixedSideContainer side="top"> <FixedSideContainer side="top">
<Section heading="shapes"> <Section heading="shapes">
{(heading) => ( {(heading) => (
@@ -103,7 +104,15 @@ export const MobileMenu = ({
appState={appState} appState={appState}
setAppState={setAppState} setAppState={setAppState}
/> />
{renderCustomFooter?.(true)} <fieldset>
<legend>{t("labels.language")}</legend>
<LanguageList
onChange={async (lng) => {
await setLanguage(lng);
setAppState({});
}}
/>
</fieldset>
<fieldset> <fieldset>
<legend>{t("labels.collaborators")}</legend> <legend>{t("labels.collaborators")}</legend>
<UserList mobile> <UserList mobile>
+8 -10
View File
@@ -30,26 +30,18 @@
z-index: 2; z-index: 2;
width: 100%; width: 100%;
max-width: var(--max-width); max-width: var(--max-width);
max-height: 100%;
opacity: 0; opacity: 0;
transform: translateY(10px); transform: translateY(10px);
animation: Modal__content_fade-in 0.1s ease-out 0.05s forwards; animation: Modal__content_fade-in 0.1s ease-out 0.05s forwards;
position: relative; position: relative;
overflow-y: auto;
// for modals, reset blurry bg // for modals, reset blurry bg
background: var(--bg-color-island); background: var(--bg-color-island);
backdrop-filter: none; backdrop-filter: none;
border: 1px solid var(--dialog-border); @media #{$media-query} {
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
border-radius: 6px;
@media #{$is-mobile-query} {
max-width: 100%; max-width: 100%;
border: 0;
border-radius: 0;
} }
} }
@@ -76,7 +68,13 @@
} }
} }
@media #{$is-mobile-query} { .Modal__close--floating {
position: absolute;
right: calc(var(--space-factor) * 5);
top: calc(var(--space-factor) * 5);
}
@media #{$media-query} {
.Modal { .Modal {
padding: 0; padding: 0;
} }
+5 -1
View File
@@ -36,7 +36,11 @@ export const Modal = (props: {
<div className="Modal__background" onClick={props.onCloseRequest}></div> <div className="Modal__background" onClick={props.onCloseRequest}></div>
<div <div
className="Modal__content" className="Modal__content"
style={{ "--max-width": `${props.maxWidth}px` }} style={{
"--max-width": `${props.maxWidth}px`,
maxHeight: "100%",
overflowY: "scroll",
}}
> >
{props.children} {props.children}
</div> </div>
-46
View File
@@ -1,46 +0,0 @@
@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;
}
}
}
}
-122
View File
@@ -1,122 +0,0 @@
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>
);
};
+5 -1
View File
@@ -130,7 +130,11 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
return ( return (
<> <>
<Dialog onCloseRequest={handleClose} title={t("shortcutsDialog.title")}> <Dialog
maxWidth={900}
onCloseRequest={handleClose}
title={t("shortcutsDialog.title")}
>
<Columns> <Columns>
<Column> <Column>
<ShortcutIsland caption={t("shortcutsDialog.shapes")}> <ShortcutIsland caption={t("shortcutsDialog.shapes")}>
+1
View File
@@ -85,6 +85,7 @@ export const Stats = (props: {
<td>{t("stats.total")}</td> <td>{t("stats.total")}</td>
<td>{nFormatter(storageSizes.total, 1)}</td> <td>{nFormatter(storageSizes.total, 1)}</td>
</tr> </tr>
{selectedElements.length === 1 && ( {selectedElements.length === 1 && (
<tr> <tr>
<th colSpan={2}>{t("stats.element")}</th> <th colSpan={2}>{t("stats.element")}</th>
+1 -5
View File
@@ -142,7 +142,6 @@
user-select: none; user-select: none;
} }
// shrink shape icons on small viewports to make them fit
@media (max-width: 425px) { @media (max-width: 425px) {
.Shape .ToolIcon__icon { .Shape .ToolIcon__icon {
width: 2rem; width: 2rem;
@@ -154,8 +153,6 @@
} }
} }
// 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) { @media (max-width: 760px) {
.ToolIcon.ToolIcon__lock { .ToolIcon.ToolIcon__lock {
display: inline-block; display: inline-block;
@@ -165,7 +162,6 @@
margin-left: 0; margin-left: 0;
border-radius: 20px 0 0 20px; border-radius: 20px 0 0 20px;
z-index: 1;
background-color: var(--button-gray-1); background-color: var(--button-gray-1);
@@ -193,7 +189,7 @@
margin-left: 5px; margin-left: 5px;
margin-top: 1px; margin-top: 1px;
@media #{$is-mobile-query} { @media #{$media-query} {
display: none; display: none;
} }
} }
-2
View File
@@ -88,5 +88,3 @@ export const STORAGE_KEYS = {
export const TAP_TWICE_TIMEOUT = 300; export const TAP_TWICE_TIMEOUT = 300;
export const TOUCH_CTX_MENU_TIMEOUT = 500; export const TOUCH_CTX_MENU_TIMEOUT = 500;
export const TITLE_TIMEOUT = 10000; export const TITLE_TIMEOUT = 10000;
export const SCENE_NAME_FALLBACK = "Untitled";
+1 -2
View File
@@ -1,4 +1,3 @@
@import "open-color/open-color.scss"; @import "open-color/open-color.scss";
// keep up to date with is-mobile.tsx $media-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
+1 -1
View File
@@ -441,7 +441,7 @@
} }
} }
@media #{$is-mobile-query} { @media #{$media-query} {
aside { aside {
display: none; display: none;
} }
+6 -2
View File
@@ -3,6 +3,7 @@
:root { :root {
--bg-color-island: rgba(255, 255, 255, 0.9); --bg-color-island: rgba(255, 255, 255, 0.9);
--popup-background-color: #{$oc-white}; --popup-background-color: #{$oc-white};
--border-radius-m: 4px;
--space-factor: 0.25rem; --space-factor: 0.25rem;
--button-gray-1: #{$oc-gray-2}; --button-gray-1: #{$oc-gray-2};
--button-gray-2: #{$oc-gray-4}; --button-gray-2: #{$oc-gray-4};
@@ -14,6 +15,7 @@
--icon-fill-color: #{$oc-black}; --icon-fill-color: #{$oc-black};
--icon-green-fill-color: #{$oc-green-9}; --icon-green-fill-color: #{$oc-green-9};
--keybinding-color: #{$oc-gray-5}; --keybinding-color: #{$oc-gray-5};
--color-overlay-text-color: #ccc;
--sat: env(safe-area-inset-top); --sat: env(safe-area-inset-top);
--sab: env(safe-area-inset-bottom); --sab: env(safe-area-inset-bottom);
--sal: env(safe-area-inset-left); --sal: env(safe-area-inset-left);
@@ -21,6 +23,8 @@
--text-color-primary: #{$oc-gray-8}; --text-color-primary: #{$oc-gray-8};
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.85)}; --shadow-island: 0 1px 5px #{transparentize($oc-black, 0.85)};
--overlay-background-color: #{transparentize($oc-white, 0.12)}; --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>'); --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}; --focus-highlight-color: #{$oc-blue-2};
--select-highlight-color: #{$oc-blue-5}; --select-highlight-color: #{$oc-blue-5};
@@ -31,7 +35,6 @@
--popup-secondary-background-color: #{$oc-gray-1}; --popup-secondary-background-color: #{$oc-gray-1};
--popup-text-color: #{$oc-black}; --popup-text-color: #{$oc-black};
--popup-text-inverted-color: #{$oc-white}; --popup-text-inverted-color: #{$oc-white};
--dialog-border: #{$oc-gray-6};
} }
.excalidraw { .excalidraw {
@@ -57,8 +60,10 @@
--icon-fill-color: #{$oc-gray-4}; --icon-fill-color: #{$oc-gray-4};
--icon-green-fill-color: #{$oc-green-4}; --icon-green-fill-color: #{$oc-green-4};
--keybinding-color: #{$oc-gray-6}; --keybinding-color: #{$oc-gray-6};
--color-overlay-text-color: #bbb;
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.7)}; --shadow-island: 0 1px 5px #{transparentize($oc-black, 0.7)};
--overlay-background-color: rgba(30, 30, 30, 0.88); --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>'); --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}; --focus-highlight-color: #{$oc-blue-6};
--select-highlight-color: #{$oc-blue-4}; --select-highlight-color: #{$oc-blue-4};
@@ -69,6 +74,5 @@
--popup-secondary-background-color: #222; --popup-secondary-background-color: #222;
--popup-text-color: #{$oc-gray-4}; --popup-text-color: #{$oc-gray-4};
--popup-text-inverted-color: #2c2c2c; --popup-text-inverted-color: #2c2c2c;
--dialog-border: #{$oc-gray-9};
} }
} }
+1
View File
@@ -75,6 +75,7 @@ export const exportCanvas = async (
exportPadding, exportPadding,
scale, scale,
shouldAddWatermark, shouldAddWatermark,
watermarkText: t("labels.madeWithExcalidraw"),
}); });
tempCanvas.style.display = "none"; tempCanvas.style.display = "none";
document.body.appendChild(tempCanvas); document.body.appendChild(tempCanvas);
+3 -2
View File
@@ -8,6 +8,8 @@ import { Library } from "./library";
import { MIME_TYPES } from "../constants"; import { MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element"; import { clearElementsForExport } from "../element";
import { EVENT_LIBRARY, trackEvent } from "../analytics"; import { EVENT_LIBRARY, trackEvent } from "../analytics";
import { t } from "../i18n";
import { getDateTime } from "../utils";
export const serializeAsJSON = ( export const serializeAsJSON = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@@ -33,11 +35,10 @@ export const saveAsJSON = async (
const blob = new Blob([serialized], { const blob = new Blob([serialized], {
type: "application/json", type: "application/json",
}); });
const fileHandle = await fileSave( const fileHandle = await fileSave(
blob, blob,
{ {
fileName: appState.name, fileName: appState.name || `${t("labels.untitled")}-${getDateTime()}`,
description: "Excalidraw file", description: "Excalidraw file",
extensions: [".excalidraw"], extensions: [".excalidraw"],
}, },
-2
View File
@@ -15,7 +15,6 @@ import {
DEFAULT_VERTICAL_ALIGN, DEFAULT_VERTICAL_ALIGN,
} from "../constants"; } from "../constants";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { getNewSceneName } from "../utils";
const getFontFamilyByName = (fontFamilyName: string): FontFamily => { const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) { for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) {
@@ -167,7 +166,6 @@ const restoreAppState = (
return { return {
...nextAppState, ...nextAppState,
name: appState.name ?? localAppState?.name ?? getNewSceneName(),
offsetLeft: appState.offsetLeft || 0, offsetLeft: appState.offsetLeft || 0,
offsetTop: appState.offsetTop || 0, offsetTop: appState.offsetTop || 0,
// Migrates from previous version where appState.zoom was a number // Migrates from previous version where appState.zoom was a number
-1
View File
@@ -49,7 +49,6 @@ export {
dragNewElement, dragNewElement,
} from "./dragElements"; } from "./dragElements";
export { isTextElement, isExcalidrawElement } from "./typeChecks"; export { isTextElement, isExcalidrawElement } from "./typeChecks";
export { textWysiwyg } from "./textWysiwyg";
export { redrawTextBoundingBox } from "./textElement"; export { redrawTextBoundingBox } from "./textElement";
export { export {
getPerfectElementSize, getPerfectElementSize,
-11
View File
@@ -1,7 +1,6 @@
import { Point } from "../types"; import { Point } from "../types";
import { FONT_FAMILY } from "../constants"; import { FONT_FAMILY } from "../constants";
export type ChartType = "bar" | "line";
export type FillStyle = "hachure" | "cross-hatch" | "solid"; export type FillStyle = "hachure" | "cross-hatch" | "solid";
export type FontFamily = keyof typeof FONT_FAMILY; export type FontFamily = keyof typeof FONT_FAMILY;
export type FontString = string & { _brand: "fontString" }; export type FontString = string & { _brand: "fontString" };
@@ -27,21 +26,11 @@ type _ExcalidrawElementBase = Readonly<{
width: number; width: number;
height: number; height: number;
angle: number; angle: number;
/** Random integer used to seed shape generation so that the roughjs shape
doesn't differ across renders. */
seed: number; seed: number;
/** Integer that is sequentially incremented on each change. Used to reconcile
elements during collaboration or when saving to server. */
version: number; 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; versionNonce: number;
isDeleted: boolean; isDeleted: boolean;
/** List of groups the element belongs to.
Ordered from deepest to shallowest. */
groupIds: readonly GroupId[]; groupIds: readonly GroupId[];
/** Ids of (linear) elements that are bound to this element. */
boundElementIds: readonly ExcalidrawLinearElement["id"][] | null; boundElementIds: readonly ExcalidrawLinearElement["id"][] | null;
}>; }>;
+5 -1
View File
@@ -123,7 +123,11 @@ const RoomDialog = ({
); );
}; };
return ( return (
<Dialog small onCloseRequest={handleClose} title={t("labels.createRoom")}> <Dialog
maxWidth={800}
onCloseRequest={handleClose}
title={t("labels.createRoom")}
>
{renderRoomDialog()} {renderRoomDialog()}
</Dialog> </Dialog>
); );
+1 -2
View File
@@ -6,7 +6,6 @@ import {
} from "../../appState"; } from "../../appState";
import { clearElementsForLocalStorage } from "../../element"; import { clearElementsForLocalStorage } from "../../element";
import { STORAGE_KEYS as APP_STORAGE_KEYS } from "../../constants"; import { STORAGE_KEYS as APP_STORAGE_KEYS } from "../../constants";
import { ImportedDataState } from "../../data/types";
export const STORAGE_KEYS = { export const STORAGE_KEYS = {
LOCAL_STORAGE_ELEMENTS: "excalidraw", LOCAL_STORAGE_ELEMENTS: "excalidraw",
@@ -82,7 +81,7 @@ export const importFromLocalStorage = () => {
} }
} }
let appState: ImportedDataState["appState"] = null; let appState = null;
if (savedState) { if (savedState) {
try { try {
appState = { appState = {
+18 -66
View File
@@ -1,16 +1,6 @@
import React, { import React, { useState, useLayoutEffect, useEffect, useRef } from "react";
useState,
useLayoutEffect,
useEffect,
useRef,
useCallback,
} from "react";
import LanguageDetector from "i18next-browser-languagedetector";
import Excalidraw, { import Excalidraw from "../packages/excalidraw/index";
languages,
defaultLang,
} from "../packages/excalidraw/index";
import { import {
getTotalStorageSize, getTotalStorageSize,
@@ -22,7 +12,7 @@ import {
import { ImportedDataState } from "../data/types"; import { ImportedDataState } from "../data/types";
import CollabWrapper, { CollabAPI } from "./collab/CollabWrapper"; import CollabWrapper, { CollabAPI } from "./collab/CollabWrapper";
import { TopErrorBoundary } from "../components/TopErrorBoundary"; import { TopErrorBoundary } from "../components/TopErrorBoundary";
import { Language, t } from "../i18n"; import { t } from "../i18n";
import { exportToBackend, loadScene } from "./data"; import { exportToBackend, loadScene } from "./data";
import { getCollaborationLinkData } from "./data"; import { getCollaborationLinkData } from "./data";
import { EVENT } from "../constants"; import { EVENT } from "../constants";
@@ -39,16 +29,6 @@ import { EVENT_LOAD, EVENT_SHARE, trackEvent } from "../analytics";
import { ErrorDialog } from "../components/ErrorDialog"; import { ErrorDialog } from "../components/ErrorDialog";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { APP_NAME, TITLE_TIMEOUT } from "../constants"; 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< const excalidrawRef: React.MutableRefObject<
MarkRequired<ExcalidrawAPIRefValue, "ready" | "readyPromise"> MarkRequired<ExcalidrawAPIRefValue, "ready" | "readyPromise">
@@ -113,6 +93,7 @@ type Scene = ImportedDataState & { commitToHistory: boolean };
const initializeScene = async (opts: { const initializeScene = async (opts: {
resetScene: ExcalidrawImperativeAPI["resetScene"]; resetScene: ExcalidrawImperativeAPI["resetScene"];
initializeSocketClient: CollabAPI["initializeSocketClient"]; initializeSocketClient: CollabAPI["initializeSocketClient"];
onLateInitialization?: (scene: Scene) => void;
}): Promise<Scene | null> => { }): Promise<Scene | null> => {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const id = searchParams.get("id"); const id = searchParams.get("id");
@@ -143,15 +124,17 @@ const initializeScene = async (opts: {
} else { } else {
// https://github.com/excalidraw/excalidraw/issues/1919 // https://github.com/excalidraw/excalidraw/issues/1919
if (document.hidden) { if (document.hidden) {
return new Promise((resolve, reject) => { window.addEventListener(
window.addEventListener( "focus",
"focus", () =>
() => initializeScene(opts).then(resolve).catch(reject), initializeScene(opts).then((_scene) => {
{ opts?.onLateInitialization?.(_scene || scene);
once: true, }),
}, {
); once: true,
}); },
);
return null;
} }
isCollabScene = false; isCollabScene = false;
@@ -202,8 +185,6 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
height: window.innerHeight, height: window.innerHeight,
}); });
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const currentLangCode = languageDetector.detect() || defaultLang.code;
const [langCode, setLangCode] = useState(currentLangCode);
useLayoutEffect(() => { useLayoutEffect(() => {
const onResize = () => { const onResize = () => {
@@ -241,6 +222,9 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
initializeScene({ initializeScene({
resetScene: excalidrawApi.resetScene, resetScene: excalidrawApi.resetScene,
initializeSocketClient: collab.initializeSocketClient, initializeSocketClient: collab.initializeSocketClient,
onLateInitialization: (scene) => {
initialStatePromiseRef.current.promise.resolve(scene);
},
}).then((scene) => { }).then((scene) => {
initialStatePromiseRef.current.promise.resolve(scene); initialStatePromiseRef.current.promise.resolve(scene);
}); });
@@ -278,10 +262,6 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
}; };
}, [collab.initializeSocketClient]); }, [collab.initializeSocketClient]);
useEffect(() => {
languageDetector.cacheUserLanguage(langCode);
}, [langCode]);
const onChange = ( const onChange = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
@@ -317,32 +297,6 @@ 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 ( return (
<> <>
<Excalidraw <Excalidraw
@@ -356,8 +310,6 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
isCollaborating={collab.isCollaborating} isCollaborating={collab.isCollaborating}
onPointerUpdate={collab.onPointerUpdate} onPointerUpdate={collab.onPointerUpdate}
onExportToBackend={onExportToBackend} onExportToBackend={onExportToBackend}
renderFooter={renderFooter}
langCode={langCode}
/> />
{errorMessage && ( {errorMessage && (
<ErrorDialog <ErrorDialog
+3 -1
View File
@@ -2,6 +2,8 @@ import { AppState } from "./types";
import { ExcalidrawElement } from "./element/types"; import { ExcalidrawElement } from "./element/types";
import { isLinearElement } from "./element/typeChecks"; import { isLinearElement } from "./element/typeChecks";
import { deepCopyElement } from "./element/newElement"; import { deepCopyElement } from "./element/newElement";
import { getDateTime } from "./utils";
import { t } from "./i18n";
export interface HistoryEntry { export interface HistoryEntry {
appState: ReturnType<typeof clearAppStatePropertiesForHistory>; appState: ReturnType<typeof clearAppStatePropertiesForHistory>;
@@ -24,7 +26,7 @@ const clearAppStatePropertiesForHistory = (appState: AppState) => {
viewBackgroundColor: appState.viewBackgroundColor, viewBackgroundColor: appState.viewBackgroundColor,
editingLinearElement: appState.editingLinearElement, editingLinearElement: appState.editingLinearElement,
editingGroupId: appState.editingGroupId, editingGroupId: appState.editingGroupId,
name: appState.name, name: appState.name || `${t("labels.untitled")}-${getDateTime()}`,
}; };
}; };
+72 -55
View File
@@ -1,85 +1,93 @@
import LanguageDetector from "i18next-browser-languagedetector";
import { EVENT_CHANGE, trackEvent } from "./analytics"; import { EVENT_CHANGE, trackEvent } from "./analytics";
import fallbackLangData from "./locales/en.json"; import fallbackLanguageData from "./locales/en.json";
import percentages from "./locales/percentages.json"; import percentages from "./locales/percentages.json";
const COMPLETION_THRESHOLD = 85; const COMPLETION_THRESHOLD = 85;
export interface Language { interface Language {
code: string; lng: string;
label: string; label: string;
rtl?: boolean; rtl?: boolean;
} }
const allLanguages: Language[] = [ const allLanguages: Language[] = [
{ code: "ar-SA", label: "العربية", rtl: true }, { lng: "ar-SA", label: "العربية", rtl: true },
{ code: "bg-BG", label: "Български" }, { lng: "bg-BG", label: "Български" },
{ code: "ca-ES", label: "Catalan" }, { lng: "ca-ES", label: "Catalan" },
{ code: "de-DE", label: "Deutsch" }, { lng: "de-DE", label: "Deutsch" },
{ code: "el-GR", label: "Ελληνικά" }, { lng: "el-GR", label: "Ελληνικά" },
{ code: "es-ES", label: "Español" }, { lng: "es-ES", label: "Español" },
{ code: "fa-IR", label: "فارسی", rtl: true }, { lng: "fa-IR", label: "فارسی", rtl: true },
{ code: "fi-FI", label: "Suomi" }, { lng: "fi-FI", label: "Suomi" },
{ code: "fr-FR", label: "Français" }, { lng: "fr-FR", label: "Français" },
{ code: "he-IL", label: "עברית", rtl: true }, { lng: "he-IL", label: "עברית", rtl: true },
{ code: "hi-IN", label: "हिन्दी" }, { lng: "hi-IN", label: "हिन्दी" },
{ code: "hu-HU", label: "Magyar" }, { lng: "hu-HU", label: "Magyar" },
{ code: "id-ID", label: "Bahasa Indonesia" }, { lng: "id-ID", label: "Bahasa Indonesia" },
{ code: "it-IT", label: "Italiano" }, { lng: "it-IT", label: "Italiano" },
{ code: "ja-JP", label: "日本語" }, { lng: "ja-JP", label: "日本語" },
{ code: "ko-KR", label: "한국어" }, { lng: "ko-KR", label: "한국어" },
{ code: "my-MM", label: "Burmese" }, { lng: "my-MM", label: "Burmese" },
{ code: "nb-NO", label: "Norsk bokmål" }, { lng: "nb-NO", label: "Norsk bokmål" },
{ code: "nl-NL", label: "Nederlands" }, { lng: "nl-NL", label: "Nederlands" },
{ code: "nn-NO", label: "Norsk nynorsk" }, { lng: "nn-NO", label: "Norsk nynorsk" },
{ code: "pl-PL", label: "Polski" }, { lng: "pl-PL", label: "Polski" },
{ code: "pt-BR", label: "Português Brasileiro" }, { lng: "pt-BR", label: "Português Brasileiro" },
{ code: "pt-PT", label: "Português" }, { lng: "pt-PT", label: "Português" },
{ code: "ro-RO", label: "Română" }, { lng: "ro-RO", label: "Română" },
{ code: "ru-RU", label: "Русский" }, { lng: "ru-RU", label: "Русский" },
{ code: "sk-SK", label: "Slovenčina" }, { lng: "sk-SK", label: "Slovenčina" },
{ code: "sv-SE", label: "Svenska" }, { lng: "sv-SE", label: "Svenska" },
{ code: "tr-TR", label: "Türkçe" }, { lng: "tr-TR", label: "Türkçe" },
{ code: "uk-UA", label: "Українська" }, { lng: "uk-UA", label: "Українська" },
{ code: "zh-CN", label: "简体中文" }, { lng: "zh-CN", label: "简体中文" },
{ code: "zh-TW", label: "繁體中文" }, { lng: "zh-TW", label: "繁體中文" },
]; ];
export const defaultLang = { code: "en", label: "English" }; export const languages: Language[] = [{ lng: "en", label: "English" }]
export const languages: Language[] = [defaultLang]
.concat( .concat(
allLanguages.sort((left, right) => (left.label > right.label ? 1 : -1)), allLanguages.sort((left, right) => (left.label > right.label ? 1 : -1)),
) )
.filter( .filter(
(lang) => (lang) =>
(percentages as Record<string, number>)[lang.code] >= (percentages as Record<string, number>)[lang.lng] >= COMPLETION_THRESHOLD,
COMPLETION_THRESHOLD,
); );
let currentLang: Language = defaultLang; let currentLanguage = languages[0];
let currentLangData = {}; let currentLanguageData = {};
const fallbackLanguage = languages[0];
export const setLanguage = async (lang: Language) => { export const setLanguage = async (newLng: string | undefined) => {
currentLang = lang; currentLanguage =
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr"; languages.find((language) => language.lng === newLng) || fallbackLanguage;
currentLangData = await import( document.documentElement.dir = currentLanguage.rtl ? "rtl" : "ltr";
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json`
currentLanguageData = await import(
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLanguage.lng}.json`
); );
trackEvent(EVENT_CHANGE, "language", currentLang.code); languageDetector.cacheUserLanguage(currentLanguage.lng);
trackEvent(EVENT_CHANGE, "language", currentLanguage.lng);
}; };
export const setLanguageFirstTime = async (lang: Language) => { export const setLanguageFirstTime = async () => {
currentLang = lang; const newLng: string | undefined = languageDetector.detect();
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
currentLangData = await import( currentLanguage =
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json` languages.find((language) => language.lng === newLng) || fallbackLanguage;
document.documentElement.dir = currentLanguage.rtl ? "rtl" : "ltr";
currentLanguageData = await import(
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLanguage.lng}.json`
); );
languageDetector.cacheUserLanguage(currentLanguage.lng);
}; };
export const getLanguage = () => currentLang; export const getLanguage = () => currentLanguage;
const findPartsForData = (data: any, parts: string[]) => { const findPartsForData = (data: any, parts: string[]) => {
for (let index = 0; index < parts.length; ++index) { for (let index = 0; index < parts.length; ++index) {
@@ -98,8 +106,8 @@ const findPartsForData = (data: any, parts: string[]) => {
export const t = (path: string, replacement?: { [key: string]: string }) => { export const t = (path: string, replacement?: { [key: string]: string }) => {
const parts = path.split("."); const parts = path.split(".");
let translation = let translation =
findPartsForData(currentLangData, parts) || findPartsForData(currentLanguageData, parts) ||
findPartsForData(fallbackLangData, parts); findPartsForData(fallbackLanguageData, parts);
if (translation === undefined) { if (translation === undefined) {
throw new Error(`Can't find translation for ${path}`); throw new Error(`Can't find translation for ${path}`);
} }
@@ -111,3 +119,12 @@ export const t = (path: string, replacement?: { [key: string]: string }) => {
} }
return translation; return translation;
}; };
const languageDetector = new LanguageDetector();
languageDetector.init({
languageUtils: {
formatLanguageCode: (lng: string) => lng,
isWhitelisted: () => true,
},
checkWhitelist: false,
});
-2
View File
@@ -1,6 +1,5 @@
import { exportToCanvas } from "./scene/export"; import { exportToCanvas } from "./scene/export";
import { getDefaultAppState } from "./appState"; import { getDefaultAppState } from "./appState";
import { SCENE_NAME_FALLBACK } from "./constants";
const { registerFont, createCanvas } = require("canvas"); const { registerFont, createCanvas } = require("canvas");
@@ -62,7 +61,6 @@ const canvas = exportToCanvas(
elements as any, elements as any,
{ {
...getDefaultAppState(), ...getDefaultAppState(),
name: SCENE_NAME_FALLBACK,
offsetTop: 0, offsetTop: 0,
offsetLeft: 0, offsetLeft: 0,
}, },
-1
View File
@@ -11,7 +11,6 @@ export const IsMobileProvider = ({
if (!query.current) { if (!query.current) {
query.current = window.matchMedia query.current = window.matchMedia
? window.matchMedia( ? window.matchMedia(
// keep up to date with _variables.scss
"(max-width: 640px), (max-height: 500px) and (max-width: 1000px)", "(max-width: 640px), (max-height: 500px) and (max-width: 1000px)",
) )
: (({ : (({
-1
View File
@@ -1,7 +1,6 @@
{ {
"labels": { "labels": {
"paste": "Paste", "paste": "Paste",
"pasteCharts": "Paste charts",
"selectAll": "Select all", "selectAll": "Select all",
"multiSelect": "Add element to selection", "multiSelect": "Add element to selection",
"moveCanvas": "Move canvas", "moveCanvas": "Move canvas",
-8
View File
@@ -16,9 +16,6 @@ Please add the latest change on the top under the correct section.
### Features ### 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 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) - 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) - Insert Library items in the middle of the screen [#2527](https://github.com/excalidraw/excalidraw/pull/2527)
@@ -30,7 +27,6 @@ Please add the latest change on the top under the correct section.
### Fixes ### 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) - 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) - 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) - Fix scroll-to-center on init for non-zero canvas offsets [#2445](https://github.com/excalidraw/excalidraw/pull/2445)
@@ -55,10 +51,6 @@ 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) - 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 ## 0.1.1
#### Fix #### Fix
-20
View File
@@ -139,8 +139,6 @@ export default function App() {
| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode | | [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode |
| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. | | [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. |
| [`onExportToBackend`](#onExportToBackend) | Function | | Callback triggered when link button is clicked on export dialog | | [`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` #### `width`
@@ -272,21 +270,3 @@ 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. 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. 2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/4c90ea5667d29effe8ec4a115e49efc7c340cdb3/src/types.ts#L33) of the scene.
3. `canvas`: The `HTMLCanvasElement` 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).
+1 -7
View File
@@ -8,7 +8,6 @@ import "../../css/styles.scss";
import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types"; import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
import { IsMobileProvider } from "../../is-mobile"; import { IsMobileProvider } from "../../is-mobile";
import { defaultLang } from "../../i18n";
const Excalidraw = (props: ExcalidrawProps) => { const Excalidraw = (props: ExcalidrawProps) => {
const { const {
@@ -24,8 +23,6 @@ const Excalidraw = (props: ExcalidrawProps) => {
isCollaborating, isCollaborating,
onPointerUpdate, onPointerUpdate,
onExportToBackend, onExportToBackend,
renderFooter,
langCode = defaultLang.code,
} = props; } = props;
useEffect(() => { useEffect(() => {
@@ -47,7 +44,7 @@ const Excalidraw = (props: ExcalidrawProps) => {
}, []); }, []);
return ( return (
<InitializeApp langCode={langCode}> <InitializeApp>
<IsMobileProvider> <IsMobileProvider>
<App <App
width={width} width={width}
@@ -62,8 +59,6 @@ const Excalidraw = (props: ExcalidrawProps) => {
isCollaborating={isCollaborating} isCollaborating={isCollaborating}
onPointerUpdate={onPointerUpdate} onPointerUpdate={onPointerUpdate}
onExportToBackend={onExportToBackend} onExportToBackend={onExportToBackend}
renderFooter={renderFooter}
langCode={langCode}
/> />
</IsMobileProvider> </IsMobileProvider>
</InitializeApp> </InitializeApp>
@@ -99,4 +94,3 @@ export {
getSyncableElements, getSyncableElements,
getElementMap, getElementMap,
} from "../../element"; } from "../../element";
export { defaultLang, languages } from "../../i18n";
+199 -75
View File
@@ -1149,12 +1149,6 @@
"to-fast-properties": "^2.0.0" "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": { "@polka/url": {
"version": "1.0.0-next.11", "version": "1.0.0-next.11",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.11.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.11.tgz",
@@ -1375,18 +1369,18 @@
} }
}, },
"@webpack-cli/info": { "@webpack-cli/info": {
"version": "1.2.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.1.0.tgz",
"integrity": "sha512-fLnDML5HZ5AEKzHul8xLAksoKN2cibu6MgonkUj8R9V7bbeVRkd1XbGEGWrAUNYHbX1jcqCsDEpBviE5StPMzQ==", "integrity": "sha512-uNWSdaYHc+f3LdIZNwhdhkjjLDDl3jP2+XBqAq9H8DjrJUvlOKdP8TNruy1yEaDfgpAIgbSAN7pye4FEHg9tYQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"envinfo": "^7.7.3" "envinfo": "^7.7.3"
} }
}, },
"@webpack-cli/serve": { "@webpack-cli/serve": {
"version": "1.2.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.1.0.tgz",
"integrity": "sha512-Zj1z6AyS+vqV6Hfi7ngCjFGdHV5EwZNIHo6QfFTNe9PyW+zBU1zJ9BiOW1pmUEq950RC4+Dym6flyA/61/vhyw==", "integrity": "sha512-7RfnMXCpJ/NThrhq4gYQYILB18xWyoQcBey81oIyVbmgbc6m5ZHHyFK+DyH7pLHJf0p14MxL4mTsoPAgBSTpIg==",
"dev": true "dev": true
}, },
"@xtuc/ieee754": { "@xtuc/ieee754": {
@@ -1452,6 +1446,12 @@
"color-convert": "^1.9.0" "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": { "babel-code-frame": {
"version": "6.26.0", "version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@@ -1761,6 +1761,18 @@
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
"dev": true "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": { "commander": {
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -1900,6 +1912,12 @@
"ms": "2.1.2" "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": { "define-properties": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@@ -1927,6 +1945,15 @@
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true "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": { "enhanced-resolve": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz",
@@ -1954,9 +1981,9 @@
"dev": true "dev": true
}, },
"errno": { "errno": {
"version": "0.1.8", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
"dev": true, "dev": true,
"requires": { "requires": {
"prr": "~1.0.1" "prr": "~1.0.1"
@@ -2020,19 +2047,19 @@
"dev": true "dev": true
}, },
"execa": { "execa": {
"version": "5.0.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
"integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==", "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
"dev": true, "dev": true,
"requires": { "requires": {
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.0",
"get-stream": "^6.0.0", "get-stream": "^5.0.0",
"human-signals": "^2.1.0", "human-signals": "^1.1.1",
"is-stream": "^2.0.0", "is-stream": "^2.0.0",
"merge-stream": "^2.0.0", "merge-stream": "^2.0.0",
"npm-run-path": "^4.0.1", "npm-run-path": "^4.0.0",
"onetime": "^5.1.2", "onetime": "^5.1.0",
"signal-exit": "^3.0.3", "signal-exit": "^3.0.2",
"strip-final-newline": "^2.0.0" "strip-final-newline": "^2.0.0"
} }
}, },
@@ -2048,12 +2075,6 @@
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true "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": { "file-loader": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
@@ -2142,10 +2163,13 @@
} }
}, },
"get-stream": { "get-stream": {
"version": "6.0.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true "dev": true,
"requires": {
"pump": "^3.0.0"
}
}, },
"glob-to-regexp": { "glob-to-regexp": {
"version": "0.4.1", "version": "0.4.1",
@@ -2205,9 +2229,9 @@
"dev": true "dev": true
}, },
"human-signals": { "human-signals": {
"version": "2.1.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
"dev": true "dev": true
}, },
"icss-utils": { "icss-utils": {
@@ -2224,6 +2248,51 @@
"requires": { "requires": {
"pkg-dir": "^4.2.0", "pkg-dir": "^4.2.0",
"resolve-cwd": "^3.0.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": { "indexes-of": {
@@ -2253,15 +2322,6 @@
"loose-envify": "^1.0.0" "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": { "is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -2353,6 +2413,12 @@
"integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==", "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==",
"dev": true "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": { "loader-runner": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.1.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.1.0.tgz",
@@ -2464,18 +2530,18 @@
"dev": true "dev": true
}, },
"mime-db": { "mime-db": {
"version": "1.45.0", "version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==",
"dev": true "dev": true
}, },
"mime-types": { "mime-types": {
"version": "2.1.28", "version": "2.1.27",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
"dev": true, "dev": true,
"requires": { "requires": {
"mime-db": "1.45.0" "mime-db": "1.44.0"
} }
}, },
"mimic-fn": { "mimic-fn": {
@@ -2576,6 +2642,15 @@
"object-keys": "^1.1.1" "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": { "onetime": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@@ -2732,6 +2807,16 @@
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
"dev": true "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": { "punycode": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -2771,6 +2856,12 @@
"resolve": "^1.9.0" "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": { "regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -2839,12 +2930,11 @@
} }
}, },
"resolve": { "resolve": {
"version": "1.19.0", "version": "1.17.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
"dev": true, "dev": true,
"requires": { "requires": {
"is-core-module": "^2.1.0",
"path-parse": "^1.0.6" "path-parse": "^1.0.6"
} }
}, },
@@ -3036,6 +3126,18 @@
"has-flag": "^3.0.0" "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": { "tapable": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
@@ -3125,9 +3227,9 @@
"dev": true "dev": true
}, },
"ts-loader": { "ts-loader": {
"version": "8.0.13", "version": "8.0.12",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.13.tgz", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.12.tgz",
"integrity": "sha512-1o1nO6aqouA23d2nlcMSEyPMAWRhnYUU0EQUJSc60E0TUyBNX792RHFYUN1ZM29vhMUNayrsbj2UVdZwKhXCDA==", "integrity": "sha512-UIivVfGVJDdwwjgSrbtcL9Nf10c1BWnL1mxAQUVcnhNIn/P9W3nP5v60Z0aBMtc7ZrE11lMmU6+5jSgAXmGaYw==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "^2.3.0", "chalk": "^2.3.0",
@@ -3151,6 +3253,12 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true "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": { "unicode-canonical-property-names-ecmascript": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -3217,9 +3325,9 @@
} }
}, },
"webpack": { "webpack": {
"version": "5.11.1", "version": "5.11.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.11.1.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.11.0.tgz",
"integrity": "sha512-tNUIdAmYJv+nupRs/U/gqmADm6fgrf5xE+rSlSsf2PgsGO7j2WG7ccU6AWNlOJlHFl+HnmXlBmHIkiLf+XA9mQ==", "integrity": "sha512-ubWv7iP54RqAC/VjixgpnLLogCFbAfSOREcSWnnOlZEU8GICC5eKmJSu6YEnph2N2amKqY9rvxSwgyHxVqpaRw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/eslint-scope": "^3.7.0", "@types/eslint-scope": "^3.7.0",
@@ -3249,13 +3357,13 @@
}, },
"dependencies": { "dependencies": {
"enhanced-resolve": { "enhanced-resolve": {
"version": "5.4.1", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.4.1.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.4.0.tgz",
"integrity": "sha512-4GbyIMzYktTFoRSmkbgZ1LU+RXwf4AQ8Z+rSuuh1dC8plp0PPeaWvx6+G4hh4KnUJ48VoxKbNyA1QQQIUpXjYA==", "integrity": "sha512-ZmqfWURB2lConOBM1JdCVfPyMRv5RdKWktLXO6123p97ovVm2CLBgw9t5MBj3jJWA6eHyOeIws9iJQoGFR4euQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"graceful-fs": "^4.2.4", "graceful-fs": "^4.2.4",
"tapable": "^2.2.0" "tapable": "^2.0.0"
} }
}, },
"find-up": { "find-up": {
@@ -3414,21 +3522,21 @@
} }
}, },
"webpack-cli": { "webpack-cli": {
"version": "4.3.1", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.3.1.tgz", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.2.0.tgz",
"integrity": "sha512-/F4+9QNZM/qKzzL9/06Am8NXIkGV+/NqQ62Dx7DSqudxxpAgBqYn6V7+zp+0Y7JuWksKUbczRY3wMTd+7Uj6OA==", "integrity": "sha512-EIl3k88vaF4fSxWSgtAQR+VwicfLMTZ9amQtqS4o+TDPW9HGaEpbFBbAZ4A3ZOT5SOnMxNOzROsSTPiE8tBJPA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/info": "^1.1.0",
"@webpack-cli/info": "^1.2.1", "@webpack-cli/serve": "^1.1.0",
"@webpack-cli/serve": "^1.2.1",
"colorette": "^1.2.1", "colorette": "^1.2.1",
"command-line-usage": "^6.1.0",
"commander": "^6.2.0", "commander": "^6.2.0",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"execa": "^5.0.0", "execa": "^4.1.0",
"fastest-levenshtein": "^1.0.12",
"import-local": "^3.0.2", "import-local": "^3.0.2",
"interpret": "^2.2.0", "interpret": "^2.2.0",
"leven": "^3.1.0",
"rechoir": "^0.7.0", "rechoir": "^0.7.0",
"v8-compile-cache": "^2.2.0", "v8-compile-cache": "^2.2.0",
"webpack-merge": "^4.2.2" "webpack-merge": "^4.2.2"
@@ -3478,6 +3586,22 @@
"isexe": "^2.0.0" "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": { "ws": {
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",
+3 -3
View File
@@ -57,10 +57,10 @@
"mini-css-extract-plugin": "1.3.3", "mini-css-extract-plugin": "1.3.3",
"sass-loader": "10.1.0", "sass-loader": "10.1.0",
"terser-webpack-plugin": "5.0.3", "terser-webpack-plugin": "5.0.3",
"ts-loader": "8.0.13", "ts-loader": "8.0.12",
"webpack": "5.11.1", "webpack": "5.11.0",
"webpack-bundle-analyzer": "4.3.0", "webpack-bundle-analyzer": "4.3.0",
"webpack-cli": "4.3.1" "webpack-cli": "4.2.0"
}, },
"bugs": "https://github.com/excalidraw/excalidraw/issues", "bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw", "repository": "https://github.com/excalidraw/excalidraw",
+1 -1
View File
@@ -1 +1 @@
export { exportToBlob, exportToSvg, exportToCanvas } from "../utils.ts"; export { exportToBlob, exportToSvg, exportToCanvas } from "./utils";
+154 -65
View File
@@ -1064,12 +1064,6 @@
"to-fast-properties": "^2.0.0" "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": { "@polka/url": {
"version": "1.0.0-next.11", "version": "1.0.0-next.11",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.11.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.11.tgz",
@@ -1109,9 +1103,9 @@
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "14.14.19", "version": "14.14.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.19.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.14.tgz",
"integrity": "sha512-4nhBPStMK04rruRVtVc6cDqhu7S9GZai0fpXgPXrFpcPX6Xul8xnrjSdGB4KPBVYG/R5+fXWdCM8qBoiULWGPQ==", "integrity": "sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ==",
"dev": true "dev": true
}, },
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
@@ -1290,18 +1284,18 @@
} }
}, },
"@webpack-cli/info": { "@webpack-cli/info": {
"version": "1.2.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.1.0.tgz",
"integrity": "sha512-fLnDML5HZ5AEKzHul8xLAksoKN2cibu6MgonkUj8R9V7bbeVRkd1XbGEGWrAUNYHbX1jcqCsDEpBviE5StPMzQ==", "integrity": "sha512-uNWSdaYHc+f3LdIZNwhdhkjjLDDl3jP2+XBqAq9H8DjrJUvlOKdP8TNruy1yEaDfgpAIgbSAN7pye4FEHg9tYQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"envinfo": "^7.7.3" "envinfo": "^7.7.3"
} }
}, },
"@webpack-cli/serve": { "@webpack-cli/serve": {
"version": "1.2.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.1.0.tgz",
"integrity": "sha512-Zj1z6AyS+vqV6Hfi7ngCjFGdHV5EwZNIHo6QfFTNe9PyW+zBU1zJ9BiOW1pmUEq950RC4+Dym6flyA/61/vhyw==", "integrity": "sha512-7RfnMXCpJ/NThrhq4gYQYILB18xWyoQcBey81oIyVbmgbc6m5ZHHyFK+DyH7pLHJf0p14MxL4mTsoPAgBSTpIg==",
"dev": true "dev": true
}, },
"@xtuc/ieee754": { "@xtuc/ieee754": {
@@ -1367,6 +1361,12 @@
"color-convert": "^1.9.0" "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": { "babel-code-frame": {
"version": "6.26.0", "version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@@ -1670,6 +1670,18 @@
"integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==",
"dev": true "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": { "commander": {
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -1750,6 +1762,12 @@
"ms": "2.1.2" "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": { "define-properties": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@@ -1777,6 +1795,15 @@
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true "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": { "enhanced-resolve": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz",
@@ -1804,9 +1831,9 @@
"dev": true "dev": true
}, },
"errno": { "errno": {
"version": "0.1.8", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
"dev": true, "dev": true,
"requires": { "requires": {
"prr": "~1.0.1" "prr": "~1.0.1"
@@ -1870,19 +1897,19 @@
"dev": true "dev": true
}, },
"execa": { "execa": {
"version": "5.0.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
"integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==", "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
"dev": true, "dev": true,
"requires": { "requires": {
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.0",
"get-stream": "^6.0.0", "get-stream": "^5.0.0",
"human-signals": "^2.1.0", "human-signals": "^1.1.1",
"is-stream": "^2.0.0", "is-stream": "^2.0.0",
"merge-stream": "^2.0.0", "merge-stream": "^2.0.0",
"npm-run-path": "^4.0.1", "npm-run-path": "^4.0.0",
"onetime": "^5.1.2", "onetime": "^5.1.0",
"signal-exit": "^3.0.3", "signal-exit": "^3.0.2",
"strip-final-newline": "^2.0.0" "strip-final-newline": "^2.0.0"
} }
}, },
@@ -1898,12 +1925,6 @@
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true "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": { "file-loader": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
@@ -1992,10 +2013,13 @@
} }
}, },
"get-stream": { "get-stream": {
"version": "6.0.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true "dev": true,
"requires": {
"pump": "^3.0.0"
}
}, },
"glob-to-regexp": { "glob-to-regexp": {
"version": "0.4.1", "version": "0.4.1",
@@ -2055,9 +2079,9 @@
"dev": true "dev": true
}, },
"human-signals": { "human-signals": {
"version": "2.1.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
"dev": true "dev": true
}, },
"import-local": { "import-local": {
@@ -2185,6 +2209,12 @@
"minimist": "^1.2.5" "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": { "loader-runner": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.1.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.1.0.tgz",
@@ -2287,18 +2317,18 @@
"dev": true "dev": true
}, },
"mime-db": { "mime-db": {
"version": "1.45.0", "version": "1.44.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==",
"dev": true "dev": true
}, },
"mime-types": { "mime-types": {
"version": "2.1.28", "version": "2.1.27",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
"dev": true, "dev": true,
"requires": { "requires": {
"mime-db": "1.45.0" "mime-db": "1.44.0"
} }
}, },
"mimic-fn": { "mimic-fn": {
@@ -2358,6 +2388,15 @@
"object-keys": "^1.1.1" "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": { "onetime": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@@ -2442,6 +2481,16 @@
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
"dev": true "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": { "punycode": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -2481,6 +2530,12 @@
"resolve": "^1.9.0" "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": { "regenerate": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -2700,6 +2755,18 @@
"has-flag": "^3.0.0" "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": { "tapable": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz",
@@ -2789,9 +2856,9 @@
"dev": true "dev": true
}, },
"ts-loader": { "ts-loader": {
"version": "8.0.13", "version": "8.0.12",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.13.tgz", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.12.tgz",
"integrity": "sha512-1o1nO6aqouA23d2nlcMSEyPMAWRhnYUU0EQUJSc60E0TUyBNX792RHFYUN1ZM29vhMUNayrsbj2UVdZwKhXCDA==", "integrity": "sha512-UIivVfGVJDdwwjgSrbtcL9Nf10c1BWnL1mxAQUVcnhNIn/P9W3nP5v60Z0aBMtc7ZrE11lMmU6+5jSgAXmGaYw==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "^2.3.0", "chalk": "^2.3.0",
@@ -2815,6 +2882,12 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true "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": { "unicode-canonical-property-names-ecmascript": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@@ -2875,9 +2948,9 @@
} }
}, },
"webpack": { "webpack": {
"version": "5.11.1", "version": "5.11.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.11.1.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.11.0.tgz",
"integrity": "sha512-tNUIdAmYJv+nupRs/U/gqmADm6fgrf5xE+rSlSsf2PgsGO7j2WG7ccU6AWNlOJlHFl+HnmXlBmHIkiLf+XA9mQ==", "integrity": "sha512-ubWv7iP54RqAC/VjixgpnLLogCFbAfSOREcSWnnOlZEU8GICC5eKmJSu6YEnph2N2amKqY9rvxSwgyHxVqpaRw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/eslint-scope": "^3.7.0", "@types/eslint-scope": "^3.7.0",
@@ -2907,13 +2980,13 @@
}, },
"dependencies": { "dependencies": {
"enhanced-resolve": { "enhanced-resolve": {
"version": "5.4.1", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.4.1.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.4.0.tgz",
"integrity": "sha512-4GbyIMzYktTFoRSmkbgZ1LU+RXwf4AQ8Z+rSuuh1dC8plp0PPeaWvx6+G4hh4KnUJ48VoxKbNyA1QQQIUpXjYA==", "integrity": "sha512-ZmqfWURB2lConOBM1JdCVfPyMRv5RdKWktLXO6123p97ovVm2CLBgw9t5MBj3jJWA6eHyOeIws9iJQoGFR4euQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"graceful-fs": "^4.2.4", "graceful-fs": "^4.2.4",
"tapable": "^2.2.0" "tapable": "^2.0.0"
} }
}, },
"find-up": { "find-up": {
@@ -3056,21 +3129,21 @@
} }
}, },
"webpack-cli": { "webpack-cli": {
"version": "4.3.1", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.3.1.tgz", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.2.0.tgz",
"integrity": "sha512-/F4+9QNZM/qKzzL9/06Am8NXIkGV+/NqQ62Dx7DSqudxxpAgBqYn6V7+zp+0Y7JuWksKUbczRY3wMTd+7Uj6OA==", "integrity": "sha512-EIl3k88vaF4fSxWSgtAQR+VwicfLMTZ9amQtqS4o+TDPW9HGaEpbFBbAZ4A3ZOT5SOnMxNOzROsSTPiE8tBJPA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/info": "^1.1.0",
"@webpack-cli/info": "^1.2.1", "@webpack-cli/serve": "^1.1.0",
"@webpack-cli/serve": "^1.2.1",
"colorette": "^1.2.1", "colorette": "^1.2.1",
"command-line-usage": "^6.1.0",
"commander": "^6.2.0", "commander": "^6.2.0",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"execa": "^5.0.0", "execa": "^4.1.0",
"fastest-levenshtein": "^1.0.12",
"import-local": "^3.0.2", "import-local": "^3.0.2",
"interpret": "^2.2.0", "interpret": "^2.2.0",
"leven": "^3.1.0",
"rechoir": "^0.7.0", "rechoir": "^0.7.0",
"v8-compile-cache": "^2.2.0", "v8-compile-cache": "^2.2.0",
"webpack-merge": "^4.2.2" "webpack-merge": "^4.2.2"
@@ -3120,6 +3193,22 @@
"isexe": "^2.0.0" "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": { "ws": {
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",
+4 -3
View File
@@ -1,5 +1,6 @@
{ {
"name": "@excalidraw/utils", "name": "@excalidraw/utils",
"sideEffects": false,
"version": "0.1.0", "version": "0.1.0",
"main": "dist/excalidraw-utils.min.js", "main": "dist/excalidraw-utils.min.js",
"files": [ "files": [
@@ -45,10 +46,10 @@
"babel-plugin-transform-class-properties": "6.24.1", "babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"ts-loader": "8.0.13", "ts-loader": "8.0.12",
"webpack": "5.11.1", "webpack": "5.11.0",
"webpack-bundle-analyzer": "4.3.0", "webpack-bundle-analyzer": "4.3.0",
"webpack-cli": "4.3.1" "webpack-cli": "4.2.0"
}, },
"bugs": "https://github.com/excalidraw/excalidraw/issues", "bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw", "repository": "https://github.com/excalidraw/excalidraw",
@@ -1,12 +1,11 @@
import { import {
exportToCanvas as _exportToCanvas, exportToCanvas as _exportToCanvas,
exportToSvg as _exportToSvg, exportToSvg as _exportToSvg,
} from "../scene/export"; } from "../../scene/export";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../../appState";
import { AppState } from "../types"; import { AppState } from "../../types";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../../element/types";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../../element";
import { SCENE_NAME_FALLBACK } from "../constants";
type ExportOpts = { type ExportOpts = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
@@ -19,7 +18,7 @@ type ExportOpts = {
export const exportToCanvas = ({ export const exportToCanvas = ({
elements, elements,
appState = { ...getDefaultAppState(), name: SCENE_NAME_FALLBACK }, appState = getDefaultAppState(),
getDimensions = (width, height) => ({ width, height, scale: 1 }), getDimensions = (width, height) => ({ width, height, scale: 1 }),
}: ExportOpts) => { }: ExportOpts) => {
return _exportToCanvas( return _exportToCanvas(
@@ -75,7 +74,7 @@ export const exportToBlob = (
export const exportToSvg = ({ export const exportToSvg = ({
elements, elements,
appState = { ...getDefaultAppState(), name: SCENE_NAME_FALLBACK }, appState = getDefaultAppState(),
exportPadding, exportPadding,
metadata, metadata,
}: ExportOpts & { }: ExportOpts & {
+15 -1
View File
@@ -13,7 +13,7 @@ module.exports = {
libraryTarget: "umd", libraryTarget: "umd",
}, },
resolve: { resolve: {
extensions: [".tsx", ".ts", ".js"], extensions: [".ts", ".js"],
}, },
optimization: { optimization: {
runtimeChunk: false, runtimeChunk: false,
@@ -40,4 +40,18 @@ module.exports = {
}), }),
...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []), ...(process.env.ANALYZER === "true" ? [new BundleAnalyzerPlugin()] : []),
], ],
externals: {
react: {
root: "React",
commonjs2: "react",
commonjs: "react",
amd: "react",
},
"react-dom": {
root: "ReactDOM",
commonjs2: "react-dom",
commonjs: "react-dom",
amd: "react-dom",
},
},
}; };
+21 -6
View File
@@ -7,7 +7,6 @@ import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
import { distance, SVG_NS } from "../utils"; import { distance, SVG_NS } from "../utils";
import { normalizeScroll } from "./scroll"; import { normalizeScroll } from "./scroll";
import { AppState } from "../types"; import { AppState } from "../types";
import { t } from "../i18n";
import { DEFAULT_FONT_FAMILY, DEFAULT_VERTICAL_ALIGN } from "../constants"; import { DEFAULT_FONT_FAMILY, DEFAULT_VERTICAL_ALIGN } from "../constants";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
@@ -23,12 +22,14 @@ export const exportToCanvas = (
viewBackgroundColor, viewBackgroundColor,
scale = 1, scale = 1,
shouldAddWatermark, shouldAddWatermark,
watermarkText,
}: { }: {
exportBackground: boolean; exportBackground: boolean;
exportPadding?: number; exportPadding?: number;
scale?: number; scale?: number;
viewBackgroundColor: string; viewBackgroundColor: string;
shouldAddWatermark: boolean; shouldAddWatermark: boolean;
watermarkText?: string;
}, },
createCanvas: (width: number, height: number) => HTMLCanvasElement = ( createCanvas: (width: number, height: number) => HTMLCanvasElement = (
width, width,
@@ -40,7 +41,11 @@ export const exportToCanvas = (
return tempCanvas; return tempCanvas;
}, },
) => { ) => {
const sceneElements = getElementsAndWatermark(elements, shouldAddWatermark); const sceneElements = getElementsAndWatermark(
elements,
shouldAddWatermark,
watermarkText,
);
const [minX, minY, width, height] = getCanvasSize( const [minX, minY, width, height] = getCanvasSize(
sceneElements, sceneElements,
@@ -152,20 +157,25 @@ export const exportToSvg = (
const getElementsAndWatermark = ( const getElementsAndWatermark = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
shouldAddWatermark: boolean, shouldAddWatermark: boolean,
watermarkText?: string,
): readonly NonDeletedExcalidrawElement[] => { ): readonly NonDeletedExcalidrawElement[] => {
let _elements = [...elements]; let _elements = [...elements];
if (shouldAddWatermark) { if (shouldAddWatermark) {
const [, , maxX, maxY] = getCommonBounds(elements); const [, , maxX, maxY] = getCommonBounds(elements);
_elements = [..._elements, getWatermarkElement(maxX, maxY)]; _elements = [..._elements, getWatermarkElement(maxX, maxY, watermarkText)];
} }
return _elements; return _elements;
}; };
const getWatermarkElement = (maxX: number, maxY: number) => { const getWatermarkElement = (
maxX: number,
maxY: number,
watermarkText: string = "",
) => {
return newTextElement({ return newTextElement({
text: t("labels.madeWithExcalidraw"), text: watermarkText,
fontSize: WATERMARK_HEIGHT, fontSize: WATERMARK_HEIGHT,
fontFamily: DEFAULT_FONT_FAMILY, fontFamily: DEFAULT_FONT_FAMILY,
textAlign: "right", textAlign: "right",
@@ -204,8 +214,13 @@ export const getExportSize = (
exportPadding: number, exportPadding: number,
shouldAddWatermark: boolean, shouldAddWatermark: boolean,
scale: number, scale: number,
watermarkText?: string,
): [number, number] => { ): [number, number] => {
const sceneElements = getElementsAndWatermark(elements, shouldAddWatermark); const sceneElements = getElementsAndWatermark(
elements,
shouldAddWatermark,
watermarkText,
);
const [, , width, height] = getCanvasSize( const [, , width, height] = getCanvasSize(
sceneElements, sceneElements,
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2,7 +2,7 @@ import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { render } from "./test-utils"; import { render } from "./test-utils";
import ExcalidrawApp from "../excalidraw-app"; import ExcalidrawApp from "../excalidraw-app";
import { defaultLang, setLanguage } from "../i18n"; import { setLanguage } from "../i18n";
import { UI, Pointer, Keyboard } from "./helpers/ui"; import { UI, Pointer, Keyboard } from "./helpers/ui";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
@@ -60,7 +60,7 @@ describe("aligning", () => {
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
mouse.reset(); mouse.reset();
await setLanguage(defaultLang); await setLanguage("en.json");
await render(<ExcalidrawApp />); await render(<ExcalidrawApp />);
}); });
+5 -4
View File
@@ -4,8 +4,9 @@ import ReactDOM from "react-dom";
import { copiedStyles } from "../actions/actionStyles"; import { copiedStyles } from "../actions/actionStyles";
import { ShortcutName } from "../actions/shortcuts"; import { ShortcutName } from "../actions/shortcuts";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { setLanguage } from "../i18n";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import ExcalidrawApp from "../excalidraw-app"; import Excalidraw from "../packages/excalidraw/index";
import { reseed } from "../random"; import { reseed } from "../random";
import * as Renderer from "../renderer/renderScene"; import * as Renderer from "../renderer/renderScene";
import { setDateTimeForTests } from "../utils"; import { setDateTimeForTests } from "../utils";
@@ -18,7 +19,6 @@ import {
screen, screen,
waitFor, waitFor,
} from "./test-utils"; } from "./test-utils";
import { defaultLang } from "../i18n";
const { h } = window; const { h } = window;
@@ -75,7 +75,8 @@ beforeEach(async () => {
finger1.reset(); finger1.reset();
finger2.reset(); finger2.reset();
await render(<ExcalidrawApp />); await setLanguage("en.json");
await render(<Excalidraw offsetLeft={0} offsetTop={0} />);
}); });
afterEach(() => { afterEach(() => {
@@ -438,7 +439,7 @@ describe("regression tests", () => {
await waitFor(() => expect(screen.queryByTitle(/thin/i)).toBeNull()); await waitFor(() => expect(screen.queryByTitle(/thin/i)).toBeNull());
// reset language // reset language
fireEvent.change(document.querySelector(".dropdown-select__language")!, { fireEvent.change(document.querySelector(".dropdown-select__language")!, {
target: { value: defaultLang.code }, target: { value: "en" },
}); });
// switching back to English // switching back to English
await waitFor(() => expect(screen.queryByTitle(/thin/i)).not.toBeNull()); await waitFor(() => expect(screen.queryByTitle(/thin/i)).not.toBeNull());
-15
View File
@@ -9,7 +9,6 @@ import {
GroupId, GroupId,
ExcalidrawBindableElement, ExcalidrawBindableElement,
Arrowhead, Arrowhead,
ChartType,
} from "./element/types"; } from "./element/types";
import { SHAPES } from "./shapes"; import { SHAPES } from "./shapes";
import { Point as RoughPoint } from "roughjs/bin/geometry"; import { Point as RoughPoint } from "roughjs/bin/geometry";
@@ -18,8 +17,6 @@ import { SuggestedBinding } from "./element/binding";
import { ImportedDataState } from "./data/types"; import { ImportedDataState } from "./data/types";
import { ExcalidrawImperativeAPI } from "./components/App"; import { ExcalidrawImperativeAPI } from "./components/App";
import type { ResolvablePromise } from "./utils"; import type { ResolvablePromise } from "./utils";
import { Spreadsheet } from "./charts";
import { Language } from "./i18n";
export type FlooredNumber = number & { _brand: "FlooredNumber" }; export type FlooredNumber = number & { _brand: "FlooredNumber" };
export type Point = Readonly<RoughPoint>; export type Point = Readonly<RoughPoint>;
@@ -100,16 +97,6 @@ export type AppState = {
fileHandle: import("browser-nativefs").FileSystemHandle | null; fileHandle: import("browser-nativefs").FileSystemHandle | null;
collaborators: Map<string, Collaborator>; collaborators: Map<string, Collaborator>;
showStats: boolean; showStats: boolean;
currentChartType: ChartType;
pasteDialog:
| {
shown: false;
data: null;
}
| {
shown: true;
data: Spreadsheet;
};
}; };
export type NormalizedZoomValue = number & { _brand: "normalizedZoom" }; export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };
@@ -182,8 +169,6 @@ export interface ExcalidrawProps {
appState: AppState, appState: AppState,
canvas: HTMLCanvasElement | null, canvas: HTMLCanvasElement | null,
) => void; ) => void;
renderFooter?: (isMobile: boolean) => JSX.Element;
langCode?: Language["code"];
} }
export type SceneData = { export type SceneData = {
-5
View File
@@ -8,7 +8,6 @@ import { FontFamily, FontString } from "./element/types";
import { Zoom } from "./types"; import { Zoom } from "./types";
import { unstable_batchedUpdates } from "react-dom"; import { unstable_batchedUpdates } from "react-dom";
import { isDarwin } from "./keys"; import { isDarwin } from "./keys";
import { t } from "./i18n";
export const SVG_NS = "http://www.w3.org/2000/svg"; export const SVG_NS = "http://www.w3.org/2000/svg";
@@ -33,10 +32,6 @@ export const getDateTime = () => {
return `${year}-${month}-${day}-${hr}${min}`; return `${year}-${month}-${day}-${hr}${min}`;
}; };
export const getNewSceneName = () => {
return `${t("labels.untitled")}-${getDateTime()}`;
};
export const capitalizeString = (str: string) => export const capitalizeString = (str: string) =>
str.charAt(0).toUpperCase() + str.slice(1); str.charAt(0).toUpperCase() + str.slice(1);