Compare commits

..

5 Commits

Author SHA1 Message Date
dwelle ce4b64b2a3 Merge branch 'master' into improve_copy_styles
# Conflicts:
#	src/tests/regressionTests.test.tsx
2020-12-12 23:36:48 +01:00
dwelle ef82e15ee8 update appState on copy-styles & improve paste 2020-12-12 21:24:52 +01:00
dwelle 9f6e3c5a9d narrow down roughness type 2020-12-12 21:23:25 +01:00
dwelle 8a106dde57 compare to undefined directly 2020-12-12 21:23:01 +01:00
dwelle 2dc84f04be prevent newElementWith from accepting undefined values 2020-12-12 21:22:34 +01:00
102 changed files with 11383 additions and 5602 deletions
-37
View File
@@ -1,37 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
day: sunday
time: "01:00"
open-pull-requests-limit: 99
reviewers:
- lipis
assignees:
- lipis
- package-ecosystem: npm
directory: "/src/packages/excalidraw/"
schedule:
interval: weekly
day: sunday
time: "01:00"
open-pull-requests-limit: 99
reviewers:
- ad1992
assignees:
- ad1992
- package-ecosystem: npm
directory: "/src/packages/utils/"
schedule:
interval: weekly
day: sunday
time: "01:00"
open-pull-requests-limit: 99
reviewers:
- ad1992
assignees:
- ad1992
+1
View File
@@ -4,6 +4,7 @@ on:
push:
branches:
- master
pull_request:
jobs:
build-docker:
+33
View File
@@ -0,0 +1,33 @@
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
branches: [master]
schedule:
- cron: "18 7 * * 0"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: ["typescript"]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
-15
View File
@@ -30,18 +30,3 @@ jobs:
git commit -am "Auto commit: Calculate translation coverage"
git push
fi
- name: Construct comment body
id: getCommentBody
run: |
body=$(npm run locales-coverage:description | grep '^[^>]')
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Update description with coverage
uses: kt3k/update-pr-description@v1.0.1
with:
pr_body: ${{ steps.getCommentBody.outputs.body }}
pr_title: "chore: New Crowdin updates"
github_token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
-16
View File
@@ -1,16 +0,0 @@
name: "Semantic PR title"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v2.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+586 -383
View File
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -19,18 +19,18 @@
]
},
"dependencies": {
"@sentry/browser": "5.29.0",
"@sentry/integrations": "5.29.0",
"@sentry/browser": "5.28.0",
"@sentry/integrations": "5.28.0",
"@testing-library/jest-dom": "5.11.6",
"@testing-library/react": "11.2.2",
"@types/jest": "26.0.19",
"@types/jest": "26.0.16",
"@types/nanoid": "2.1.0",
"@types/react": "17.0.0",
"@types/react-dom": "17.0.0",
"@types/socket.io-client": "1.4.34",
"browser-nativefs": "0.12.0",
"browser-nativefs": "0.11.1",
"clsx": "1.1.1",
"firebase": "8.2.1",
"firebase": "8.1.2",
"i18next-browser-languagedetector": "6.0.1",
"lodash.throttle": "4.1.1",
"nanoid": "2.1.11",
@@ -47,18 +47,19 @@
"react-scripts": "4.0.1",
"roughjs": "4.3.1",
"socket.io-client": "2.3.1",
"typescript": "4.1.3"
"typescript": "4.0.5"
},
"devDependencies": {
"@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.1",
"eslint-config-prettier": "7.1.0",
"eslint-plugin-prettier": "3.3.0",
"firebase-tools": "9.0.1",
"husky": "4.3.6",
"asar": "3.0.3",
"eslint-config-prettier": "7.0.0",
"eslint-plugin-prettier": "3.1.4",
"firebase-tools": "8.17.0",
"husky": "4.3.0",
"jest-canvas-mock": "2.3.0",
"lint-staged": "10.5.3",
"pepjs": "0.5.3",
"pepjs": "0.5.2",
"prettier": "2.2.1",
"rewire": "5.0.0"
},
@@ -83,14 +84,13 @@
"build-node": "node ./scripts/build-node.js",
"build:app:docker": "REACT_APP_INCLUDE_GTAG=false REACT_APP_DISABLE_SENTRY=true react-scripts build",
"build:app": "REACT_APP_INCLUDE_GTAG=true REACT_APP_GIT_SHA=$NOW_GITHUB_COMMIT_SHA react-scripts build",
"build:version": "node ./scripts/build-version.js",
"build": "npm run build:app && npm run build:version",
"build:zip": "node ./scripts/build-version.js",
"build": "npm run build:app && npm run build:zip",
"eject": "react-scripts eject",
"fix:code": "npm run test:code -- --fix",
"fix:other": "npm run prettier -- --write",
"fix": "npm run fix:other && npm run fix:code",
"locales-coverage": "node scripts/build-locales-coverage.js",
"locales-coverage:description": "node scripts/locales-coverage-description.js",
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"start": "react-scripts start",
"test:all": "npm run test:typecheck && npm run test:code && npm run test:other && npm run test:app -- --watchAll=false",
-2
View File
@@ -55,8 +55,6 @@
<meta name="twitter:image" content="https://excalidraw.com/og-image.png" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<!-- Excalidraw version -->
<meta name="version" content="{version}" />
<link
rel="preload"
href="FG_Virgil.woff2"
+1 -2
View File
@@ -25,6 +25,5 @@
"application/vnd.excalidraw+json": [".excalidraw"]
}
}
],
"capture_links": "new_client"
]
}
+8 -15
View File
@@ -2,8 +2,7 @@
const fs = require("fs");
const path = require("path");
const versionFile = path.join("build", "version.json");
const indexFile = path.join("build", "index.html");
const asar = require("asar");
const zero = (digit) => `0${digit}`.slice(-2);
@@ -21,24 +20,18 @@ const now = new Date();
const data = JSON.stringify(
{
asar: "excalidraw.asar",
version: versionDate(now),
},
undefined,
2,
);
fs.writeFileSync(versionFile, data);
fs.writeFileSync(path.join("build", "version.json"), data);
// https://stackoverflow.com/a/14181136/8418
fs.readFile(indexFile, "utf8", (error, data) => {
if (error) {
return console.error(error);
}
const result = data.replace(/{version}/g, versionDate(now));
(async () => {
const src = "build/";
const dest = path.join("build", `excalidraw.asar`);
fs.writeFile(indexFile, result, "utf8", (error) => {
if (error) {
return console.error(error);
}
});
});
await asar.createPackage(src, dest);
})();
-156
View File
@@ -1,156 +0,0 @@
const fs = require("fs");
const THRESSHOLD = 85;
const crowdinMap = {
"ar-SA": "en-ar",
"el-GR": "en-el",
"fi-FI": "en-fi",
"ja-JP": "en-ja",
"bg-BG": "en-bg",
"ca-ES": "en-ca",
"de-DE": "en-de",
"es-ES": "en-es",
"fa-IR": "en-fa",
"fr-FR": "en-fr",
"he-IL": "en-he",
"hi-IN": "en-hi",
"hu-HU": "en-hu",
"id-ID": "en-id",
"it-IT": "en-it",
"ko-KR": "en-ko",
"my-MM": "en-my",
"nb-NO": "en-nb",
"nl-NL": "en-nl",
"nn-NO": "en-nnno",
"pl-PL": "en-pl",
"pt-PT": "en-pt",
"ro-RO": "en-ro",
"ru-RU": "en-ru",
"sk-SK": "en-sk",
"sv-SE": "en-sv",
"tr-TR": "en-tr",
"uk-UA": "en-uk",
"zh-CN": "en-zhcn",
"zh-TW": "en-zhtw",
};
const flags = {
"ar-SA": "🇸🇦",
"bg-BG": "🇧🇬",
"ca-ES": "🇪🇸",
"de-DE": "🇩🇪",
"el-GR": "🇬🇷",
"es-ES": "🇪🇸",
"fa-IR": "🇮🇷",
"fi-FI": "🇫🇮",
"fr-FR": "🇫🇷",
"he-IL": "🇮🇱",
"hi-IN": "🇮🇳",
"hu-HU": "🇭🇺",
"id-ID": "🇮🇩",
"it-IT": "🇮🇹",
"ja-JP": "🇯🇵",
"ko-KR": "🇰🇷",
"my-MM": "🇲🇲",
"nb-NO": "🇳🇴",
"nl-NL": "🇳🇱",
"nn-NO": "🇳🇴",
"pl-PL": "🇵🇱",
"pt-PT": "🇵🇹",
"ro-RO": "🇷🇴",
"ru-RU": "🇷🇺",
"sk-SK": "🇸🇰",
"sv-SE": "🇸🇪",
"tr-TR": "🇹🇷",
"uk-UA": "🇺🇦",
"zh-CN": "🇨🇳",
"zh-TW": "🇹🇼",
};
const languages = {
"ar-SA": "العربية",
"bg-BG": "Български",
"ca-ES": "Catalan",
"de-DE": "Deutsch",
"el-GR": "Ελληνικά",
"es-ES": "Español",
"fa-IR": "فارسی",
"fi-FI": "Suomi",
"fr-FR": "Français",
"he-IL": "עברית",
"hi-IN": "हिन्दी",
"hu-HU": "Magyar",
"id-ID": "Bahasa Indonesia",
"it-IT": "Italiano",
"ja-JP": "日本語",
"ko-KR": "한국어",
"my-MM": "Burmese",
"nb-NO": "Norsk bokmål",
"nl-NL": "Nederlands",
"nn-NO": "Norsk nynorsk",
"pl-PL": "Polski",
"pt-PT": "Português",
"ro-RO": "Română",
"ru-RU": "Русский",
"sk-SK": "Slovenčina",
"sv-SE": "Svenska",
"tr-TR": "Türkçe",
"uk-UA": "Українська",
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
};
const percentages = fs.readFileSync(
`${__dirname}/../src/locales/percentages.json`,
);
const rowData = JSON.parse(percentages);
const coverages = Object.entries(rowData)
.sort(([, a], [, b]) => b - a)
.reduce((r, [k, v]) => ({ ...r, [k]: v }), {});
const boldIf = (text, condition) => (condition ? `**${text}**` : text);
const printHeader = () => {
let result = "| | Flag | Locale | % |\n";
result += "| --: | :--: | -- | --: |";
return result;
};
const printRow = (id, locale, coverage) => {
const isOver = coverage > THRESSHOLD;
let result = `| ${boldIf(id, isOver)} | `;
result += `${locale in flags ? flags[locale] : ""} | `;
const language = locale in languages ? languages[locale] : locale;
if (locale in crowdinMap && crowdinMap[locale]) {
result += `[${boldIf(
language,
isOver,
)}](https://crowdin.com/translate/excalidraw/10/${crowdinMap[locale]}) | `;
} else {
result += `${boldIf(language, isOver)} | `;
}
result += `${boldIf(coverage, isOver)} |`;
return result;
};
console.info("## Languages check");
console.info("\n\r");
console.info(
`Our translations for every languages should be at least **${THRESSHOLD}%** to appear on Excalidraw. Join our project in [Crowdin](https://crowdin.com/project/excalidraw) and help us translate it in your language. **Can't find your own?** Open an [issue](https://github.com/excalidraw/excalidraw/issues/new) and we'll add it to the list.`,
);
console.info("\n\r");
console.info(printHeader());
let index = 1;
for (const coverage in coverages) {
if (coverage === "en") {
continue;
}
console.info(printRow(index, coverage, coverages[coverage]));
index++;
}
console.info("\n\r");
console.info("\\* Languages in **bold** are going to appear on production.");
+36 -70
View File
@@ -4,14 +4,12 @@ import { getDefaultAppState } from "../appState";
import { trash, zoomIn, zoomOut, resetZoom } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { getNormalizedZoom, getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import { getNormalizedZoom } from "../scene";
import { CODES, KEYS } from "../keys";
import { getShortcutKey } from "../utils";
import useIsMobile from "../is-mobile";
import { register } from "./register";
import { newElementWith } from "../element/mutateElement";
import { ExcalidrawElement } from "../element/types";
import { AppState, NormalizedZoomValue } from "../types";
import { getCommonBounds } from "../element";
import { getNewZoom } from "../scene/zoom";
@@ -64,7 +62,7 @@ export const actionClearCanvas = register({
elementLocked: appState.elementLocked,
exportBackground: appState.exportBackground,
exportEmbedScene: appState.exportEmbedScene,
showGrid: appState.showGrid,
gridSize: appState.gridSize,
shouldAddWatermark: appState.shouldAddWatermark,
showStats: appState.showStats,
},
@@ -95,7 +93,6 @@ export const actionZoomIn = register({
const zoom = getNewZoom(
getNormalizedZoom(appState.zoom.value + ZOOM_STEP),
appState.zoom,
{ left: appState.offsetLeft, top: appState.offsetTop },
{ x: appState.width / 2, y: appState.height / 2 },
);
trackEvent(EVENT_ACTION, "zoom", "in", zoom.value * 100);
@@ -129,7 +126,6 @@ export const actionZoomOut = register({
const zoom = getNewZoom(
getNormalizedZoom(appState.zoom.value - ZOOM_STEP),
appState.zoom,
{ left: appState.offsetLeft, top: appState.offsetTop },
{ x: appState.width / 2, y: appState.height / 2 },
);
@@ -165,15 +161,10 @@ export const actionResetZoom = register({
return {
appState: {
...appState,
zoom: getNewZoom(
1 as NormalizedZoomValue,
appState.zoom,
{ left: appState.offsetLeft, top: appState.offsetTop },
{
x: appState.width / 2,
y: appState.height / 2,
},
),
zoom: getNewZoom(1 as NormalizedZoomValue, appState.zoom, {
x: appState.width / 2,
y: appState.height / 2,
}),
},
commitToHistory: false,
};
@@ -213,63 +204,38 @@ const zoomValueToFitBoundsOnViewport = (
return clampedZoomValueToFitElements as NormalizedZoomValue;
};
const zoomToFitElements = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
zoomToSelection: boolean,
) => {
const nonDeletedElements = getNonDeletedElements(elements);
const selectedElements = getSelectedElements(nonDeletedElements, appState);
const commonBounds =
zoomToSelection && selectedElements.length > 0
? getCommonBounds(selectedElements)
: getCommonBounds(nonDeletedElements);
const zoomValue = zoomValueToFitBoundsOnViewport(commonBounds, {
width: appState.width,
height: appState.height,
});
const newZoom = getNewZoom(zoomValue, appState.zoom, {
left: appState.offsetLeft,
top: appState.offsetTop,
});
const action = zoomToSelection ? "selection" : "fit";
const [x1, y1, x2, y2] = commonBounds;
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
trackEvent(EVENT_ACTION, "zoom", action, newZoom.value * 100);
return {
appState: {
...appState,
...centerScrollOn({
scenePoint: { x: centerX, y: centerY },
viewportDimensions: {
width: appState.width,
height: appState.height,
},
zoom: newZoom,
}),
zoom: newZoom,
},
commitToHistory: false,
};
};
export const actionZoomToSelected = register({
name: "zoomToSelection",
perform: (elements, appState) => zoomToFitElements(elements, appState, true),
keyTest: (event) =>
event.code === CODES.TWO &&
event.shiftKey &&
!event.altKey &&
!event[KEYS.CTRL_OR_CMD],
});
export const actionZoomToFit = register({
name: "zoomToFit",
perform: (elements, appState) => zoomToFitElements(elements, appState, false),
perform: (elements, appState) => {
const nonDeletedElements = elements.filter((element) => !element.isDeleted);
const commonBounds = getCommonBounds(nonDeletedElements);
const zoomValue = zoomValueToFitBoundsOnViewport(commonBounds, {
width: appState.width,
height: appState.height,
});
const newZoom = getNewZoom(zoomValue, appState.zoom);
const [x1, y1, x2, y2] = commonBounds;
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
trackEvent(EVENT_ACTION, "zoom", "fit", newZoom.value * 100);
return {
appState: {
...appState,
...centerScrollOn({
scenePoint: { x: centerX, y: centerY },
viewportDimensions: {
width: appState.width,
height: appState.height,
},
zoom: newZoom,
}),
zoom: newZoom,
},
commitToHistory: false,
};
},
keyTest: (event) =>
event.code === CODES.ONE &&
event.shiftKey &&
+1 -1
View File
@@ -136,7 +136,7 @@ export const actionDeleteSelected = register({
};
},
contextItemLabel: "labels.delete",
contextMenuOrder: 999999,
contextMenuOrder: 3,
keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
+1 -11
View File
@@ -3,15 +3,12 @@ import { EVENT_CHANGE, EVENT_IO, trackEvent } from "../analytics";
import { load, save, saveAs } from "../components/icons";
import { ProjectName } from "../components/ProjectName";
import { ToolButton } from "../components/ToolButton";
import { Tooltip } from "../components/Tooltip";
import { questionCircle } from "../components/icons";
import { loadFromJSON, saveAsJSON } from "../data";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { KEYS } from "../keys";
import { muteFSAbortError } from "../utils";
import { register } from "./register";
import "../components/ToolIcon.scss";
export const actionChangeProjectName = register({
name: "changeProjectName",
@@ -57,20 +54,13 @@ export const actionChangeExportEmbedScene = register({
};
},
PanelComponent: ({ appState, updateData }) => (
<label style={{ display: "flex" }}>
<label title={t("labels.exportEmbedScene_details")}>
<input
type="checkbox"
checked={appState.exportEmbedScene}
onChange={(event) => updateData(event.target.checked)}
/>{" "}
{t("labels.exportEmbedScene")}
<Tooltip
label={t("labels.exportEmbedScene_details")}
position="above"
long={true}
>
<div className="TooltipIcon">{questionCircle}</div>
</Tooltip>
</label>
),
});
+128 -28
View File
@@ -1,28 +1,110 @@
import {
isTextElement,
isExcalidrawElement,
redrawTextBoundingBox,
getNonDeletedElements,
} from "../element";
import { CODES, KEYS } from "../keys";
import { register } from "./register";
import { mutateElement, newElementWith } from "../element/mutateElement";
import { newElementWith } from "../element/mutateElement";
import {
DEFAULT_FONT_SIZE,
DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN,
} from "../constants";
ExcalidrawElement,
ExcalidrawElementPossibleProps,
} from "../element/types";
import { AppState } from "../types";
import {
canChangeSharpness,
getSelectedElements,
hasBackground,
hasStroke,
hasText,
} from "../scene";
import { isLinearElement, isLinearElementType } from "../element/typeChecks";
type AppStateStyles = {
[K in AssertSubset<
keyof AppState,
typeof copyableStyles[number][0]
>]: AppState[K];
};
type ElementStyles = {
[K in AssertSubset<
keyof ExcalidrawElementPossibleProps,
typeof copyableStyles[number][1]
>]: ExcalidrawElementPossibleProps[K];
};
type ElemelementStylesByType = Record<ExcalidrawElement["type"], ElementStyles>;
// `copiedStyles` is exported only for tests.
export let copiedStyles: string = "{}";
let COPIED_STYLES: {
appStateStyles: Partial<AppStateStyles>;
elementStyles: Partial<ElementStyles>;
elementStylesByType: Partial<ElemelementStylesByType>;
} | null = null;
/* [AppState prop, ExcalidrawElement prop, predicate] */
const copyableStyles = [
["currentItemOpacity", "opacity", () => true],
["currentItemStrokeColor", "strokeColor", () => true],
["currentItemStrokeStyle", "strokeStyle", hasStroke],
["currentItemStrokeWidth", "strokeWidth", hasStroke],
["currentItemRoughness", "roughness", hasStroke],
["currentItemBackgroundColor", "backgroundColor", hasBackground],
["currentItemFillStyle", "fillStyle", hasBackground],
["currentItemStrokeSharpness", "strokeSharpness", canChangeSharpness],
["currentItemLinearStrokeSharpness", "strokeSharpness", isLinearElementType],
["currentItemStartArrowhead", "startArrowhead", isLinearElementType],
["currentItemEndArrowhead", "endArrowhead", isLinearElementType],
["currentItemFontFamily", "fontFamily", hasText],
["currentItemFontSize", "fontSize", hasText],
["currentItemTextAlign", "textAlign", hasText],
] as const;
const getCommonStyleProps = (
elements: readonly ExcalidrawElement[],
): Exclude<typeof COPIED_STYLES, null> => {
const appStateStyles = {} as AppStateStyles;
const elementStyles = {} as ElementStyles;
const elementStylesByType = elements.reduce((acc, element) => {
// only use the first element of given type
if (!acc[element.type]) {
acc[element.type] = {} as ElementStyles;
copyableStyles.forEach(([appStateProp, prop, predicate]) => {
const value = (element as any)[prop];
if (value !== undefined && predicate(element.type)) {
if (appStateStyles[appStateProp] === undefined) {
(appStateStyles as any)[appStateProp] = value;
}
if (elementStyles[prop] === undefined) {
(elementStyles as any)[prop] = value;
}
(acc as any)[element.type][prop] = value;
}
});
}
return acc;
}, {} as ElemelementStylesByType);
// clone in case we ever make some of the props into non-primitives
return JSON.parse(
JSON.stringify({ appStateStyles, elementStyles, elementStylesByType }),
);
};
export const actionCopyStyles = register({
name: "copyStyles",
perform: (elements, appState) => {
const element = elements.find((el) => appState.selectedElementIds[el.id]);
if (element) {
copiedStyles = JSON.stringify(element);
}
COPIED_STYLES = getCommonStyleProps(
getSelectedElements(getNonDeletedElements(elements), appState),
);
return {
appState: {
...appState,
...COPIED_STYLES.appStateStyles,
},
commitToHistory: false,
};
},
@@ -35,31 +117,49 @@ export const actionCopyStyles = register({
export const actionPasteStyles = register({
name: "pasteStyles",
perform: (elements, appState) => {
const pastedElement = JSON.parse(copiedStyles);
if (!isExcalidrawElement(pastedElement)) {
if (!COPIED_STYLES) {
return { elements, commitToHistory: false };
}
const getStyle = <T extends ExcalidrawElement, K extends keyof T>(
element: T,
prop: K,
) => {
return (COPIED_STYLES?.elementStylesByType[element.type]?.[
prop as keyof ElementStyles
] ??
COPIED_STYLES?.elementStyles[prop as keyof ElementStyles] ??
element[prop]) as T[K];
};
return {
elements: elements.map((element) => {
if (appState.selectedElementIds[element.id]) {
const newElement = newElementWith(element, {
backgroundColor: pastedElement?.backgroundColor,
strokeWidth: pastedElement?.strokeWidth,
strokeColor: pastedElement?.strokeColor,
strokeStyle: pastedElement?.strokeStyle,
fillStyle: pastedElement?.fillStyle,
opacity: pastedElement?.opacity,
roughness: pastedElement?.roughness,
});
if (isTextElement(newElement)) {
mutateElement(newElement, {
fontSize: pastedElement?.fontSize || DEFAULT_FONT_SIZE,
fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY,
textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN,
const commonProps = {
backgroundColor: getStyle(element, "backgroundColor"),
strokeWidth: getStyle(element, "strokeWidth"),
strokeColor: getStyle(element, "strokeColor"),
strokeStyle: getStyle(element, "strokeStyle"),
fillStyle: getStyle(element, "fillStyle"),
opacity: getStyle(element, "opacity"),
roughness: getStyle(element, "roughness"),
strokeSharpness: getStyle(element, "strokeSharpness"),
};
if (isTextElement(element)) {
const newElement = newElementWith(element, {
...commonProps,
fontSize: getStyle(element, "fontSize"),
fontFamily: getStyle(element, "fontFamily"),
textAlign: getStyle(element, "textAlign"),
});
redrawTextBoundingBox(newElement);
return newElement;
} else if (isLinearElement(element)) {
return newElementWith(element, {
...commonProps,
startArrowhead: getStyle(element, "startArrowhead"),
endArrowhead: getStyle(element, "endArrowhead"),
});
}
return newElement;
return newElementWith(element, commonProps);
}
return element;
}),
+4 -4
View File
@@ -19,8 +19,8 @@ export type ShortcutName =
| "copyAsSvg"
| "group"
| "ungroup"
| "gridMode"
| "stats"
| "toggleGridMode"
| "toggleStats"
| "addToLibrary";
const shortcutMap: Record<ShortcutName, string[]> = {
@@ -51,8 +51,8 @@ const shortcutMap: Record<ShortcutName, string[]> = {
copyAsSvg: [],
group: [getShortcutKey("CtrlOrCmd+G")],
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
gridMode: [getShortcutKey("CtrlOrCmd+'")],
stats: [],
toggleGridMode: [getShortcutKey("CtrlOrCmd+'")],
toggleStats: [],
addToLibrary: [],
};
-1
View File
@@ -58,7 +58,6 @@ export type ActionName =
| "zoomOut"
| "resetZoom"
| "zoomToFit"
| "zoomToSelection"
| "changeFontFamily"
| "changeTextAlign"
| "toggleFullScreen"
+11 -14
View File
@@ -11,17 +11,14 @@ export const EVENT_SHAPE = "shape";
export const EVENT_SHARE = "share";
export const EVENT_MAGIC = "magic";
export const trackEvent =
typeof window !== "undefined" && window.gtag
? (category: string, name: string, label?: string, value?: number) => {
window.gtag("event", name, {
event_category: category,
event_label: label,
value,
});
}
: typeof process !== "undefined" && process?.env?.JEST_WORKER_ID
? (category: string, name: string, label?: string, value?: number) => {}
: (category: string, name: string, label?: string, value?: number) => {
console.info("Track Event", category, name, label, value);
};
export const trackEvent = window.gtag
? (category: string, name: string, label?: string, value?: number) => {
window.gtag("event", name, {
event_category: category,
event_label: label,
value,
});
}
: (category: string, name: string, label?: string, value?: number) => {
console.info("Track Event", category, name, label, value);
};
+7 -2
View File
@@ -65,7 +65,7 @@ export const getDefaultAppState = (): Omit<
showShortcutsDialog: false,
suggestedBindings: [],
zenModeEnabled: false,
showGrid: false,
gridSize: null,
editingGroupId: null,
selectedGroupIds: {},
width: window.innerWidth,
@@ -120,7 +120,7 @@ const APP_STATE_STORAGE_CONF = (<
errorMessage: { browser: false, export: false },
exportBackground: { browser: true, export: false },
exportEmbedScene: { browser: true, export: false },
showGrid: { browser: true, export: false },
gridSize: { browser: true, export: true },
height: { browser: false, export: false },
isBindingEnabled: { browser: false, export: false },
isLibraryOpen: { browser: false, export: false },
@@ -166,6 +166,11 @@ const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
const stateForExport = {} as { [K in ExportableKeys]?: typeof appState[K] };
for (const key of Object.keys(appState) as (keyof typeof appState)[]) {
const propConfig = APP_STATE_STORAGE_CONF[key];
if (!propConfig) {
console.error(
`_clearAppStateForStorage: appState key "${key}" config doesn't exist for "${exportType}" export type`,
);
}
if (propConfig?.[exportType]) {
// @ts-ignore see https://github.com/microsoft/TypeScript/issues/31445
stateForExport[key] = appState[key];
+12 -18
View File
@@ -19,15 +19,15 @@ export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
export const VALID_SPREADSHEET = "VALID_SPREADSHEET";
type ParseSpreadsheetResult =
| { type: typeof NOT_SPREADSHEET; reason: string }
| { type: typeof NOT_SPREADSHEET }
| { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet };
const tryParseNumber = (s: string): number | null => {
const match = /^[$€£¥₩]?([0-9,]+(\.[0-9]+)?)$/.exec(s);
const match = /^[$€£¥₩]?([0-9]+(\.[0-9]+)?)$/.exec(s);
if (!match) {
return null;
}
return parseFloat(match[1].replace(/,/g, ""));
return parseFloat(match[1]);
};
const isNumericColumn = (lines: string[][], columnIndex: number) =>
@@ -37,12 +37,12 @@ const tryParseCells = (cells: string[][]): ParseSpreadsheetResult => {
const numCols = cells[0].length;
if (numCols > 2) {
return { type: NOT_SPREADSHEET, reason: "More than 2 columns" };
return { type: NOT_SPREADSHEET };
}
if (numCols === 1) {
if (!isNumericColumn(cells, 0)) {
return { type: NOT_SPREADSHEET, reason: "Value is not numeric" };
return { type: NOT_SPREADSHEET };
}
const hasHeader = tryParseNumber(cells[0][0]) === null;
@@ -51,7 +51,7 @@ const tryParseCells = (cells: string[][]): ParseSpreadsheetResult => {
);
if (values.length < 2) {
return { type: NOT_SPREADSHEET, reason: "Less than two rows" };
return { type: NOT_SPREADSHEET };
}
return {
@@ -67,7 +67,7 @@ const tryParseCells = (cells: string[][]): ParseSpreadsheetResult => {
const valueColumnIndex = isNumericColumn(cells, 0) ? 0 : 1;
if (!isNumericColumn(cells, valueColumnIndex)) {
return { type: NOT_SPREADSHEET, reason: "Value is not numeric" };
return { type: NOT_SPREADSHEET };
}
const labelColumnIndex = (valueColumnIndex + 1) % 2;
@@ -75,7 +75,7 @@ const tryParseCells = (cells: string[][]): ParseSpreadsheetResult => {
const rows = hasHeader ? cells.slice(1) : cells;
if (rows.length < 2) {
return { type: NOT_SPREADSHEET, reason: "Less than 2 rows" };
return { type: NOT_SPREADSHEET };
}
return {
@@ -104,13 +104,13 @@ export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
// Copy/paste from excel, spreadhseets, tsv, csv.
// For now we only accept 2 columns with an optional header
// Check for tab separated values
// Check for tab separeted values
let lines = text
.trim()
.split("\n")
.map((line) => line.trim().split("\t"));
// Check for comma separated files
// Check for comma separeted files
if (lines.length && lines[0].length !== 2) {
lines = text
.trim()
@@ -119,17 +119,14 @@ export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
}
if (lines.length === 0) {
return { type: NOT_SPREADSHEET, reason: "No values" };
return { type: NOT_SPREADSHEET };
}
const numColsFirstLine = lines[0].length;
const isSpreadsheet = lines.every((line) => line.length === numColsFirstLine);
if (!isSpreadsheet) {
return {
type: NOT_SPREADSHEET,
reason: "All rows don't have same number of columns",
};
return { type: NOT_SPREADSHEET };
}
const result = tryParseCells(lines);
@@ -195,7 +192,6 @@ export const renderSpreadsheet = (
y,
startArrowhead: null,
endArrowhead: null,
width: chartWidth,
points: [
[0, 0],
[chartWidth, 0],
@@ -209,7 +205,6 @@ export const renderSpreadsheet = (
y,
startArrowhead: null,
endArrowhead: null,
height: chartHeight,
points: [
[0, 0],
[0, -chartHeight],
@@ -225,7 +220,6 @@ export const renderSpreadsheet = (
endArrowhead: null,
...commonProps,
strokeStyle: "dotted",
width: chartWidth,
points: [
[0, 0],
[chartWidth, 0],
+41 -80
View File
@@ -124,7 +124,6 @@ import {
MIME_TYPES,
TAP_TWICE_TIMEOUT,
TOUCH_CTX_MENU_TIMEOUT,
APP_NAME,
} from "../constants";
import LayerUI from "./LayerUI";
@@ -346,15 +345,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
offsetLeft,
} = this.state;
const { onCollabButtonClick, onExportToBackend } = this.props;
const { onCollabButtonClick } = this.props;
const canvasScale = window.devicePixelRatio;
const canvasWidth = canvasDOMWidth * canvasScale;
const canvasHeight = canvasDOMHeight * canvasScale;
const DEFAULT_PASTE_X = canvasDOMWidth / 2;
const DEFAULT_PASTE_Y = canvasDOMHeight / 2;
return (
<div
className="excalidraw"
@@ -375,17 +371,12 @@ class App extends React.Component<ExcalidrawProps, AppState> {
onCollabButtonClick={onCollabButtonClick}
onLockToggle={this.toggleLock}
onInsertShape={(elements) =>
this.addElementsFromPasteOrLibrary(
elements,
DEFAULT_PASTE_X,
DEFAULT_PASTE_Y,
)
this.addElementsFromPasteOrLibrary(elements)
}
zenModeEnabled={zenModeEnabled}
toggleZenMode={this.toggleZenMode}
lng={getLanguage().lng}
isCollaborating={this.props.isCollaborating || false}
onExportToBackend={onExportToBackend}
/>
{this.state.showStats && (
<Stats
@@ -503,7 +494,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
};
private importLibraryFromUrl = async (url: string) => {
window.history.replaceState({}, APP_NAME, window.location.origin);
window.history.replaceState({}, "Excalidraw", window.location.origin);
try {
const request = await fetch(url);
const blob = await request.blob();
@@ -595,8 +586,6 @@ class App extends React.Component<ExcalidrawProps, AppState> {
scene.elements,
{
...scene.appState,
width: this.state.width,
height: this.state.height,
offsetTop: this.state.offsetTop,
offsetLeft: this.state.offsetLeft,
},
@@ -1036,7 +1025,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const dy = y - elementsCenterY;
const groupIdMap = new Map();
const [gridX, gridY] = getGridPoint(dx, dy, this.state.showGrid);
const [gridX, gridY] = getGridPoint(dx, dy, this.state.gridSize);
const oldIdToDuplicatedId = new Map();
const newElements = clipboardElements.map((element) => {
@@ -1149,7 +1138,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
toggleGridMode = () => {
this.setState({
showGrid: !this.state.showGrid,
gridSize: this.state.gridSize ? null : GRID_SIZE,
});
};
@@ -1275,8 +1264,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (isArrowKey(event.key)) {
const step =
(this.state.showGrid &&
(event.shiftKey ? ELEMENT_TRANSLATE_AMOUNT : GRID_SIZE)) ||
(this.state.gridSize &&
(event.shiftKey ? ELEMENT_TRANSLATE_AMOUNT : this.state.gridSize)) ||
(event.shiftKey
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
: ELEMENT_TRANSLATE_AMOUNT);
@@ -1427,27 +1416,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
private onGestureChange = withBatchedUpdates((event: GestureEvent) => {
event.preventDefault();
// onGestureChange only has zoom factor but not the center.
// If we're on iPad or iPhone, then we recognize multi-touch and will
// zoom in at the right location on the touchMove handler already.
// On Macbook, we don't have those events so will zoom in at the
// current location instead.
if (gesture.pointers.size === 2) {
return;
}
const initialScale = gesture.initialScale;
if (initialScale) {
this.setState(({ zoom, offsetLeft, offsetTop }) => ({
zoom: getNewZoom(
getNormalizedZoom(initialScale * event.scale),
zoom,
{ left: offsetLeft, top: offsetTop },
{ x: cursorX, y: cursorY },
),
}));
}
this.setState(({ zoom }) => ({
zoom: getNewZoom(
getNormalizedZoom(gesture.initialScale! * event.scale),
zoom,
{ x: cursorX, y: cursorY },
),
}));
});
private onGestureEnd = withBatchedUpdates((event: GestureEvent) => {
@@ -1759,28 +1734,21 @@ class App extends React.Component<ExcalidrawProps, AppState> {
});
}
const initialScale = gesture.initialScale;
if (
gesture.pointers.size === 2 &&
gesture.lastCenter &&
initialScale &&
gesture.initialDistance
) {
if (gesture.pointers.size === 2) {
const center = getCenter(gesture.pointers);
const deltaX = center.x - gesture.lastCenter.x;
const deltaY = center.y - gesture.lastCenter.y;
const deltaX = center.x - gesture.lastCenter!.x;
const deltaY = center.y - gesture.lastCenter!.y;
gesture.lastCenter = center;
const distance = getDistance(Array.from(gesture.pointers.values()));
const scaleFactor = distance / gesture.initialDistance;
const scaleFactor = distance / gesture.initialDistance!;
this.setState(({ zoom, scrollX, scrollY, offsetLeft, offsetTop }) => ({
this.setState(({ zoom, scrollX, scrollY }) => ({
scrollX: normalizeScroll(scrollX + deltaX / zoom.value),
scrollY: normalizeScroll(scrollY + deltaY / zoom.value),
zoom: getNewZoom(
getNormalizedZoom(initialScale * scaleFactor),
getNormalizedZoom(gesture.initialScale! * scaleFactor),
zoom,
{ left: offsetLeft, top: offsetTop },
center,
),
shouldCacheIgnoreZoom: true,
@@ -1819,7 +1787,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
scenePointerX,
scenePointerY,
this.state.editingLinearElement,
this.state.showGrid,
this.state.gridSize,
);
if (editingLinearElement !== this.state.editingLinearElement) {
this.setState({ editingLinearElement });
@@ -2249,7 +2217,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
return {
origin,
originInGrid: tupleToCoors(
getGridPoint(origin.x, origin.y, this.state.showGrid),
getGridPoint(origin.x, origin.y, this.state.gridSize),
),
scrollbars: isOverScrollBars(
currentScrollBars,
@@ -2607,13 +2575,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x,
pointerDownState.origin.y,
elementType === "draw" ? false : this.state.showGrid,
elementType === "draw" ? null : this.state.gridSize,
);
/* If arrow is pre-arrowheads, it will have undefined for both start and end arrowheads.
If so, we want it to be null for start and "arrow" for end. If the linear item is not
an arrow, we want it to be null for both. Otherwise, we want it to use the
values from appState. */
If so, we want it to be null for start and "arrow" for end. If the linear item is not
an arrow, we want it to be null for both. Otherwise, we want it to use the
values from appState. */
const { currentItemStartArrowhead, currentItemEndArrowhead } = this.state;
const [startArrowhead, endArrowhead] =
@@ -2669,7 +2637,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x,
pointerDownState.origin.y,
this.state.showGrid,
this.state.gridSize,
);
const element = newElement({
type: elementType,
@@ -2758,7 +2726,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
this.state.showGrid,
this.state.gridSize,
);
// for arrows/lines, don't start dragging until a given threshold
@@ -2830,7 +2798,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const [dragX, dragY] = getGridPoint(
pointerCoords.x - pointerDownState.drag.offset.x,
pointerCoords.y - pointerDownState.drag.offset.y,
this.state.showGrid,
this.state.gridSize,
);
const [dragDistanceX, dragDistanceY] = [
@@ -2882,7 +2850,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const [originDragX, originDragY] = getGridPoint(
pointerDownState.origin.x - pointerDownState.drag.offset.x,
pointerDownState.origin.y - pointerDownState.drag.offset.y,
this.state.showGrid,
this.state.gridSize,
);
mutateElement(duplicatedElement, {
x: duplicatedElement.x + (originDragX - dragX),
@@ -3542,7 +3510,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
this.state.showGrid,
this.state.gridSize,
);
dragNewElement(
draggingElement,
@@ -3580,7 +3548,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const [resizeX, resizeY] = getGridPoint(
pointerCoords.x - pointerDownState.resize.offset.x,
pointerCoords.y - pointerDownState.resize.offset.y,
this.state.showGrid,
this.state.gridSize,
);
if (
transformElements(
@@ -3644,15 +3612,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
CANVAS_ONLY_ACTIONS.includes(action.name),
),
{
checked: this.state.showGrid,
shortcutName: "gridMode",
label: t("labels.gridMode"),
shortcutName: "toggleGridMode",
label: t("labels.toggleGridMode"),
action: this.toggleGridMode,
},
{
checked: this.state.showStats,
shortcutName: "stats",
label: t("stats.title"),
shortcutName: "toggleStats",
label: t("labels.toggleStats"),
action: this.toggleStats,
},
],
@@ -3729,16 +3695,11 @@ class App extends React.Component<ExcalidrawProps, AppState> {
}, 1000);
}
this.setState(({ zoom, offsetLeft, offsetTop }) => ({
zoom: getNewZoom(
getNormalizedZoom(zoom.value - delta / 100),
zoom,
{ left: offsetLeft, top: offsetTop },
{
x: cursorX,
y: cursorY,
},
),
this.setState(({ zoom }) => ({
zoom: getNewZoom(getNormalizedZoom(zoom.value - delta / 100), zoom, {
x: cursorX,
y: cursorY,
}),
selectedElementIds: {},
previousSelectedElementIds:
Object.keys(selectedElementIds).length !== 0
-22
View File
@@ -32,21 +32,6 @@
display: grid;
grid-template-columns: 1fr 0.2fr;
align-items: center;
&.checkmark::before {
position: absolute;
left: 6px;
margin-bottom: 1px;
content: "\2713";
}
&.dangerous {
div:nth-child(1) {
color: $oc-red-7;
}
}
div:nth-child(1) {
justify-self: start;
margin-inline-end: 20px;
@@ -61,13 +46,6 @@
.context-menu-option:hover {
color: var(--popup-background-color);
background-color: var(--select-highlight-color);
&.dangerous {
div:nth-child(1) {
color: var(--popup-background-color);
}
background-color: $oc-red-6;
}
}
.context-menu-option:focus {
+3 -8
View File
@@ -10,7 +10,6 @@ import {
} from "../actions/shortcuts";
type ContextMenuOption = {
checked?: boolean;
shortcutName: ShortcutName;
label: string;
action(): void;
@@ -27,6 +26,7 @@ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
const isDarkTheme = !!document
.querySelector(".excalidraw")
?.classList.contains("Appearance_dark");
return (
<div
className={clsx("excalidraw", {
@@ -43,14 +43,9 @@ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
className="context-menu"
onContextMenu={(event) => event.preventDefault()}
>
{options.map(({ action, checked, shortcutName, label }, idx) => (
{options.map(({ action, shortcutName, label }, idx) => (
<li data-testid={shortcutName} key={idx} onClick={onCloseRequest}>
<button
className={`context-menu-option
${shortcutName === "delete" ? "dangerous" : ""}
${checked ? "checkmark" : ""}`}
onClick={action}
>
<button className="context-menu-option" onClick={action}>
<div>{label}</div>
<div>
{shortcutName
+2 -8
View File
@@ -14,9 +14,7 @@ export const DarkModeToggle = (props: {
return (
<label
className={`ToolIcon ToolIcon_type_floating ToolIcon_size_M`}
title={
props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode")
}
title={t("buttons.toggleDarkMode")}
>
<input
className="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
@@ -25,11 +23,7 @@ export const DarkModeToggle = (props: {
props.onChange(event.target.checked ? "dark" : "light")
}
checked={props.value === "dark"}
aria-label={
props.value === "dark"
? t("buttons.lightMode")
: t("buttons.darkMode")
}
aria-label={t("buttons.toggleDarkMode")}
/>
<div className="ToolIcon__icon">
{props.value === "light" ? ICONS.MOON : ICONS.SUN}
+1 -8
View File
@@ -28,14 +28,7 @@ export const ErrorDialog = ({
onCloseRequest={handleClose}
title={t("errorDialog.title")}
>
<div>
{message.split("\n").map((line) => (
<>
{line}
<br />
</>
))}
</div>
<div>{message}</div>
</Dialog>
)}
</>
+9 -11
View File
@@ -67,7 +67,7 @@ const ExportModal = ({
onExportToPng: ExportCB;
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
onExportToBackend?: ExportCB;
onExportToBackend: ExportCB;
onCloseRequest: () => void;
}) => {
const someElementIsSelected = isSomeElementSelected(elements, appState);
@@ -155,15 +155,13 @@ const ExportModal = ({
onClick={() => onExportToClipboard(exportedElements, scale)}
/>
)}
{onExportToBackend && (
<ToolButton
type="button"
icon={link}
title={t("buttons.getShareableLink")}
aria-label={t("buttons.getShareableLink")}
onClick={() => onExportToBackend(exportedElements)}
/>
)}
<ToolButton
type="button"
icon={link}
title={t("buttons.getShareableLink")}
aria-label={t("buttons.getShareableLink")}
onClick={() => onExportToBackend(exportedElements)}
/>
</Stack.Row>
<div className="ExportDialog__name">
{actionManager.renderAction("changeProjectName")}
@@ -237,7 +235,7 @@ export const ExportDialog = ({
onExportToPng: ExportCB;
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
onExportToBackend?: ExportCB;
onExportToBackend: ExportCB;
}) => {
const [modalIsShown, setModalIsShown] = useState(false);
const triggerButton = useRef<HTMLButtonElement>(null);
+2 -2
View File
@@ -38,8 +38,8 @@ const getHints = ({ appState, elements }: Hint) => {
selectedElements.length === 1
) {
const targetElement = selectedElements[0];
if (isLinearElement(targetElement) && targetElement.points.length === 2) {
return t("hints.lockAngle");
if (isLinearElement(targetElement) && targetElement.points.length > 2) {
return null;
}
return t("hints.resize");
}
-3
View File
@@ -93,9 +93,6 @@
grid-auto-flow: column;
grid-gap: 0.5rem;
border-radius: 4px;
:root[dir="rtl"] & {
padding: 0.4rem;
}
}
.picker-keybinding {
+58
View File
@@ -39,6 +39,64 @@
width: 1.2rem;
height: 1.2rem;
}
&.tooltip .tooltip-text {
visibility: hidden;
width: 20rem;
bottom: calc(50% + 0.8rem + 6px);
:root[dir="ltr"] & {
left: -5px;
}
:root[dir="rtl"] & {
right: -5px;
}
background-color: $oc-black;
color: $oc-white;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 10;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
&::after {
--size: 6px;
content: "";
border: var(--size) solid transparent;
border-top-color: $oc-black;
position: absolute;
bottom: calc(-2 * var(--size));
:root[dir="ltr"] & {
left: calc(5px + var(--size) / 2);
}
:root[dir="rtl"] & {
right: calc(5px + var(--size) / 2);
}
}
}
// the following 3 rules ensure that the tooltip doesn't show (nor affect
// the cursor) when you drag over when you draw on canvas, but at the same
// time it still works when clicking on the link/shield
body:active &.tooltip:not(:hover) {
pointer-events: none;
}
body:not(:active) &.tooltip:hover .tooltip-text {
visibility: visible;
}
.tooltip-text:hover {
visibility: visible;
}
}
&__github-corner {
+26 -17
View File
@@ -65,11 +65,6 @@ interface LayerUIProps {
toggleZenMode: () => void;
lng: string;
isCollaborating: boolean;
onExportToBackend?: (
exportedElements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
canvas: HTMLCanvasElement | null,
) => void;
}
const useOnClickOutside = (
@@ -322,10 +317,10 @@ const LayerUI = ({
zenModeEnabled,
toggleZenMode,
isCollaborating,
onExportToBackend,
}: LayerUIProps) => {
const isMobile = useIsMobile();
// TODO: Extend tooltip component and use here.
const renderEncryptedIcon = () => (
<a
className={clsx("encrypted-icon tooltip zen-mode-visibility", {
@@ -338,9 +333,10 @@ const LayerUI = ({
trackEvent(EVENT_EXIT, "e2ee shield");
}}
>
<Tooltip label={t("encrypted.tooltip")} position="above" long={true}>
{shield}
</Tooltip>
<span className="tooltip-text" dir="auto">
{t("encrypted.tooltip")}
</span>
{shield}
</a>
);
@@ -364,7 +360,6 @@ const LayerUI = ({
});
}
};
return (
<ExportDialog
elements={elements}
@@ -373,14 +368,28 @@ const LayerUI = ({
onExportToPng={createExporter("png")}
onExportToSvg={createExporter("svg")}
onExportToClipboard={createExporter("clipboard")}
onExportToBackend={
onExportToBackend
? (elements) => {
onExportToBackend &&
onExportToBackend(elements, appState, canvas);
onExportToBackend={async (exportedElements) => {
if (canvas) {
try {
await exportCanvas(
"backend",
exportedElements,
{
...appState,
selectedElementIds: {},
},
canvas,
appState,
);
} catch (error) {
if (error.name !== "AbortError") {
const { width, height } = canvas;
console.error(error, { width, height });
setAppState({ errorMessage: error.message });
}
: undefined
}
}
}
}}
/>
);
};
+4 -5
View File
@@ -207,16 +207,15 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
shortcuts={["Shift+1"]}
/>
<Shortcut
label={t("shortcutsDialog.zoomToSelection")}
shortcuts={["Shift+2"]}
label={t("buttons.toggleFullScreen")}
shortcuts={["F"]}
/>
<Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
<Shortcut
label={t("buttons.zenMode")}
label={t("buttons.toggleZenMode")}
shortcuts={[getShortcutKey("Alt+Z")]}
/>
<Shortcut
label={t("labels.gridMode")}
label={t("labels.toggleGridMode")}
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
/>
</ShortcutIsland>
+1 -15
View File
@@ -6,10 +6,8 @@
right: 12px;
font-size: 12px;
z-index: 999;
h3 {
margin: 0 24px 8px 0;
white-space: nowrap;
}
.close {
@@ -31,21 +29,9 @@
}
tr {
td:nth-child(2) {
min-width: 24px;
min-width: 48px;
text-align: right;
}
}
}
:root[dir="rtl"] & {
left: 12px;
right: initial;
h3 {
margin: 0 0 8px 24px;
}
.close {
float: left;
}
}
}
-13
View File
@@ -1,6 +1,4 @@
@import "open-color/open-color.scss";
@import "../css/variables";
.excalidraw {
.ToolIcon {
display: inline-flex;
@@ -183,17 +181,6 @@
}
}
.TooltipIcon {
width: 0.9em;
height: 0.9em;
margin-left: 5px;
margin-top: 1px;
@media #{$media-query} {
display: none;
}
}
.unlocked-icon {
:root[dir="ltr"] & {
left: 2px;
+10 -27
View File
@@ -7,56 +7,39 @@
.Tooltip__label {
--arrow-size: 4px;
visibility: hidden;
width: 10ch;
background: $oc-black;
color: $oc-white;
text-align: center;
border-radius: 6px;
padding: 8px;
border-radius: 4px;
padding: 4px;
position: absolute;
z-index: 10;
font-size: 13px;
font-size: 0.7rem;
line-height: 1.5;
font-weight: 500;
top: calc(100% + var(--arrow-size) + 3px);
// extra pixel offset for unknown reasons
left: calc(50% + var(--arrow-size) / 2 - 1px);
transform: translateX(-50%);
left: calc(-50% + var(--arrow-size) / 2 - 1px);
word-wrap: break-word;
&::after {
content: "";
border: var(--arrow-size) solid transparent;
border-bottom-color: $oc-black;
position: absolute;
bottom: 100%;
left: calc(50% - var(--arrow-size));
}
&--above {
bottom: calc(100% + var(--arrow-size) + 3px);
&::after {
border-top-color: $oc-black;
top: 100%;
}
}
&--below {
top: calc(100% + var(--arrow-size) + 3px);
&::after {
border-bottom-color: $oc-black;
bottom: 100%;
}
}
}
// the following 3 rules ensure that the tooltip doesn't show (nor affect
// the cursor) when you drag over when you draw on canvas, but at the same
// time it still works when clicking on the link/shield
body:active & .Tooltip:not(:hover) {
body:active .Tooltip:not(:hover) {
pointer-events: none;
}
body:not(:active) & .Tooltip:hover .Tooltip__label {
body:not(:active) .Tooltip:hover .Tooltip__label {
visibility: visible;
}
+2 -18
View File
@@ -5,27 +5,11 @@ import React from "react";
type TooltipProps = {
children: React.ReactNode;
label: string;
position?: "above" | "below";
long?: boolean;
};
export const Tooltip = ({
children,
label,
position = "below",
long = false,
}: TooltipProps) => (
export const Tooltip = ({ children, label }: TooltipProps) => (
<div className="Tooltip">
<span
className={
position === "above"
? "Tooltip__label Tooltip__label--above"
: "Tooltip__label Tooltip__label--below"
}
style={{ width: long ? "50ch" : "10ch" }}
>
{label}
</span>
<span className="Tooltip__label">{label}</span>
{children}
</div>
);
-5
View File
@@ -108,11 +108,6 @@ export const redo = createIcon(
{ mirror: true },
);
export const questionCircle = createIcon(
"M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zM262.655 90c-54.497 0-89.255 22.957-116.549 63.758-3.536 5.286-2.353 12.415 2.715 16.258l34.699 26.31c5.205 3.947 12.621 3.008 16.665-2.122 17.864-22.658 30.113-35.797 57.303-35.797 20.429 0 45.698 13.148 45.698 32.958 0 14.976-12.363 22.667-32.534 33.976C247.128 238.528 216 254.941 216 296v4c0 6.627 5.373 12 12 12h56c6.627 0 12-5.373 12-12v-1.333c0-28.462 83.186-29.647 83.186-106.667 0-58.002-60.165-102-116.531-102zM256 338c-25.365 0-46 20.635-46 46 0 25.364 20.635 46 46 46s46-20.636 46-46c0-25.365-20.635-46-46-46z",
{ mirror: true },
);
// Icon imported form Storybook
// Storybook is licensed under MIT https://github.com/storybookjs/storybook/blob/next/LICENSE
export const resetZoom = createIcon(
+1 -4
View File
@@ -1,7 +1,5 @@
import { FontFamily } from "./element/types";
export const APP_NAME = "Excalidraw";
export const DRAGGING_THRESHOLD = 10; // 10px
export const LINE_CONFIRM_THRESHOLD = 10; // 10px
export const ELEMENT_SHIFT_TRANSLATE_AMOUNT = 5;
@@ -73,7 +71,7 @@ export const DEFAULT_VERTICAL_ALIGN = "top";
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
export const GRID_SIZE = 20;
export const GRID_SIZE = 20; // TODO make it configurable?
export const MIME_TYPES = {
excalidraw: "application/vnd.excalidraw+json",
@@ -87,4 +85,3 @@ export const STORAGE_KEYS = {
// time in milliseconds
export const TAP_TWICE_TIMEOUT = 300;
export const TOUCH_CTX_MENU_TIMEOUT = 500;
export const TITLE_TIMEOUT = 10000;
+1 -1
View File
@@ -9,7 +9,7 @@ import { AppState } from "../types";
import { restore } from "./restore";
import { ImportedDataState, LibraryData } from "./types";
const parseFileContents = async (blob: Blob | File) => {
export const parseFileContents = async (blob: Blob | File) => {
let contents: string;
if (blob.type === "image/png") {
+71 -1
View File
@@ -1,10 +1,14 @@
import { fileSave } from "browser-nativefs";
import { EVENT_IO, trackEvent } from "../analytics";
import { getDefaultAppState } from "../appState";
import {
copyCanvasToClipboardAsPng,
copyTextToSystemClipboard,
} from "../clipboard";
import { NonDeletedExcalidrawElement } from "../element/types";
import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "../element/types";
import { t } from "../i18n";
import { exportToCanvas, exportToSvg } from "../scene/export";
import { ExportType } from "../scene/types";
@@ -15,6 +19,65 @@ import { serializeAsJSON } from "./json";
export { loadFromBlob } from "./blob";
export { loadFromJSON, saveAsJSON } from "./json";
const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
export const exportToBackend = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const json = serializeAsJSON(elements, appState);
const encoded = new TextEncoder().encode(json);
const key = await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 128,
},
true, // extractable
["encrypt", "decrypt"],
);
// The iv is set to 0. We are never going to reuse the same key so we don't
// need to have an iv. (I hope that's correct...)
const iv = new Uint8Array(12);
// We use symmetric encryption. AES-GCM is the recommended algorithm and
// includes checks that the ciphertext has not been modified by an attacker.
const encrypted = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
},
key,
encoded,
);
// We use jwk encoding to be able to extract just the base64 encoded key.
// We will hardcode the rest of the attributes when importing back the key.
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
try {
const response = await fetch(BACKEND_V2_POST, {
method: "POST",
body: encrypted,
});
const json = await response.json();
if (json.id) {
const url = new URL(window.location.href);
// We need to store the key (and less importantly the id) as hash instead
// of queryParam in order to never send it to the server
url.hash = `json=${json.id},${exportedKey.k!}`;
const urlString = url.toString();
window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString);
trackEvent(EVENT_IO, "export", "backend");
} else if (json.error_class === "RequestTooLargeError") {
window.alert(t("alerts.couldNotCreateShareableLinkTooBig"));
} else {
window.alert(t("alerts.couldNotCreateShareableLink"));
}
} catch (error) {
console.error(error);
window.alert(t("alerts.couldNotCreateShareableLink"));
}
};
export const exportCanvas = async (
type: ExportType,
elements: readonly NonDeletedExcalidrawElement[],
@@ -106,6 +169,13 @@ export const exportCanvas = async (
}
throw new Error(t("alerts.couldNotCopyToClipboard"));
}
} else if (type === "backend") {
exportToBackend(elements, {
...appState,
viewBackgroundColor: exportBackground
? appState.viewBackgroundColor
: getDefaultAppState().viewBackgroundColor,
});
}
// clean up the DOM
+6 -6
View File
@@ -102,7 +102,7 @@ export class LinearElementEditor {
element,
scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y,
appState.showGrid,
appState.gridSize,
);
LinearElementEditor.movePoint(element, activePointIndex, newPoint);
if (isBindingElement(element)) {
@@ -198,7 +198,7 @@ export class LinearElementEditor {
element,
scenePointer.x,
scenePointer.y,
appState.showGrid,
appState.gridSize,
),
],
});
@@ -282,7 +282,7 @@ export class LinearElementEditor {
scenePointerX: number,
scenePointerY: number,
editingLinearElement: LinearElementEditor,
isGridOn: boolean,
gridSize: number | null,
): LinearElementEditor {
const { elementId, lastUncommittedPoint } = editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
@@ -304,7 +304,7 @@ export class LinearElementEditor {
element,
scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y,
isGridOn,
gridSize,
);
if (lastPoint === lastUncommittedPoint) {
@@ -398,9 +398,9 @@ export class LinearElementEditor {
element: NonDeleted<ExcalidrawLinearElement>,
scenePointerX: number,
scenePointerY: number,
isGridOn: boolean,
gridSize: number | null,
): Point {
const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, isGridOn);
const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
+10 -7
View File
@@ -24,13 +24,13 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
// (see https://github.com/microsoft/TypeScript/issues/21732)
const { points } = updates as any;
if (typeof points !== "undefined") {
if (points !== undefined) {
updates = { ...getSizeFromPoints(points), ...updates };
}
for (const key in updates) {
const value = (updates as any)[key];
if (typeof value !== "undefined") {
if (value !== undefined) {
if (
(element as any)[key] === value &&
// if object, always update in case its deep prop was mutated
@@ -72,9 +72,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
}
if (
typeof updates.height !== "undefined" ||
typeof updates.width !== "undefined" ||
typeof points !== "undefined"
updates.height !== undefined ||
updates.width !== undefined ||
points !== undefined
) {
invalidateShapeForElement(element);
}
@@ -84,9 +84,12 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
Scene.getScene(element)?.informMutation();
};
export const newElementWith = <TElement extends ExcalidrawElement>(
export const newElementWith = <
TElement extends ExcalidrawElement,
K extends keyof Omit<TElement, "id" | "version" | "versionNonce">
>(
element: TElement,
updates: ElementUpdate<TElement>,
updates: Pick<TElement, K>,
): TElement => ({
...element,
...updates,
+14 -1
View File
@@ -21,7 +21,7 @@ type _ExcalidrawElementBase = Readonly<{
strokeWidth: number;
strokeStyle: StrokeStyle;
strokeSharpness: StrokeSharpness;
roughness: number;
roughness: 0 | 1 | 2;
opacity: number;
width: number;
height: number;
@@ -110,3 +110,16 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
startArrowhead: Arrowhead | null;
endArrowhead: Arrowhead | null;
}>;
export type ExcalidrawElementTypes = Pick<ExcalidrawElement, "type">["type"];
/** @private */
type __ExcalidrawElementPossibleProps_withoutType<T> = T extends any
? { [K in keyof Omit<T, "type">]: T[K] }
: never;
/** Do not use for anything unless you really need it for some abstract
API types */
export type ExcalidrawElementPossibleProps = UnionToIntersection<
__ExcalidrawElementPossibleProps_withoutType<ExcalidrawElement>
> & { type: ExcalidrawElementTypes };
+7 -3
View File
@@ -1,7 +1,7 @@
import React, { PureComponent } from "react";
import throttle from "lodash.throttle";
import { APP_NAME, ENV, EVENT } from "../../constants";
import { ENV, EVENT } from "../../constants";
import {
decryptAESGEM,
@@ -157,7 +157,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
};
openPortal = async () => {
window.history.pushState({}, APP_NAME, await generateCollaborationLink());
window.history.pushState(
{},
"Excalidraw",
await generateCollaborationLink(),
);
const elements = this.excalidrawRef.current!.getSceneElements();
// remove deleted elements from elements array & history to ensure we don't
// expose potentially sensitive user data in case user manually deletes
@@ -174,7 +178,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
closePortal = () => {
this.saveCollabRoomToFirebase();
window.history.pushState({}, APP_NAME, window.location.origin);
window.history.pushState({}, "Excalidraw", window.location.origin);
this.destroySocketClient();
trackEvent(EVENT_SHARE, "session end");
};
+1 -60
View File
@@ -3,14 +3,12 @@ import { ExcalidrawElement } from "../../element/types";
import { AppState } from "../../types";
import { ImportedDataState } from "../../data/types";
import { restore } from "../../data/restore";
import { EVENT_ACTION, EVENT_IO, trackEvent } from "../../analytics";
import { serializeAsJSON } from "../../data/json";
import { EVENT_ACTION, trackEvent } from "../../analytics";
const byteToHex = (byte: number): string => `0${byte.toString(16)}`.slice(-2);
const BACKEND_GET = process.env.REACT_APP_BACKEND_V1_GET_URL;
const BACKEND_V2_GET = process.env.REACT_APP_BACKEND_V2_GET_URL;
const BACKEND_V2_POST = process.env.REACT_APP_BACKEND_V2_POST_URL;
const generateRandomID = async () => {
const arr = new Uint8Array(10);
@@ -230,60 +228,3 @@ export const loadScene = async (
commitToHistory: false,
};
};
export const exportToBackend = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const json = serializeAsJSON(elements, appState);
const encoded = new TextEncoder().encode(json);
const key = await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 128,
},
true, // extractable
["encrypt", "decrypt"],
);
// The iv is set to 0. We are never going to reuse the same key so we don't
// need to have an iv. (I hope that's correct...)
const iv = new Uint8Array(12);
// We use symmetric encryption. AES-GCM is the recommended algorithm and
// includes checks that the ciphertext has not been modified by an attacker.
const encrypted = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
},
key,
encoded,
);
// We use jwk encoding to be able to extract just the base64 encoded key.
// We will hardcode the rest of the attributes when importing back the key.
const exportedKey = await window.crypto.subtle.exportKey("jwk", key);
try {
const response = await fetch(BACKEND_V2_POST, {
method: "POST",
body: encrypted,
});
const json = await response.json();
if (json.id) {
const url = new URL(window.location.href);
// We need to store the key (and less importantly the id) as hash instead
// of queryParam in order to never send it to the server
url.hash = `json=${json.id},${exportedKey.k!}`;
const urlString = url.toString();
window.prompt(`🔒${t("alerts.uploadedSecurly")}`, urlString);
trackEvent(EVENT_IO, "export", "backend");
} else if (json.error_class === "RequestTooLargeError") {
window.alert(t("alerts.couldNotCreateShareableLinkTooBig"));
} else {
window.alert(t("alerts.couldNotCreateShareableLink"));
}
} catch (error) {
console.error(error);
window.alert(t("alerts.couldNotCreateShareableLink"));
}
};
+15 -61
View File
@@ -13,22 +13,16 @@ import { ImportedDataState } from "../data/types";
import CollabWrapper, { CollabAPI } from "./collab/CollabWrapper";
import { TopErrorBoundary } from "../components/TopErrorBoundary";
import { t } from "../i18n";
import { exportToBackend, loadScene } from "./data";
import { loadScene } from "./data";
import { getCollaborationLinkData } from "./data";
import { EVENT } from "../constants";
import { loadFromFirebase } from "./data/firebase";
import { ExcalidrawImperativeAPI } from "../components/App";
import { debounce, ResolvablePromise, resolvablePromise } from "../utils";
import { AppState, ExcalidrawAPIRefValue } from "../types";
import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "../element/types";
import { ExcalidrawElement } from "../element/types";
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT } from "./app_constants";
import { EVENT_LOAD, EVENT_SHARE, trackEvent } from "../analytics";
import { ErrorDialog } from "../components/ErrorDialog";
import { getDefaultAppState } from "../appState";
import { APP_NAME, TITLE_TIMEOUT } from "../constants";
const excalidrawRef: React.MutableRefObject<
MarkRequired<ExcalidrawAPIRefValue, "ready" | "readyPromise">
@@ -119,7 +113,7 @@ const initializeScene = async (opts: {
scene = await loadScene(jsonMatch[1], jsonMatch[2], initialData);
}
if (!isCollabScene) {
window.history.replaceState({}, APP_NAME, window.location.origin);
window.history.replaceState({}, "Excalidraw", window.location.origin);
}
} else {
// https://github.com/excalidraw/excalidraw/issues/1919
@@ -138,7 +132,7 @@ const initializeScene = async (opts: {
}
isCollabScene = false;
window.history.replaceState({}, APP_NAME, window.location.origin);
window.history.replaceState({}, "Excalidraw", window.location.origin);
}
}
if (isCollabScene) {
@@ -184,7 +178,6 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
width: window.innerWidth,
height: window.innerHeight,
});
const [errorMessage, setErrorMessage] = useState("");
useLayoutEffect(() => {
const onResize = () => {
@@ -247,10 +240,6 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
}
};
const titleTimeout = setTimeout(
() => (document.title = APP_NAME),
TITLE_TIMEOUT,
);
window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
window.addEventListener(EVENT.UNLOAD, onBlur, false);
window.addEventListener(EVENT.BLUR, onBlur, false);
@@ -258,7 +247,6 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
window.removeEventListener(EVENT.UNLOAD, onBlur, false);
window.removeEventListener(EVENT.BLUR, onBlur, false);
clearTimeout(titleTimeout);
};
}, [collab.initializeSocketClient]);
@@ -272,52 +260,18 @@ function ExcalidrawWrapper(props: { collab: CollabAPI }) {
}
};
const onExportToBackend = async (
exportedElements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
canvas: HTMLCanvasElement | null,
) => {
if (exportedElements.length === 0) {
return window.alert(t("alerts.cannotExportEmptyCanvas"));
}
if (canvas) {
try {
await exportToBackend(exportedElements, {
...appState,
viewBackgroundColor: appState.exportBackground
? appState.viewBackgroundColor
: getDefaultAppState().viewBackgroundColor,
});
} catch (error) {
if (error.name !== "AbortError") {
const { width, height } = canvas;
console.error(error, { width, height });
setErrorMessage(error.message);
}
}
}
};
return (
<>
<Excalidraw
ref={excalidrawRef}
onChange={onChange}
width={dimensions.width}
height={dimensions.height}
initialData={initialStatePromiseRef.current.promise}
user={{ name: collab.username }}
onCollabButtonClick={collab.onCollabButtonClick}
isCollaborating={collab.isCollaborating}
onPointerUpdate={collab.onPointerUpdate}
onExportToBackend={onExportToBackend}
/>
{errorMessage && (
<ErrorDialog
message={errorMessage}
onClose={() => setErrorMessage("")}
/>
)}
</>
<Excalidraw
ref={excalidrawRef}
onChange={onChange}
width={dimensions.width}
height={dimensions.height}
initialData={initialStatePromiseRef.current.promise}
user={{ name: collab.username }}
onCollabButtonClick={collab.onCollabButtonClick}
isCollaborating={collab.isCollaborating}
onPointerUpdate={collab.onPointerUpdate}
/>
);
}
+9
View File
@@ -46,6 +46,15 @@ type MarkOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> &
Required<Pick<T, RK>>;
type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (
x: infer R,
) => any
? R
: never;
/** Assert K is a subset of T, and returns K */
type AssertSubset<T, K extends T> = K;
// PNG encoding/decoding
// -----------------------------------------------------------------------------
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
-1
View File
@@ -9,7 +9,6 @@ export const CODES = {
BRACKET_RIGHT: "BracketRight",
BRACKET_LEFT: "BracketLeft",
ONE: "Digit1",
TWO: "Digit2",
NINE: "Digit9",
QUOTE: "Quote",
ZERO: "Digit0",
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "تحديد الكل",
"multiSelect": "إضافة عنصر للتحديد",
"moveCanvas": "نقل لوح رسم",
"cut": "",
"copy": "نسخ",
"copyAsPng": "نسخ إلى الحافظة بصيغة PNG",
"copyAsSvg": "نسخ بصيغة SVG",
@@ -29,11 +28,6 @@
"edges": "الحواف",
"sharp": "حادة",
"round": "دائرية",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "حجم الخط",
"fontFamily": "نوع الخط",
"onlySelected": "المحدد فقط",
@@ -77,11 +71,9 @@
"ungroup": "إلغاء تحديد مجموعة",
"collaborators": "المتعاونون",
"toggleGridMode": "التبديل إلى وضع الشبكة",
"toggleStats": "",
"addToLibrary": "أضف إلى المكتبة",
"removeFromLibrary": "حذف من المكتبة",
"libraryLoadingMessage": "جارٍ تحميل المكتبة...",
"libraries": "",
"loadingScene": "جاري تحميل المشهد...",
"align": "محاذاة",
"alignTop": "محاذاة إلى اﻷعلى",
@@ -213,22 +205,13 @@
"textNewLine": "إضافة سطر جديد (نص)",
"textFinish": "الانتهاء من تحرير (النص)",
"zoomToFit": "تكبير لتلائم جميع العناصر",
"zoomToSelection": "",
"preventBinding": "منع ربط السهم"
},
"encrypted": {
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا."
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "قمت بلصق جدول بيانات دون عمود رقمي.",
"tooManyColumns": "قمت بلصق جدول بيانات دون عمود رقمي."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Маркирай всичко",
"multiSelect": "",
"moveCanvas": "",
"cut": "",
"copy": "Копирай",
"copyAsPng": "Копиране в клипборда",
"copyAsSvg": "Копиране в клипборда",
@@ -29,11 +28,6 @@
"edges": "",
"sharp": "",
"round": "",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "Размер на шрифта",
"fontFamily": "Семейство шрифтове",
"onlySelected": "Само избраното",
@@ -77,11 +71,9 @@
"ungroup": "",
"collaborators": "",
"toggleGridMode": "",
"toggleStats": "",
"addToLibrary": "",
"removeFromLibrary": "",
"libraryLoadingMessage": "",
"libraries": "",
"loadingScene": "",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "Добавяне на нов ред (текст)",
"textFinish": "Завършете редактиране (текст)",
"zoomToFit": "",
"zoomToSelection": "",
"preventBinding": ""
},
"encrypted": {
"tooltip": ""
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "",
"tooManyColumns": ""
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Seleccionar tot",
"multiSelect": "Afegir element a la selecció",
"moveCanvas": "Moure el llenç",
"cut": "",
"copy": "Copiar",
"copyAsPng": "Copiar al porta-retalls com a PNG",
"copyAsSvg": "Copiar al porta-retalls com a SVG",
@@ -29,11 +28,6 @@
"edges": "Vores",
"sharp": "Agut",
"round": "Arrodonit",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "Mida de lletra",
"fontFamily": "Tipus de lletra",
"onlySelected": "Només seleccionats",
@@ -77,11 +71,9 @@
"ungroup": "Desagrupar la selecció",
"collaborators": "Col·laboradors",
"toggleGridMode": "Commutar línies de graella",
"toggleStats": "",
"addToLibrary": "Afegir a la biblioteca",
"removeFromLibrary": "Eliminar de la biblioteca",
"libraryLoadingMessage": "Carregant la biblioteca...",
"libraries": "",
"loadingScene": "Carregant escena ...",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "Afegir línea nova (text)",
"textFinish": "Acabar d'editar (text)",
"zoomToFit": "Zoom per veure tots els elements",
"zoomToSelection": "",
"preventBinding": "Prevenir vinculació de la fletxa"
},
"encrypted": {
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors dExcalidraw no els veuran mai."
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "Has enganxat un full de càlcul sense columna numèrica.",
"tooManyColumns": "Has enganxat un full de càlcul amb més de dues columnes."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Alle auswählen",
"multiSelect": "Element zur Auswahl hinzufügen",
"moveCanvas": "Leinwand verschieben",
"cut": "Ausschneiden",
"copy": "Kopieren",
"copyAsPng": "In Zwischenablage kopieren (PNG)",
"copyAsSvg": "In Zwischenablage kopieren (SVG)",
@@ -29,11 +28,6 @@
"edges": "Kanten",
"sharp": "Eckig",
"round": "Rund",
"arrowheads": "Pfeilspitzen",
"arrowhead_none": "Keine",
"arrowhead_arrow": "Pfeil",
"arrowhead_bar": "Balken",
"arrowhead_dot": "Punkt",
"fontSize": "Schriftgröße",
"fontFamily": "Schriftfamilie",
"onlySelected": "Nur ausgewählte",
@@ -77,11 +71,9 @@
"ungroup": "Gruppierung aufheben",
"collaborators": "Mitarbeitende",
"toggleGridMode": "Gitterlinien ein-/ausschalten",
"toggleStats": "Statistiken für Nerds ein-/ausschalten",
"addToLibrary": "Zur Bibliothek hinzufügen",
"removeFromLibrary": "Aus Bibliothek entfernen",
"libraryLoadingMessage": "Lade Bibliothek...",
"libraries": "Bibliotheken durchsuchen",
"loadingScene": "Lade Zeichnung...",
"align": "Ausrichten",
"alignTop": "Obere Kanten",
@@ -213,22 +205,13 @@
"textNewLine": "Neue Zeile hinzufügen (Text)",
"textFinish": "Bearbeiten beenden (Text)",
"zoomToFit": "Zoomen um alle Elemente einzupassen",
"zoomToSelection": "",
"preventBinding": "Pfeil-Bindung verhindern"
},
"encrypted": {
"tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals."
},
"stats": {
"angle": "Winkel",
"element": "Element",
"elements": "Elemente",
"height": "Höhe",
"scene": "Zeichnung",
"selected": "Ausgewählt",
"storage": "Speicher",
"title": "Statistiken für Nerds",
"total": "Gesamt",
"width": "Breite"
"charts": {
"noNumericColumn": "Du hast eine Tabelle ohne numerische Spalte eingefügt.",
"tooManyColumns": "Du hast eine Tabelle mit mehr als zwei Spalten eingefügt."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Επιλογή όλων",
"multiSelect": "Προσθέστε το στοιχείο στην επιλογή",
"moveCanvas": "Μετακίνηση καμβά",
"cut": "Αποκοπή",
"copy": "Αντιγραφή",
"copyAsPng": "Αντιγραφή στο πρόχειρο ως PNG",
"copyAsSvg": "Αντιγραφή στο πρόχειρο ως SVG",
@@ -29,11 +28,6 @@
"edges": "Άκρες",
"sharp": "Οξύ",
"round": "Στρογγυλό",
"arrowheads": "Σύμβολα βελών",
"arrowhead_none": "Κανένα",
"arrowhead_arrow": "Βέλος",
"arrowhead_bar": "Μπάρα",
"arrowhead_dot": "Τελεία",
"fontSize": "Μέγεθος γραμματοσειράς",
"fontFamily": "Γραμματοσειρά",
"onlySelected": "Μόνο τα Επιλεγμένα",
@@ -77,11 +71,9 @@
"ungroup": "Κατάργηση ομάδας από επιλογή",
"collaborators": "Συνεργάτες",
"toggleGridMode": "Εναλλαγή λειτουργίας πλέγματος",
"toggleStats": "",
"addToLibrary": "Προσθήκη στη βιβλιοθήκη",
"removeFromLibrary": "Αφαίρεση από τη βιβλιοθήκη",
"libraryLoadingMessage": "Φόρτωση βιβλιοθήκης...",
"libraries": "Άλλες βιβλιοθήκες",
"loadingScene": "Φόρτωση σκηνής...",
"align": "Στοίχιση",
"alignTop": "Στοίχιση πάνω",
@@ -213,22 +205,13 @@
"textNewLine": "Προσθήκη νέας γραμμής (κείμενο)",
"textFinish": "Ολοκλήρωση επεξεργασίας (κείμενο)",
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
"zoomToSelection": "",
"preventBinding": "Αποτροπή δέσμευσης βέλων"
},
"encrypted": {
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα έιναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw."
},
"stats": {
"angle": "Γωνία",
"element": "Στοιχείο",
"elements": "Στοιχεία",
"height": "Ύψος",
"scene": "",
"selected": "Επιλεγμένα",
"storage": "Χώρος",
"title": "",
"total": "Σύνολο ",
"width": "Πλάτος"
"charts": {
"noNumericColumn": "Επικόλλησες ένα υπολογιστικό φύλλο χωρίς αριθμητική στήλη.",
"tooManyColumns": "Επικόλλησες ένα υπολογιστικό φύλλο με περισσότερες από δύο στήλες."
}
}
+7 -9
View File
@@ -37,7 +37,7 @@
"fontSize": "Font size",
"fontFamily": "Font family",
"onlySelected": "Only selected",
"withBackground": "With background",
"withBackground": "With Background",
"exportEmbedScene": "Embed scene into exported file",
"exportEmbedScene_details": "Scene data will be saved into the exported PNG/SVG file so that the scene can be restored from it.\nWill increase exported file size.",
"addWatermark": "Add \"Made with Excalidraw\"",
@@ -76,7 +76,8 @@
"group": "Group selection",
"ungroup": "Ungroup selection",
"collaborators": "Collaborators",
"gridMode": "Grid mode",
"toggleGridMode": "Toggle grid mode",
"toggleStats": "Toggle stats for nerds",
"addToLibrary": "Add to library",
"removeFromLibrary": "Remove from library",
"libraryLoadingMessage": "Loading library...",
@@ -117,10 +118,9 @@
"redo": "Redo",
"roomDialog": "Start live collaboration",
"createNewRoom": "Create new room",
"fullScreen": "Full screen",
"darkMode": "Dark mode",
"lightMode": "Light mode",
"zenMode": "Zen mode",
"toggleFullScreen": "Toggle full screen",
"toggleDarkMode": "Toggle dark mode",
"toggleZenMode": "Toggle zen mode",
"exitZenMode": "Exit zen mode"
},
"alerts": {
@@ -136,7 +136,7 @@
"loadSceneOverridePrompt": "Loading external drawing will replace your existing content. Do you wish to continue?",
"errorLoadingLibrary": "There was an error loading the third party library.",
"confirmAddLibrary": "This will add {{numShapes}} shape(s) to your library. Are you sure?",
"imageDoesNotContainScene": "Importing images isn't supported at the moment.\n\nDid you want to import a scene? This image does not seem to contain any scene data. Have you enabled this during export?",
"imageDoesNotContainScene": "Image file doesn't contain scene data. Have you enabled this during export?",
"cannotRestoreFromImage": "Scene couldn't be restored from this image file"
},
"toolBar": {
@@ -161,7 +161,6 @@
"freeDraw": "Click and drag, release when you're finished",
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
"linearElementMulti": "Click on last point or press Escape or Enter to finish",
"lockAngle": "You can constrain angle by holding SHIFT",
"resize": "You can constrain proportions by holding SHIFT while resizing,\nhold ALT to resize from the center",
"rotate": "You can constrain angles by holding SHIFT while rotating",
"lineEditor_info": "Double-click or press Enter to edit points",
@@ -214,7 +213,6 @@
"textNewLine": "Add new line (text)",
"textFinish": "Finish editing (text)",
"zoomToFit": "Zoom to fit all elements",
"zoomToSelection": "Zoom to selection",
"preventBinding": "Prevent arrow binding"
},
"encrypted": {
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Seleccionar todo",
"multiSelect": "Añadir elemento a la selección",
"moveCanvas": "Mover el lienzo",
"cut": "",
"copy": "Copiar",
"copyAsPng": "Copiar al portapapeles como PNG",
"copyAsSvg": "Copiar al portapapeles como SVG",
@@ -29,11 +28,6 @@
"edges": "Bordes",
"sharp": "Afilado",
"round": "Redondo",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "Tamaño de la fuente",
"fontFamily": "Tipo de fuente",
"onlySelected": "Sólo seleccionados",
@@ -77,11 +71,9 @@
"ungroup": "Desagrupar",
"collaborators": "Colaboradores",
"toggleGridMode": "Alternar modo cuadrícula",
"toggleStats": "",
"addToLibrary": "Añadir a la biblioteca",
"removeFromLibrary": "Eliminar de la biblioteca",
"libraryLoadingMessage": "Cargando biblioteca...",
"libraries": "",
"loadingScene": "Cargando escena...",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "Añadir nueva línea (texto)",
"textFinish": "Finalizar edición (texto)",
"zoomToFit": "Ajustar para mostrar todos los elementos",
"zoomToSelection": "",
"preventBinding": "Evitar enlace de flecha"
},
"encrypted": {
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán."
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "Pegaste una hoja de cálculo sin una columna numérica.",
"tooManyColumns": "Pegaste una hoja de cálculo con más de dos columnas."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "انتخاب همه",
"multiSelect": "یک ایتم به انتخاب شده ها اضافه کنید.",
"moveCanvas": "بوم را حرکت بدهید",
"cut": "",
"copy": "کپی",
"copyAsPng": "کپی در حافطه موقت به صورت PNG",
"copyAsSvg": "کپی در حافطه موقت به صورت SVG",
@@ -29,11 +28,6 @@
"edges": "لبه ها",
"sharp": "تیز",
"round": "دور",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "اندازه قلم",
"fontFamily": "نوع قلم",
"onlySelected": "فقط انتخاب شده ها",
@@ -77,11 +71,9 @@
"ungroup": "حذف گروهبندی انتخابها",
"collaborators": "همکاران",
"toggleGridMode": "سويچ خطوط راهنما",
"toggleStats": "",
"addToLibrary": "افزودن به کتابخانه",
"removeFromLibrary": "حذف از کتابخانه",
"libraryLoadingMessage": "بارگذاری کتابخانه...",
"libraries": "",
"loadingScene": "باگذاری صحنه...",
"align": "تراز",
"alignTop": "تراز به بالا",
@@ -213,22 +205,13 @@
"textNewLine": "یک خط جدید اضافه کنید (متن)",
"textFinish": "پایان ویرایش (متن)",
"zoomToFit": "بزرگنمایی برای دیدن تمام آیتم ها",
"zoomToSelection": "",
"preventBinding": "مانع شدن از چسبیدن فلش ها"
},
"encrypted": {
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند."
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "شما یک صفحه گسترده را بدون ستون عددی کپی کرده اید.",
"tooManyColumns": "شما یک صفحه گسترده را با بیش از دو ستون کپی کرده اید."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Valitse kaikki",
"multiSelect": "Lisää kohde valintaan",
"moveCanvas": "Siirrä piirtoaluetta",
"cut": "Leikkaa",
"copy": "Kopioi",
"copyAsPng": "Kopioi leikepöydälle PNG-tiedostona",
"copyAsSvg": "Kopioi leikepöydälle SVG-tiedostona",
@@ -29,11 +28,6 @@
"edges": "Reunat",
"sharp": "Terävä",
"round": "Pyöreä",
"arrowheads": "Nuolenkärjet",
"arrowhead_none": "Ei mitään",
"arrowhead_arrow": "Nuoli",
"arrowhead_bar": "Tasapää",
"arrowhead_dot": "Piste",
"fontSize": "Kirjasinkoko",
"fontFamily": "Kirjasintyyppi",
"onlySelected": "Vain valitut",
@@ -77,11 +71,9 @@
"ungroup": "Pura valittu ryhmä",
"collaborators": "Yhteistyökumppanit",
"toggleGridMode": "Ruudukko päälle/pois",
"toggleStats": "Nörttien tilastot päälle/pois",
"addToLibrary": "Lisää kirjastoon",
"removeFromLibrary": "Poista kirjastosta",
"libraryLoadingMessage": "Ladataan kirjastoa...",
"libraries": "Selaa kirjastoja",
"loadingScene": "Ladataan työtä...",
"align": "Tasaa",
"alignTop": "Tasaa ylös",
@@ -213,22 +205,13 @@
"textNewLine": "Lisää uusi rivi (teksti)",
"textFinish": "Lopeta muokkaus (teksti)",
"zoomToFit": "Zoomaa kaikki elementit näkyviin",
"zoomToSelection": "Zoomaa valintaan",
"preventBinding": "Estä nuolten sitominen"
},
"encrypted": {
"tooltip": "Piirroksesi ovat päästä päähän salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä."
},
"stats": {
"angle": "Kulma",
"element": "Elementti",
"elements": "Elementit",
"height": "Korkeus",
"scene": "Teos",
"selected": "Valitut",
"storage": "Tallennustila",
"title": "Nörttien tilastot",
"total": "Yhteensä",
"width": "Leveys"
"charts": {
"noNumericColumn": "Liitit taulukon ilman lukuja sisältävää saraketta.",
"tooManyColumns": "Liitit taulukon, jossa on enemmän kuin kaksi saraketta."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Tout sélectionner",
"multiSelect": "Ajouter l'élément à la sélection",
"moveCanvas": "Déplacer le canvas",
"cut": "Couper",
"copy": "Copier",
"copyAsPng": "Copier dans le presse-papier en PNG",
"copyAsSvg": "Copier dans le presse-papier en SVG",
@@ -29,11 +28,6 @@
"edges": "Angles",
"sharp": "Aigu",
"round": "Rond",
"arrowheads": "Extrémités de ligne",
"arrowhead_none": "Aucun",
"arrowhead_arrow": "Flèche",
"arrowhead_bar": "Barre",
"arrowhead_dot": "Point",
"fontSize": "Taille de la police",
"fontFamily": "Police",
"onlySelected": "Uniquement la sélection",
@@ -77,11 +71,9 @@
"ungroup": "Dégrouper la sélection",
"collaborators": "Collaborateurs",
"toggleGridMode": "Basculer le mode grille",
"toggleStats": "Activer/désactiver les stats pour les nerds",
"addToLibrary": "Ajouter à la bibliothèque",
"removeFromLibrary": "Supprimer de la bibliothèque",
"libraryLoadingMessage": "Chargement de la bibliothèque...",
"libraries": "Explorer les bibliothèques",
"loadingScene": "Chargement de la scène...",
"align": "Aligner",
"alignTop": "Aligner en haut",
@@ -213,22 +205,13 @@
"textNewLine": "Ajouter une nouvelle ligne (texte)",
"textFinish": "Terminer l'édition (texte)",
"zoomToFit": "Zoomer pour visualiser tous les éléments",
"zoomToSelection": "Zoom sur la sélection",
"preventBinding": "Empêcher la liaison de la flèche"
},
"encrypted": {
"tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais."
},
"stats": {
"angle": "Angle",
"element": "Élément",
"elements": "Éléments",
"height": "Hauteur",
"scene": "Scène",
"selected": "Sélectionné",
"storage": "Stockage",
"title": "Stats pour nerds",
"total": "Total",
"width": "Largeur"
"charts": {
"noNumericColumn": "Vous avez collé une feuille de calcul sans données numérique.",
"tooManyColumns": "Vous avez collé une feuille de calcul avec plus de deux colonnes."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "בחר הכל",
"multiSelect": "",
"moveCanvas": "",
"cut": "",
"copy": "העתק",
"copyAsPng": "העתק ללוח כ PNG",
"copyAsSvg": "העתק ללוח כ SVG",
@@ -29,11 +28,6 @@
"edges": "",
"sharp": "",
"round": "",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "גודל גופן",
"fontFamily": "סוג הגופן",
"onlySelected": "רק מה שנבחר",
@@ -77,11 +71,9 @@
"ungroup": "פרק קבוצה",
"collaborators": "",
"toggleGridMode": "",
"toggleStats": "",
"addToLibrary": "",
"removeFromLibrary": "",
"libraryLoadingMessage": "",
"libraries": "",
"loadingScene": "",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "הוסף שורה חדשה (טקסט)",
"textFinish": "סיים עריכה (טקסט)",
"zoomToFit": "זום להתאמת כל האלמנטים למסך",
"zoomToSelection": "",
"preventBinding": ""
},
"encrypted": {
"tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם."
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "",
"tooManyColumns": ""
}
}
+4 -21
View File
@@ -4,7 +4,6 @@
"selectAll": "सभी चुनें",
"multiSelect": "आकार को चयन में जोड़ें",
"moveCanvas": "कैनवास को स्थानांतरित करें",
"cut": "काटें",
"copy": "प्रतिलिपि",
"copyAsPng": "क्लिपबोर्ड पर कॉपी करें ,पीएनजी के रूप में",
"copyAsSvg": "क्लिपबोर्ड पर कॉपी करें,एसवीजी के रूप में",
@@ -29,11 +28,6 @@
"edges": "किनारा",
"sharp": "नुकीला",
"round": "गोल",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "तीर",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "फ़ॉन्ट का आकार",
"fontFamily": "फ़ॉन्ट का परिवार",
"onlySelected": "केवल चयनित",
@@ -69,7 +63,7 @@
"language": "भाषा",
"createRoom": "अधिवेशन",
"duplicateSelection": "डुप्लिकेट",
"untitled": "अशीर्षित",
"untitled": "",
"name": "नाम",
"yourName": "आपका नाम",
"madeWithExcalidraw": "मेड विथ एक्सकैलिडराव",
@@ -77,11 +71,9 @@
"ungroup": "समूह चयन असमूहीकृत करें",
"collaborators": "सहयोगी",
"toggleGridMode": "टॉगल ग्रिड मोड",
"toggleStats": "",
"addToLibrary": "लाइब्रेरी से जोड़ें",
"removeFromLibrary": "लाइब्रेरी से निकालें",
"libraryLoadingMessage": "लाइब्रेरी खुल रही है",
"libraries": "",
"loadingScene": "दृश्य खुल रहा है",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "नई पंक्ति (पाठ) जोड़ें",
"textFinish": "संपादन समाप्त करें (पाठ)",
"zoomToFit": "सभी तत्वों को फिट करने के लिए ज़ूम करें",
"zoomToSelection": "",
"preventBinding": ""
},
"encrypted": {
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।"
},
"stats": {
"angle": "कोण",
"element": "",
"elements": "",
"height": "ऊंचाई",
"scene": "दृश्य",
"selected": "चयनित",
"storage": "संग्रह",
"title": "",
"total": "कुल",
"width": "चौड़ाई"
"charts": {
"noNumericColumn": "आपने एक संख्यात्मक कॉलम के बिना एक स्प्रेडशीट चिपकाई।",
"tooManyColumns": "आपने दो से अधिक कॉलम के साथ एक स्प्रेडशीट चिपकाई।"
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Összes kijelölése",
"multiSelect": "",
"moveCanvas": "",
"cut": "",
"copy": "Másolás",
"copyAsPng": "Vágólapra másolás mint PNG",
"copyAsSvg": "Vágólapra másolás mint SVG",
@@ -29,11 +28,6 @@
"edges": "",
"sharp": "",
"round": "",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "",
"fontFamily": "",
"onlySelected": "Csak a kiválasztott",
@@ -77,11 +71,9 @@
"ungroup": "",
"collaborators": "",
"toggleGridMode": "",
"toggleStats": "",
"addToLibrary": "",
"removeFromLibrary": "",
"libraryLoadingMessage": "",
"libraries": "",
"loadingScene": "",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "",
"textFinish": "",
"zoomToFit": "",
"zoomToSelection": "",
"preventBinding": ""
},
"encrypted": {
"tooltip": ""
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "",
"tooManyColumns": ""
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Pilih semua",
"multiSelect": "Tambahkan elemen ke pilihan",
"moveCanvas": "Pindahkan kanvas",
"cut": "Potong",
"copy": "Salin",
"copyAsPng": "Salin ke papan klip sebagai PNG",
"copyAsSvg": "Salin ke papan klip sebagai SVG",
@@ -29,11 +28,6 @@
"edges": "Tepi",
"sharp": "Tajam",
"round": "Bulat",
"arrowheads": "Mata panah",
"arrowhead_none": "Tidak ada",
"arrowhead_arrow": "Panah",
"arrowhead_bar": "Batang",
"arrowhead_dot": "Titik",
"fontSize": "Ukuran font",
"fontFamily": "Jenis font",
"onlySelected": "Hanya yang Dipilih",
@@ -77,11 +71,9 @@
"ungroup": "Pisahkan pilihan",
"collaborators": "Kolaborator",
"toggleGridMode": "Aktifkan/Matikan mode kisi",
"toggleStats": "Aktifkan statistik untuk nerd",
"addToLibrary": "Tambahkan ke pustaka",
"removeFromLibrary": "Hapus dari pustaka",
"libraryLoadingMessage": "Memuat pustaka...",
"libraries": "Telusur pustaka",
"loadingScene": "Memuat pemandangan...",
"align": "Perataan",
"alignTop": "Rata atas",
@@ -213,22 +205,13 @@
"textNewLine": "Tambahkan baris baru (teks)",
"textFinish": "Selesai mengedit (teks)",
"zoomToFit": "Perbesar agar sesuai dengan semua elemen",
"zoomToSelection": "",
"preventBinding": "Cegah pengikatan panah"
},
"encrypted": {
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya."
},
"stats": {
"angle": "Sudut",
"element": "Elemen",
"elements": "Elemen",
"height": "Tinggi",
"scene": "Pemandangan",
"selected": "Terpilih",
"storage": "Penyimpanan",
"title": "Statistik untuk nerd",
"total": "Total",
"width": "Lebar"
"charts": {
"noNumericColumn": "Anda menempelkan sebuah lembar bentang tanpa sebuah kolom numerik.",
"tooManyColumns": "Anda menempelkan sebuah lembar bentang dengan lebih dari dua kolom."
}
}
+5 -22
View File
@@ -4,7 +4,6 @@
"selectAll": "Seleziona tutto",
"multiSelect": "Aggiungi elemento alla selezione",
"moveCanvas": "Sposta tela",
"cut": "Taglia",
"copy": "Copia",
"copyAsPng": "Copia negli appunti come PNG",
"copyAsSvg": "Copia negli appunti come SVG",
@@ -29,11 +28,6 @@
"edges": "Bordi",
"sharp": "Acuto",
"round": "Rotondo",
"arrowheads": "Punta della freccia",
"arrowhead_none": "Nessuno",
"arrowhead_arrow": "Freccia",
"arrowhead_bar": "Barra",
"arrowhead_dot": "Punto",
"fontSize": "Dimensione carattere",
"fontFamily": "Carattere",
"onlySelected": "Solo selezionati",
@@ -76,12 +70,10 @@
"group": "Crea gruppo da selezione",
"ungroup": "Dividi gruppo da selezione",
"collaborators": "Collaboratori",
"toggleGridMode": "Attiva/disattiva modalità griglia",
"toggleStats": "Attiva/disattiva statistiche per nerd",
"toggleGridMode": "Attiva/disattiva modalità quadrícula",
"addToLibrary": "Aggiungi alla biblioteca",
"removeFromLibrary": "Rimuovi dalla biblioteca",
"libraryLoadingMessage": "Caricamento della biblioteca...",
"libraries": "Sfoglia librerie",
"loadingScene": "Caricamento della scena...",
"align": "Allinea",
"alignTop": "Allinea in alto",
@@ -124,7 +116,7 @@
"exitZenMode": "Uscire dalla modalità zen"
},
"alerts": {
"clearReset": "Questa azione cancellerà l'intera tela. Sei sicuro?",
"clearReset": "Questo cancellerà l'intera tela. Sei sicuro?",
"couldNotCreateShareableLink": "Non riesco a creare un link condivisibile.",
"couldNotCreateShareableLinkTooBig": "Impossibile creare il link condivisibile: la scena è troppo grande",
"couldNotLoadInvalidFile": "Impossibile caricare un file no valido",
@@ -213,22 +205,13 @@
"textNewLine": "Aggiungi nuova riga (testo)",
"textFinish": "Completa la modifica (testo)",
"zoomToFit": "Adatta zoom per mostrare tutti gli elementi",
"zoomToSelection": "Zoom alla selezione",
"preventBinding": "Prevenire l'associazione freccia"
},
"encrypted": {
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere."
},
"stats": {
"angle": "Angolo",
"element": "Elemento",
"elements": "Elementi",
"height": "Altezza",
"scene": "Scena",
"selected": "Selezionato",
"storage": "Memoria",
"title": "Statistiche per nerd",
"total": "Totale",
"width": "Larghezza"
"charts": {
"noNumericColumn": "Hai incollato un foglio di calcolo senza una colonna numerica.",
"tooManyColumns": "Hai incollato un foglio di calcolo con più di due colonne."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "すべて選択",
"multiSelect": "複数選択",
"moveCanvas": "キャンバスを移動",
"cut": "",
"copy": "コピー",
"copyAsPng": "PNGとしてクリップボードへコピー",
"copyAsSvg": "SVGとしてクリップボードへコピー",
@@ -29,11 +28,6 @@
"edges": "角",
"sharp": "四角",
"round": "丸",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "フォントの大きさ",
"fontFamily": "フォントの種類",
"onlySelected": "選択中のみ",
@@ -77,11 +71,9 @@
"ungroup": "グループ化を解除",
"collaborators": "共同編集者",
"toggleGridMode": "グリッドモードに切り替える",
"toggleStats": "",
"addToLibrary": "ライブラリに追加",
"removeFromLibrary": "ライブラリから削除",
"libraryLoadingMessage": "ライブラリを読み込み中...",
"libraries": "",
"loadingScene": "シーンを読み込み中...",
"align": "整列",
"alignTop": "上揃え",
@@ -213,22 +205,13 @@
"textNewLine": "テキストの改行",
"textFinish": "テキストの編集を終える",
"zoomToFit": "すべての図形が収まるよう拡大/縮小",
"zoomToSelection": "",
"preventBinding": "矢印を結合しない"
},
"encrypted": {
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。"
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "数値の列が存在しないスプレッドシートをペーストしました。",
"tooManyColumns": "2列以上のスプレッドシートを貼り付けました."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "전체 선택",
"multiSelect": "선택 영역에 추가하기",
"moveCanvas": "캔버스 이동",
"cut": "",
"copy": "복사하기",
"copyAsPng": "클립보드로 PNG 이미지 복사",
"copyAsSvg": "클립보드로 SVG 이미지 복사",
@@ -29,11 +28,6 @@
"edges": "가장자리",
"sharp": "선명하게",
"round": "둥글게",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "폰트 크기",
"fontFamily": "폰트 스타일",
"onlySelected": "선택한 항목만",
@@ -77,11 +71,9 @@
"ungroup": "그룹 해제",
"collaborators": "공동 작업자",
"toggleGridMode": "격자 모드 켜기/끄기",
"toggleStats": "",
"addToLibrary": "라이브러리에 추가",
"removeFromLibrary": "라이브러리에서 제거",
"libraryLoadingMessage": "라이브러리 불러오는 중...",
"libraries": "",
"loadingScene": "화면 불러오는 중...",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "줄바꾸기",
"textFinish": "편집 완료",
"zoomToFit": "",
"zoomToSelection": "",
"preventBinding": ""
},
"encrypted": {
"tooltip": ""
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "",
"tooManyColumns": ""
}
}
+7 -24
View File
@@ -4,7 +4,6 @@
"selectAll": "အကုန်ရွေး",
"multiSelect": "ရွေးထားသည့်ထဲပုံထည့်",
"moveCanvas": "ကားချပ်ရွှေ့",
"cut": "",
"copy": "ကူး",
"copyAsPng": "PNG အနေဖြင့်ကူး",
"copyAsSvg": "SVG အနေဖြင့်ကူး",
@@ -29,11 +28,6 @@
"edges": "အစွန်း",
"sharp": "ထောင့်ချွန်",
"round": "ထောင့်ဝိုင်း",
"arrowheads": "မြှားခေါင်း",
"arrowhead_none": "ဘာမျှမရှိ",
"arrowhead_arrow": "မြှား",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "စာလုံးအရွယ်",
"fontFamily": "စာလုံးပုံစံ",
"onlySelected": "ရွေးထားသလောက်",
@@ -60,7 +54,7 @@
"architect": "ဗိသုကာ",
"artist": "ပန်းချီ",
"cartoonist": "ကာတွန်း",
"fileTitle": "ခေါင်းစဉ်",
"fileTitle": "",
"colorPicker": "အရောင်ရွေး",
"canvasBackground": "ကားချပ်နောက်ခံ",
"drawingCanvas": "ပုံဆွဲကားချပ်",
@@ -69,7 +63,7 @@
"language": "ဘာသာစကား",
"createRoom": "တိုက်ရိုက်ပူးပေါင်းဆောင်ရွက်ရန်အဖွဲ့ဖွဲ့",
"duplicateSelection": "ပွား",
"untitled": "အမည်မရှိ",
"untitled": "",
"name": "အမည်",
"yourName": "သင့်အမည်",
"madeWithExcalidraw": "Excalidraw ဖြင့်ဖန်တီးသည်။",
@@ -77,11 +71,9 @@
"ungroup": "အုပ်စုဖျက်သိမ်း",
"collaborators": "ပူးပေါင်းပါဝင်သူများ",
"toggleGridMode": "ဇယားကွက်ဖော်/ဖျောက်",
"toggleStats": "",
"addToLibrary": "မှတ်တမ်းတင်",
"removeFromLibrary": "မှတ်တမ်းမှထုတ်",
"libraryLoadingMessage": "မှတ်တမ်းအား တင်သွင်းနေသည်...",
"libraries": "စာကြည့်တိုက်တွင်ရှာဖွေပါ",
"loadingScene": "မြင်ကွင်းဖော်နေသည်...",
"align": "ချိန်ညှိ",
"alignTop": "ထိပ်ညှိ",
@@ -90,8 +82,8 @@
"alignRight": "ညာညှိ",
"centerVertically": "ဒေါင်လိုက်အလယ်ညှိ",
"centerHorizontally": "အလျားလိုက်အလယ်ညှိ",
"distributeHorizontally": "အလျားလိုက်",
"distributeVertically": "ထောင်လိုက်"
"distributeHorizontally": "",
"distributeVertically": ""
},
"buttons": {
"clearReset": "ကားချပ်ရှင်းလင်း",
@@ -213,22 +205,13 @@
"textNewLine": "စာသားဖြည့်သွင်း",
"textFinish": "စာသားဖြည့်သွင်းပြီး",
"zoomToFit": "ကားချပ်အပြည့်ဖေါ်",
"zoomToSelection": "",
"preventBinding": "မြှားများမပေါင်းစေရန်"
},
"encrypted": {
"tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။"
},
"stats": {
"angle": "ထောင့်",
"element": "",
"elements": "",
"height": "အမြင့်",
"scene": "မြင်ကွင်း",
"selected": "ရွေးချယ်သည်",
"storage": "သိုလှောင်ခန်း",
"title": "အက္ခရာများအတွက်အချက်အလက်များ",
"total": "စုစုပေါင်း",
"width": "အကျယ်"
"charts": {
"noNumericColumn": "အမှတ်စဉ်ကော်လံမပါဝင်သောဇယားအားထည့်သွင်းလိုက်သည်။",
"tooManyColumns": "ကော်လံနှစ်ခုထက်ပိုပါနေသောဇယားအားထည့်သွင်းလိုက်သည်။"
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Velg alt",
"multiSelect": "Legg til element i utvalg",
"moveCanvas": "Flytt lerretet",
"cut": "Klipp ut",
"copy": "Kopier",
"copyAsPng": "Kopier til PNG",
"copyAsSvg": "Kopier til utklippstavlen som SVG",
@@ -29,11 +28,6 @@
"edges": "Kanter",
"sharp": "Skarp",
"round": "Rund",
"arrowheads": "Pilspisser",
"arrowhead_none": "Ingen",
"arrowhead_arrow": "Pil",
"arrowhead_bar": "Søyle",
"arrowhead_dot": "Prikk",
"fontSize": "Skriftstørrelse",
"fontFamily": "Fontfamilie",
"onlySelected": "Kun valgte",
@@ -77,11 +71,9 @@
"ungroup": "Avgruppér utvalg",
"collaborators": "Samarbeidspartnere",
"toggleGridMode": "Slå av/på rutenett",
"toggleStats": "Skru av/på statistikk for nerder",
"addToLibrary": "Legg til i bibliotek",
"removeFromLibrary": "Fjern fra bibliotek",
"libraryLoadingMessage": "Laster bibliotek...",
"libraries": "Bla gjennom biblioteker",
"loadingScene": "Laster inn scene...",
"align": "Juster",
"alignTop": "Juster øverst",
@@ -213,22 +205,13 @@
"textNewLine": "Legg til ny linje (tekst)",
"textFinish": "Fullfør redigering (tekst)",
"zoomToFit": "Zoom for å passe alle elementene",
"zoomToSelection": "Zoom til utvalg",
"preventBinding": "Forhindre pilbinding"
},
"encrypted": {
"tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem."
},
"stats": {
"angle": "Vinkel",
"element": "Element",
"elements": "Elementer",
"height": "Høyde",
"scene": "Scene",
"selected": "Valgt",
"storage": "Lagring",
"title": "Statistikk for nerder",
"total": "Totalt",
"width": "Bredde"
"charts": {
"noNumericColumn": "Du limte inn et regneark uten en numerisk kolonne.",
"tooManyColumns": "Du limte inn et regneark med mer enn to kolonner."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Alles selecteren",
"multiSelect": "Voeg element toe aan selectie",
"moveCanvas": "Canvas verplaatsen",
"cut": "",
"copy": "Kopiëren",
"copyAsPng": "Kopieer als PNG",
"copyAsSvg": "Kopieer als SVG",
@@ -29,11 +28,6 @@
"edges": "Randen",
"sharp": "Hoekig",
"round": "Rond",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "Tekstgrootte",
"fontFamily": "Lettertype",
"onlySelected": "Enkel geselecteerde",
@@ -77,11 +71,9 @@
"ungroup": "Groep opheffen",
"collaborators": "Deelnemers",
"toggleGridMode": "Rasterlijnen in-/uitschakelen",
"toggleStats": "",
"addToLibrary": "Voeg toe aan bibliotheek",
"removeFromLibrary": "Verwijder uit bibliotheek",
"libraryLoadingMessage": "Bibliotheek laden...",
"libraries": "",
"loadingScene": "Scène laden...",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "Nieuwe regel toevoegen (tekst)",
"textFinish": "Voltooi bewerken (tekst)",
"zoomToFit": "Zoom in op alle elementen",
"zoomToSelection": "",
"preventBinding": ""
},
"encrypted": {
"tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent."
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "Je hebt een werkblad geplakt zonder een numerieke kolom.",
"tooManyColumns": "Je hebt een werkblad geplakt met meer dan twee kolommen."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Vel alt",
"multiSelect": "Legg til element i utval",
"moveCanvas": "Flytt lerretet",
"cut": "",
"copy": "Kopier",
"copyAsPng": "Kopier til utklippstavla som PNG",
"copyAsSvg": "Kopier til utklippstavla som SVG",
@@ -29,11 +28,6 @@
"edges": "Kanter",
"sharp": "Skarp",
"round": "Rund",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "Skriftstorleik",
"fontFamily": "Skrifttype",
"onlySelected": "Kun valde",
@@ -77,11 +71,9 @@
"ungroup": "Avgrupper utval",
"collaborators": "Samarbeidarar",
"toggleGridMode": "Sla på/av rutenett",
"toggleStats": "",
"addToLibrary": "Legg til i bibliotek",
"removeFromLibrary": "Fjern frå bibliotek",
"libraryLoadingMessage": "Laster bibliotek...",
"libraries": "",
"loadingScene": "Laster scene...",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "Legg til ny linje (tekst)",
"textFinish": "Fullfør redigering (tekst)",
"zoomToFit": "Zoom for å sjå alle elementa",
"zoomToSelection": "",
"preventBinding": "Hindre pilkobling"
},
"encrypted": {
"tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei."
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "Du limte inn eit rekneark utan ei numerisk kolonne.",
"tooManyColumns": "Du limte inn eit rekneark med meir enn to kolonnar."
}
}
+23 -23
View File
@@ -1,33 +1,33 @@
{
"ar-SA": 89,
"bg-BG": 61,
"ca-ES": 81,
"de-DE": 99,
"el-GR": 95,
"ar-SA": 98,
"bg-BG": 67,
"ca-ES": 89,
"de-DE": 100,
"el-GR": 96,
"en": 100,
"es-ES": 81,
"fa-IR": 89,
"es-ES": 89,
"fa-IR": 98,
"fi-FI": 100,
"fr-FR": 100,
"he-IL": 69,
"hi-IN": 82,
"hu-HU": 44,
"id-ID": 99,
"he-IL": 76,
"hi-IN": 85,
"hu-HU": 48,
"id-ID": 100,
"it-IT": 100,
"ja-JP": 89,
"ko-KR": 68,
"my-MM": 96,
"ja-JP": 98,
"ko-KR": 75,
"my-MM": 97,
"nb-NO": 100,
"nl-NL": 80,
"nn-NO": 80,
"pl-PL": 79,
"pt-PT": 83,
"nl-NL": 88,
"nn-NO": 89,
"pl-PL": 88,
"pt-PT": 92,
"ro-RO": 100,
"ru-RU": 81,
"ru-RU": 85,
"sk-SK": 100,
"sv-SE": 100,
"tr-TR": 81,
"uk-UA": 98,
"zh-CN": 95,
"zh-TW": 99
"tr-TR": 90,
"uk-UA": 100,
"zh-CN": 100,
"zh-TW": 100
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Zaznacz wszystko",
"multiSelect": "Dodaj element do zaznaczenia",
"moveCanvas": "Przesuń obszar roboczy",
"cut": "",
"copy": "Kopiuj",
"copyAsPng": "Skopiuj do schowka jako plik PNG",
"copyAsSvg": "Skopiuj do schowka jako plik SVG",
@@ -29,11 +28,6 @@
"edges": "Krawędzie",
"sharp": "Ostry",
"round": "Zaokrąglij",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "Rozmiar tekstu",
"fontFamily": "Krój pisma",
"onlySelected": "Tylko wybrane",
@@ -77,11 +71,9 @@
"ungroup": "Rozgrupuj wybrane",
"collaborators": "Współtwórcy",
"toggleGridMode": "Włącz siatkę",
"toggleStats": "",
"addToLibrary": "Dodaj do biblioteki",
"removeFromLibrary": "Usuń z biblioteki",
"libraryLoadingMessage": "Wczytywanie biblioteki...",
"libraries": "",
"loadingScene": "Ładowanie sceny...",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "Dodaj nową linię (tekst)",
"textFinish": "Zakończ edycję (tekst)",
"zoomToFit": "Powiększ, aby wyświetlić wszystkie elementy",
"zoomToSelection": "",
"preventBinding": "Zablokuj przywiązanie strzałek do obiektu"
},
"encrypted": {
"tooltip": "Twoje rysunki są zabezpieczone szyfrowaniem end-to-end, tak więc nawet w Excalidraw nie jesteśmy w stanie zobaczyć tego co tworzysz."
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "Wklejono arkusz kalkulacyjny bez kolumny numerycznej.",
"tooManyColumns": "Wklejono arkusz kalkulacyjny z więcej niż dwoma kolumnami."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Selecionar tudo",
"multiSelect": "Adicionar elemento à seleção",
"moveCanvas": "Mover tela",
"cut": "",
"copy": "Copiar",
"copyAsPng": "Copiar para a área de transferência como PNG",
"copyAsSvg": "Copiar para a área de transferência como SVG",
@@ -29,11 +28,6 @@
"edges": "Arestas",
"sharp": "Aguçado",
"round": "Redondo",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "Tamanho da fonte",
"fontFamily": "Família da fontes",
"onlySelected": "Somente a seleção",
@@ -77,11 +71,9 @@
"ungroup": "Desagrupar seleção",
"collaborators": "Colaboradores",
"toggleGridMode": "Alternar modo de grade",
"toggleStats": "",
"addToLibrary": "Adicionar à biblioteca",
"removeFromLibrary": "Remover da biblioteca",
"libraryLoadingMessage": "Carregando biblioteca...",
"libraries": "",
"loadingScene": "Carregando cena...",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "Adicionar nova linha (texto)",
"textFinish": "Finalizar edição (texto)",
"zoomToFit": "Ajustar para caber todos os elementos",
"zoomToSelection": "",
"preventBinding": "Prevenir fixação de seta"
},
"encrypted": {
"tooltip": "Seus desenhos são criptografados de ponta a ponta, então os servidores do Excalidraw nunca os verão."
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "Você colou uma planilha sem uma coluna numérica.",
"tooManyColumns": "Você colou uma planilha com mais de duas colunas."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Selectare totală",
"multiSelect": "Adaugă element la selecție",
"moveCanvas": "Mutare pânză",
"cut": "Decupare",
"copy": "Copiere",
"copyAsPng": "Copiere în memoria temporară ca PNG",
"copyAsSvg": "Copiere în memoria temporară ca SVG",
@@ -29,11 +28,6 @@
"edges": "Margini",
"sharp": "Ascuțite",
"round": "Rotunde",
"arrowheads": "Vârfuri de săgeată",
"arrowhead_none": "Niciunul",
"arrowhead_arrow": "Săgeată",
"arrowhead_bar": "Bară",
"arrowhead_dot": "Bulină",
"fontSize": "Dimensiune font",
"fontFamily": "Familia de fonturi",
"onlySelected": "Numai selecția",
@@ -77,11 +71,9 @@
"ungroup": "Degrupare selecție",
"collaborators": "Colaboratori",
"toggleGridMode": "Comută modul grilă",
"toggleStats": "Comută statisticile pentru pasionați",
"addToLibrary": "Adăugare la bibliotecă",
"removeFromLibrary": "Eliminare din bibliotecă",
"libraryLoadingMessage": "Se încarcă biblioteca...",
"libraries": "Răsfoiește bibliotecile",
"loadingScene": "Se încarcă scena...",
"align": "Aliniere",
"alignTop": "Aliniere sus",
@@ -213,22 +205,13 @@
"textNewLine": "Adaugă o linie nouă (text)",
"textFinish": "Finalizează editarea (text)",
"zoomToFit": "Apropiere/depărtare pentru a cuprinde totul",
"zoomToSelection": "Panoramare la selecție",
"preventBinding": "Împiedică legarea săgeții"
},
"encrypted": {
"tooltip": "Desenele tale sunt criptate integral, astfel că serverele Excalidraw nu le vor vedea niciodată."
},
"stats": {
"angle": "Unghi",
"element": "Element",
"elements": "Elemente",
"height": "Înălțime",
"scene": "Scenă",
"selected": "Selectate",
"storage": "Stocare",
"title": "Statistici pentru pasionați",
"total": "Total",
"width": "Lățime"
"charts": {
"noNumericColumn": "Ai inserat o foaie de calcul fără o coloană numerică.",
"tooManyColumns": "Ai inserat o foaie de calcul cu mai mult de două coloane."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Выбрать все",
"multiSelect": "Добавить элемент к выбору",
"moveCanvas": "Переместить холст",
"cut": "",
"copy": "Копировать",
"copyAsPng": "Скопировать в буфер обмена как PNG",
"copyAsSvg": "Скопировать в буфер обмена как SVG",
@@ -29,11 +28,6 @@
"edges": "Края",
"sharp": "Острые",
"round": "Скругленные",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "Cтрелка",
"arrowhead_bar": "",
"arrowhead_dot": "Точка",
"fontSize": "Размер шрифта",
"fontFamily": "Семейство шрифтов",
"onlySelected": "Только выбранные",
@@ -77,11 +71,9 @@
"ungroup": "Разделить выделение",
"collaborators": "Сотрудники",
"toggleGridMode": "Переключить режим сетки",
"toggleStats": "",
"addToLibrary": "Добавить в библиотеку",
"removeFromLibrary": "Удалить из библиотеки",
"libraryLoadingMessage": "Загрузка библиотеки...",
"libraries": "",
"loadingScene": "Загрузка сцены...",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "Добавить новую строку (текст)",
"textFinish": "Закончить редактирование (текст)",
"zoomToFit": "",
"zoomToSelection": "",
"preventBinding": "Предотвратить привязку стрелок"
},
"encrypted": {
"tooltip": ""
},
"stats": {
"angle": "Угол",
"element": "Элемент",
"elements": "Элементы",
"height": "Высота",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": "Ширина"
"charts": {
"noNumericColumn": "",
"tooManyColumns": "Вы вставили таблицу с более чем двумя столбцами."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Vybrať všetko",
"multiSelect": "Pridať prvok do výberu",
"moveCanvas": "Pohyb plátna",
"cut": "Vystrihnúť",
"copy": "Kopírovať",
"copyAsPng": "Kopírovať do schránky ako PNG",
"copyAsSvg": "Kopírovať do schránky ako SVG",
@@ -29,11 +28,6 @@
"edges": "Okraje",
"sharp": "Ostré",
"round": "Zaokrúhlené",
"arrowheads": "Zakončenie šípky",
"arrowhead_none": "Žiadne",
"arrowhead_arrow": "Šípka",
"arrowhead_bar": "Čiara",
"arrowhead_dot": "Bod",
"fontSize": "Veľkosť písma",
"fontFamily": "Písmo",
"onlySelected": "Iba vybrané",
@@ -77,11 +71,9 @@
"ungroup": "Zrušiť zoskupenie",
"collaborators": "Spolupracovníci",
"toggleGridMode": "Prepnúť mriežku",
"toggleStats": "Prepnúť štatistiky",
"addToLibrary": "Pridať do knižnice",
"removeFromLibrary": "Odstrániť z knižnice",
"libraryLoadingMessage": "Načítavanie knižnice...",
"libraries": "Prehliadať knižnice",
"loadingScene": "Načítavanie scény...",
"align": "Zarovnanie",
"alignTop": "Zarovnať nahor",
@@ -213,22 +205,13 @@
"textNewLine": "Vložiť nový riadok (text)",
"textFinish": "Ukončenie editovania (text)",
"zoomToFit": "Priblížiť aby boli zahrnuté všetky prvky",
"zoomToSelection": "Priblížiť na výber",
"preventBinding": "Zakázať pripájanie šípky"
},
"encrypted": {
"tooltip": "Vaše kresby používajú end-to-end šifrovanie, takže ich Excalidraw server nedokáže prečítať."
},
"stats": {
"angle": "Uhol",
"element": "Prvok",
"elements": "Prvky",
"height": "Výška",
"scene": "Scéna",
"selected": "Vybrané",
"storage": "Úložisko",
"title": "Štatistiky",
"total": "Celkom",
"width": "Šírka"
"charts": {
"noNumericColumn": "Prilepili ste tabuľku bez číselného stĺpca.",
"tooManyColumns": "Prilepili ste tabuľku s viac ako dvoma stĺpcami."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Markera alla",
"multiSelect": "Lägg till element till markering",
"moveCanvas": "Flytta canvas",
"cut": "Klipp ut",
"copy": "Kopiera",
"copyAsPng": "Kopiera till urklipp som PNG",
"copyAsSvg": "Kopiera till urklipp som SVG",
@@ -29,11 +28,6 @@
"edges": "Kanter",
"sharp": "Skarp",
"round": "Rund",
"arrowheads": "Pilhuvuden",
"arrowhead_none": "Inga",
"arrowhead_arrow": "Pil",
"arrowhead_bar": "Stolpe",
"arrowhead_dot": "Punkt",
"fontSize": "Teckenstorlek",
"fontFamily": "Teckensnitt",
"onlySelected": "Endast markering",
@@ -77,11 +71,9 @@
"ungroup": "Avgruppera markering",
"collaborators": "Medarbetare",
"toggleGridMode": "Växla rutnätsläge",
"toggleStats": "Visa/dölj statistik för nördar",
"addToLibrary": "Lägg till i biblioteket",
"removeFromLibrary": "Ta bort från bibliotek",
"libraryLoadingMessage": "Laddar bibliotek...",
"libraries": "Bläddra i bibliotek",
"loadingScene": "Laddar scen...",
"align": "Justera",
"alignTop": "Justera överkant",
@@ -213,22 +205,13 @@
"textNewLine": "Lägg till ny rad (text)",
"textFinish": "Slutför redigering (text)",
"zoomToFit": "Zooma för att rymma alla element",
"zoomToSelection": "Zooma till markering",
"preventBinding": "Förhindra pilbindning"
},
"encrypted": {
"tooltip": "Dina skisser är krypterade från ände till ände så Excalidraws servrar kommer aldrig att se dem."
},
"stats": {
"angle": "Vinkel",
"element": "Element",
"elements": "Element",
"height": "Höjd",
"scene": "Skiss",
"selected": "Valda",
"storage": "Lagring",
"title": "Statistik för nördar",
"total": "Totalt",
"width": "Bredd"
"charts": {
"noNumericColumn": "Du klistrade in ett kalkylblad utan en numerisk kolumn.",
"tooManyColumns": "Du klistrade in ett kalkylblad med mer än två kolumner."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Tümünü seç",
"multiSelect": "Seçime öge ekle",
"moveCanvas": "Tuvali taşı",
"cut": "",
"copy": "Kopyala",
"copyAsPng": "Panoya PNG olarak kopyala",
"copyAsSvg": "Panoya SVG olarak kopyala",
@@ -29,11 +28,6 @@
"edges": "Kenarlar",
"sharp": "Keskin",
"round": "Yuvarlak",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "Yazı tipi boyutu",
"fontFamily": "Yazı tipi ailesi",
"onlySelected": "Sadece seçilen",
@@ -77,11 +71,9 @@
"ungroup": "Seçilen grubu dağıt",
"collaborators": "Ortaklar",
"toggleGridMode": "Izgara modunu aç",
"toggleStats": "",
"addToLibrary": "Kütüphaneye ekle",
"removeFromLibrary": "Kütüphaneden kaldır",
"libraryLoadingMessage": "Kütüphane yükleniyor...",
"libraries": "",
"loadingScene": "Çalışma alanı yükleniyor...",
"align": "",
"alignTop": "",
@@ -213,22 +205,13 @@
"textNewLine": "Yeni satır ekle (yazı)",
"textFinish": "(Yazıyı) düzenlemeyi bitir",
"zoomToFit": "Tüm öğeleri sığdırmak için yakınlaştır",
"zoomToSelection": "",
"preventBinding": "Ok bağlamayı önleyin"
},
"encrypted": {
"tooltip": "Çizimleriniz uçtan-uca şifrelenmiştir, Excalidraw'ın sunucuları bile onları göremez."
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"width": ""
"charts": {
"noNumericColumn": "Sayısal sütunu olmayan bir tablo yapıştırdın.",
"tooManyColumns": "İkiden daha fazla sütuna sahip bir tablo yapıştırdın."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "Вибрати все",
"multiSelect": "Додати елемент до вибраного",
"moveCanvas": "Перемістити полотно",
"cut": "Вирізати",
"copy": "Копіювати",
"copyAsPng": "Копіювати як PNG",
"copyAsSvg": "Копіювати як SVG",
@@ -29,11 +28,6 @@
"edges": "Краї",
"sharp": "Гострі",
"round": "Круглі",
"arrowheads": "",
"arrowhead_none": "Жоден",
"arrowhead_arrow": "Стрілка",
"arrowhead_bar": "Колона",
"arrowhead_dot": "Точка",
"fontSize": "Розмір шрифту",
"fontFamily": "Шрифт",
"onlySelected": "Тільки вибране",
@@ -77,11 +71,9 @@
"ungroup": "Розгрупувати виділене",
"collaborators": "Співавтори",
"toggleGridMode": "Режим сітки",
"toggleStats": "",
"addToLibrary": "Додати до бібліотеки",
"removeFromLibrary": "Видалити з бібліотеки",
"libraryLoadingMessage": "Завантажити бібліотеку...",
"libraries": "Огляд бібліотек",
"loadingScene": "Завантаження сцени...",
"align": "Вирівнювання",
"alignTop": "Вирівняти по верхньому краю",
@@ -213,22 +205,13 @@
"textNewLine": "Додати новий рядок (текст)",
"textFinish": "Завершити редагування (текст)",
"zoomToFit": "Збільшити щоб умістити все",
"zoomToSelection": "Перейти до виділеного",
"preventBinding": "Запобігти зв'язування зі стрілками"
},
"encrypted": {
"tooltip": "Ваші креслення захищені наскрізним шифруванням — сервери Excalidraw ніколи їх не побачать."
},
"stats": {
"angle": "Кут",
"element": "Елемент",
"elements": "Елементи",
"height": "Висота",
"scene": "Сцена",
"selected": "Вибраний",
"storage": "Сховище",
"title": "",
"total": "Всього",
"width": "Ширина"
"charts": {
"noNumericColumn": "Ви вставили таблицю без числової колонки.",
"tooManyColumns": "Ви вставляли таблицю з більш ніж двома колонками."
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "全部选中",
"multiSelect": "添加元素到选区",
"moveCanvas": "移动画布",
"cut": "",
"copy": "复制",
"copyAsPng": "复制为 PNG 到剪贴板",
"copyAsSvg": "复制为 SVG 到剪贴板",
@@ -29,11 +28,6 @@
"edges": "边角",
"sharp": "尖锐",
"round": "圆润",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"fontSize": "字体大小",
"fontFamily": "字体",
"onlySelected": "仅被选中",
@@ -77,11 +71,9 @@
"ungroup": "取消组选",
"collaborators": "协作者",
"toggleGridMode": "切换网格模式",
"toggleStats": "",
"addToLibrary": "添加到库中",
"removeFromLibrary": "从库中移除",
"libraryLoadingMessage": "正在加载库...",
"libraries": "",
"loadingScene": "正在加载绘图...",
"align": "对齐",
"alignTop": "顶部对齐",
@@ -213,22 +205,13 @@
"textNewLine": "文本换行",
"textFinish": "完成编辑文本",
"zoomToFit": "缩放以适应所有元素",
"zoomToSelection": "缩放至选择部分",
"preventBinding": "防止箭头吸附"
},
"encrypted": {
"tooltip": "您的绘图采用的端到端加密,因此Excalidraw服务器永远不会收集。"
},
"stats": {
"angle": "角度",
"element": "元素",
"elements": "元素",
"height": "高度",
"scene": "场景",
"selected": "选中",
"storage": "存储",
"title": "",
"total": "总计",
"width": "宽度"
"charts": {
"noNumericColumn": "您粘贴了一个没有数字列的表格。",
"tooManyColumns": "您粘贴了两列以上的表格。"
}
}
+3 -20
View File
@@ -4,7 +4,6 @@
"selectAll": "全選",
"multiSelect": "將物件加入選取範圍",
"moveCanvas": "移動畫布",
"cut": "剪下",
"copy": "複製",
"copyAsPng": "複製 PNG 至剪貼簿",
"copyAsSvg": "複製 SVG 至剪貼簿",
@@ -29,11 +28,6 @@
"edges": "邊緣",
"sharp": "尖銳",
"round": "平滑",
"arrowheads": "箭頭",
"arrowhead_none": "無",
"arrowhead_arrow": "箭頭",
"arrowhead_bar": "塊",
"arrowhead_dot": "點",
"fontSize": "字型大小",
"fontFamily": "字體",
"onlySelected": "僅選取物件",
@@ -77,11 +71,9 @@
"ungroup": "取消群組",
"collaborators": "協作者",
"toggleGridMode": "切換格線模式",
"toggleStats": "切換詳細統計",
"addToLibrary": "加入資料庫",
"removeFromLibrary": "從資料庫中移除",
"libraryLoadingMessage": "資料庫讀取中…",
"libraries": "",
"loadingScene": "場景讀取中…",
"align": "對齊",
"alignTop": "對齊頂部",
@@ -213,22 +205,13 @@
"textNewLine": "換行(文字)",
"textFinish": "完成編輯(文字)",
"zoomToFit": "放大至填滿畫面",
"zoomToSelection": "縮放至選取區",
"preventBinding": "防止箭頭綁定"
},
"encrypted": {
"tooltip": "你的作品已使用 end-to-end 方式加密,Excalidraw 的伺服器也無法取得其內容。"
},
"stats": {
"angle": "角度",
"element": "元素",
"elements": "元素",
"height": "高度",
"scene": "場景",
"selected": "已選",
"storage": "儲存",
"title": "詳細統計",
"total": "合計",
"width": "寬度"
"charts": {
"noNumericColumn": "你貼上的 spreadsheet 沒有數字欄。",
"tooManyColumns": "你貼上的 spreadsheet 超過兩欄。"
}
}
+5 -5
View File
@@ -1,5 +1,5 @@
import { Point } from "./types";
import { GRID_SIZE, LINE_CONFIRM_THRESHOLD } from "./constants";
import { LINE_CONFIRM_THRESHOLD } from "./constants";
import { ExcalidrawLinearElement } from "./element/types";
export const rotate = (
@@ -307,12 +307,12 @@ const doSegmentsIntersect = (p1: Point, q1: Point, p2: Point, q2: Point) => {
export const getGridPoint = (
x: number,
y: number,
isGridOn: boolean,
gridSize: number | null,
): [number, number] => {
if (isGridOn) {
if (gridSize) {
return [
Math.round(x / GRID_SIZE) * GRID_SIZE,
Math.round(y / GRID_SIZE) * GRID_SIZE,
Math.round(x / gridSize) * gridSize,
Math.round(y / gridSize) * gridSize,
];
}
return [x, y];
+10
View File
@@ -0,0 +1,10 @@
# Changelog
## 0.1.0
First release of `@excalidraw/excalidraw`
## 0.1.1
#### Fix
- Update the homepage URL so it redirects to correct readme [#2498](https://github.com/excalidraw/excalidraw/pull/2498)
-62
View File
@@ -1,62 +0,0 @@
# Changelog
<!--
Guidelines for changelog:
The change should be grouped under one of the below section and must contain PR link.
- Features: For new features.
- Fixes: For bug fixes.
- Chore: Changes for non src files example package.json.
- Improvements: For any improvements.
- Refactor: For any refactoring.
Please add the latest change on the top under the correct section.
-->
## [Unreleased]
### Features
- Add support for `exportToBackend` prop to allow host apps to implement shareable links [#2612](https://github.com/excalidraw/excalidraw/pull/2612/files)
- Add zoom to selection [#2522](https://github.com/excalidraw/excalidraw/pull/2522)
- Insert Library items in the middle of the screen [#2527](https://github.com/excalidraw/excalidraw/pull/2527)
- Show shortcut context menu [#2501](https://github.com/excalidraw/excalidraw/pull/2501)
- Aligns arrowhead schemas [#2517](https://github.com/excalidraw/excalidraw/pull/2517)
- Add Cut to menus [#2511](https://github.com/excalidraw/excalidraw/pull/2511)
- More Arrowheads: dot, bar [#2486](https://github.com/excalidraw/excalidraw/pull/2486)
- Support CSV graphs and improve the look and feel [#2495](https://github.com/excalidraw/excalidraw/pull/2495)
### Fixes
- Consistent case for export locale strings [#2622](https://github.com/excalidraw/excalidraw/pull/2622)
- Remove unnecessary console.error as it was polluting Sentry [#2637](https://github.com/excalidraw/excalidraw/pull/2637)
- Fix scroll-to-center on init for non-zero canvas offsets [#2445](https://github.com/excalidraw/excalidraw/pull/2445)
- Hide collab button when onCollabButtonClick not supplied [#2598](https://github.com/excalidraw/excalidraw/pull/2598)
- Fix resizing the pasted charts [#2586](https://github.com/excalidraw/excalidraw/pull/2586)
- Fix element visibility and zoom on cursor when canvas offset isn't 0. [#2534](https://github.com/excalidraw/excalidraw/pull/2534)
- Fix Library Menu Layout [#2502](https://github.com/excalidraw/excalidraw/pull/2502)
- Support number with commas in charts [#2636](https://github.com/excalidraw/excalidraw/pull/2636)
- Don't break zoom when zooming in on UI [#2638](https://github.com/excalidraw/excalidraw/pull/2638)
### Improvements
- Display proper tooltip for 2-point lines during resize, and normalize modifier key labels in hints [#2655](https://github.com/excalidraw/excalidraw/pull/2655)
- Improve error message around importing images [#2619](https://github.com/excalidraw/excalidraw/pull/2619)
- Add tooltip with icon for embedding scenes [#2532](https://github.com/excalidraw/excalidraw/pull/2532)
- RTL support for the stats dialog [#2530](https://github.com/excalidraw/excalidraw/pull/2530)
- Expand canvas padding based on zoom. [#2515](https://github.com/excalidraw/excalidraw/pull/2515)
- Hide shortcuts on pickers for mobile [#2508](https://github.com/excalidraw/excalidraw/pull/2508)
- Hide stats and scrollToContent-button when mobile menus open [#2509](https://github.com/excalidraw/excalidraw/pull/2509)
### Chore
- Bump ini from 1.3.5 to 1.3.7 in /src/packages/excalidraw [#2500](https://github.com/excalidraw/excalidraw/pull/2500)
## 0.1.1
#### Fix
- Update the homepage URL so it redirects to correct readme [#2498](https://github.com/excalidraw/excalidraw/pull/2498)
## 0.1.0
First release of `@excalidraw/excalidraw`
+23 -33
View File
@@ -39,19 +39,26 @@ import "./styles.css";
export default function App() {
const excalidrawRef = createRef();
const onChange = (elements, state) => {
console.log(excalidrawRef.current);
console.log("Elements :", elements, "State : ", state);
};
const [dimensions, setDimensions] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
const onResize = () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight,
});
};
useEffect(() => {
const onResize = () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
@@ -87,6 +94,7 @@ export default function App() {
excalidrawRef.current.updateScene(sceneData);
};
const { width, height } = dimensions;
return (
<div className="App">
<button className="update-scene" onClick={updateScene}>
@@ -103,17 +111,12 @@ export default function App() {
<div className="excalidraw-wrapper">
<Excalidraw
ref={excalidrawRef}
width={dimensions.width}
height={dimensions.height}
width={width}
height={height}
initialData={InitialData}
onChange={(elements, state) => {
console.log("Latest elements:", elements, "Latest state:", state);
}}
onChange={onChange}
user={{ name: "Excalidraw User" }}
onPointerUpdate={(pointerData) => console.log(pointerData)}
onCollabButtonClick={() => {
window.alert("You clicked on collab button");
}}
onPointerUpdate={(payload) => console.log(payload)}
/>
</div>
</div>
@@ -138,7 +141,6 @@ export default function App() {
| [`onCollabButtonClick`](#onCollabButtonClick) | Function | | Callback to be triggered when the collab button is clicked |
| [`isCollaborating`](#isCollaborating) | `boolean` | | This implies if the app is in collaboration mode |
| [`onPointerUpdate`](#onPointerUpdate) | Function | | Callback triggered when mouse pointer is updated. |
| [`onExportToBackend`](#onExportToBackend) | Function | | Callback triggered when link button is clicked on export dialog |
#### `width`
@@ -217,8 +219,8 @@ This is the user name which shows during collaboration. Defaults to `{name: ''}`
#### `excalidrawRef`
You can pass a `ref` when you want to access some excalidraw APIs.
We expose the below APIs:
You can pass a ref when you want to access some excalidraw API's.
We expose the below API's
| API | signature | Usage |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -239,15 +241,15 @@ const excalidrawRef = { current: { readyPromise: <a href="https://github.com/exc
#### `onCollabButtonClick`
This callback is triggered when clicked on the collab button in excalidraw. If not supplied, the collab dialog button is not rendered.
This callback is triggered when clicked on the collab button in excalidraw
#### `isCollaborating`
This prop indicates if the app is in collaboration mode.
This props implies if the app is in collaboration mode
#### `onPointerUpdate`
This callback is triggered when mouse pointer is updated.
This callback is triggered when mouse pointer is updated
```js
({ x, y }, button, pointersMap}) => void;
@@ -258,15 +260,3 @@ This callback is triggered when mouse pointer is updated.
2.`button`: The position of the button. This will be one of `["down", "up"]`
3.`pointersMap`: [`pointers map`](https://github.com/excalidraw/excalidraw/blob/182a3e39e1362d73d2a565c870eb2fb72071fdcc/src/types.ts#L122) of the scene
#### `onExportToBackend`
This callback is triggered when the shareable-link button is clicked in the export dialog. The link button will only be shown if this callback is passed.
```js
(exportedElements, appState, canvas) => void
```
1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/6e45cb95dbd7a8be1859c7055b06957298e3097c/src/element/types.ts#L76) which needs to be exported.
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/4c90ea5667d29effe8ec4a115e49efc7c340cdb3/src/types.ts#L33) of the scene.
3. `canvas`: The `HTMLCanvasElement` of the scene.
+2 -3
View File
@@ -8,6 +8,7 @@ import "../../css/styles.scss";
import { ExcalidrawAPIRefValue, ExcalidrawProps } from "../../types";
import { IsMobileProvider } from "../../is-mobile";
import { noop } from "../../utils";
const Excalidraw = (props: ExcalidrawProps) => {
const {
@@ -19,10 +20,9 @@ const Excalidraw = (props: ExcalidrawProps) => {
initialData,
user,
excalidrawRef,
onCollabButtonClick,
onCollabButtonClick = noop,
isCollaborating,
onPointerUpdate,
onExportToBackend,
} = props;
useEffect(() => {
@@ -58,7 +58,6 @@ const Excalidraw = (props: ExcalidrawProps) => {
onCollabButtonClick={onCollabButtonClick}
isCollaborating={isCollaborating}
onPointerUpdate={onPointerUpdate}
onExportToBackend={onExportToBackend}
/>
</IsMobileProvider>
</InitializeApp>
+4823 -1428
View File
File diff suppressed because it is too large Load Diff
+21 -21
View File
@@ -37,30 +37,30 @@
]
},
"peerDependencies": {
"react": "^17.0.1",
"react-dom": "^17.0.1"
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"devDependencies": {
"@babel/core": "7.12.10",
"@babel/plugin-transform-arrow-functions": "7.12.1",
"@babel/plugin-transform-async-to-generator": "7.12.1",
"@babel/plugin-transform-runtime": "7.12.10",
"@babel/plugin-transform-typescript": "7.12.1",
"@babel/preset-env": "7.12.11",
"@babel/preset-react": "7.12.10",
"@babel/preset-typescript": "7.12.7",
"babel-loader": "8.2.2",
"@babel/core": "7.9.0",
"@babel/plugin-transform-arrow-functions": "7.8.3",
"@babel/plugin-transform-async-to-generator": "7.8.3",
"@babel/plugin-transform-runtime": "7.12.1",
"@babel/plugin-transform-typescript": "7.9.4",
"@babel/preset-env": "7.9.5",
"@babel/preset-react": "7.9.4",
"@babel/preset-typescript": "7.9.0",
"babel-loader": "8.1.0",
"babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3",
"css-loader": "5.0.1",
"file-loader": "6.2.0",
"mini-css-extract-plugin": "1.3.3",
"sass-loader": "10.1.0",
"terser-webpack-plugin": "5.0.3",
"ts-loader": "8.0.12",
"webpack": "5.11.0",
"webpack-bundle-analyzer": "4.3.0",
"webpack-cli": "4.2.0"
"cross-env": "7.0.2",
"css-loader": "3.5.2",
"file-loader": "6.0.0",
"mini-css-extract-plugin": "0.8.0",
"sass-loader": "8.0.2",
"terser-webpack-plugin": "2.3.5",
"ts-loader": "7.0.0",
"webpack": "4.42.0",
"webpack-bundle-analyzer": "3.9.0",
"webpack-cli": "3.3.11"
},
"bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw",
+1 -1
View File
@@ -16,7 +16,7 @@ type ExportOpts = {
) => { width: number; height: number; scale: number };
};
export const exportToCanvas = ({
const exportToCanvas = ({
elements,
appState = getDefaultAppState(),
getDimensions = (width, height) => ({ width, height, scale: 1 }),
+1
View File
@@ -75,6 +75,7 @@ const excalidrawDiagram = {
],
appState: {
viewBackgroundColor: "#ffffff",
gridSize: null,
},
};
+1 -1
View File
@@ -1 +1 @@
export { exportToBlob, exportToSvg, exportToCanvas } from "../utils.ts";
export { exportToBlob, exportToSvg } from "../utils.ts";
+4268 -972
View File
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -34,21 +34,21 @@
]
},
"devDependencies": {
"@babel/core": "7.12.10",
"@babel/plugin-transform-arrow-functions": "7.12.1",
"@babel/plugin-transform-async-to-generator": "7.12.1",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/plugin-transform-typescript": "7.12.1",
"@babel/preset-env": "7.12.11",
"@babel/preset-typescript": "7.12.7",
"babel-loader": "8.2.2",
"@babel/core": "7.9.0",
"@babel/plugin-transform-arrow-functions": "7.8.3",
"@babel/plugin-transform-async-to-generator": "7.8.3",
"@babel/plugin-transform-runtime": "^7.12.1",
"@babel/plugin-transform-typescript": "7.9.4",
"@babel/preset-env": "7.9.5",
"@babel/preset-typescript": "7.9.0",
"babel-loader": "8.1.0",
"babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3",
"file-loader": "6.2.0",
"ts-loader": "8.0.12",
"webpack": "5.11.0",
"webpack-bundle-analyzer": "4.3.0",
"webpack-cli": "4.2.0"
"cross-env": "7.0.2",
"file-loader": "6.0.0",
"ts-loader": "7.0.0",
"webpack": "4.42.0",
"webpack-bundle-analyzer": "3.9.0",
"webpack-cli": "3.3.11"
},
"bugs": "https://github.com/excalidraw/excalidraw/issues",
"repository": "https://github.com/excalidraw/excalidraw",
+17 -23
View File
@@ -48,7 +48,6 @@ import {
TransformHandleType,
} from "../element/transformHandles";
import { viewportCoordsToSceneCoords } from "../utils";
import { GRID_SIZE } from "../constants";
const strokeRectWithRotation = (
context: CanvasRenderingContext2D,
@@ -119,6 +118,7 @@ const fillCircle = (
const strokeGrid = (
context: CanvasRenderingContext2D,
gridSize: number,
offsetX: number,
offsetY: number,
width: number,
@@ -127,13 +127,13 @@ const strokeGrid = (
const origStrokeStyle = context.strokeStyle;
context.strokeStyle = "rgba(0,0,0,0.1)";
context.beginPath();
for (let x = offsetX; x < offsetX + width + GRID_SIZE * 2; x += GRID_SIZE) {
context.moveTo(x, offsetY - GRID_SIZE);
context.lineTo(x, offsetY + height + GRID_SIZE * 2);
for (let x = offsetX; x < offsetX + width + gridSize * 2; x += gridSize) {
context.moveTo(x, offsetY - gridSize);
context.lineTo(x, offsetY + height + gridSize * 2);
}
for (let y = offsetY; y < offsetY + height + GRID_SIZE * 2; y += GRID_SIZE) {
context.moveTo(offsetX - GRID_SIZE, y);
context.lineTo(offsetX + width + GRID_SIZE * 2, y);
for (let y = offsetY; y < offsetY + height + gridSize * 2; y += gridSize) {
context.moveTo(offsetX - gridSize, y);
context.lineTo(offsetX + width + gridSize * 2, y);
}
context.stroke();
context.strokeStyle = origStrokeStyle;
@@ -233,15 +233,16 @@ export const renderScene = (
context.scale(sceneState.zoom.value, sceneState.zoom.value);
// Grid
if (renderGrid && appState.showGrid) {
if (renderGrid && appState.gridSize) {
strokeGrid(
context,
-Math.ceil(zoomTranslationX / sceneState.zoom.value / GRID_SIZE) *
GRID_SIZE +
(sceneState.scrollX % GRID_SIZE),
-Math.ceil(zoomTranslationY / sceneState.zoom.value / GRID_SIZE) *
GRID_SIZE +
(sceneState.scrollY % GRID_SIZE),
appState.gridSize,
-Math.ceil(zoomTranslationX / sceneState.zoom.value / appState.gridSize) *
appState.gridSize +
(sceneState.scrollX % appState.gridSize),
-Math.ceil(zoomTranslationY / sceneState.zoom.value / appState.gridSize) *
appState.gridSize +
(sceneState.scrollY % appState.gridSize),
normalizedCanvasWidth / sceneState.zoom.value,
normalizedCanvasHeight / sceneState.zoom.value,
);
@@ -762,20 +763,13 @@ const isVisibleElement = (
) => {
const [x1, y1, x2, y2] = getElementBounds(element); // scene coordinates
const topLeftSceneCoords = viewportCoordsToSceneCoords(
{
clientX: viewTransformations.offsetLeft,
clientY: viewTransformations.offsetTop,
},
{ clientX: 0, clientY: 0 },
viewTransformations,
);
const bottomRightSceneCoords = viewportCoordsToSceneCoords(
{
clientX: viewTransformations.offsetLeft + canvasWidth,
clientY: viewTransformations.offsetTop + canvasHeight,
},
{ clientX: canvasWidth, clientY: canvasHeight },
viewTransformations,
);
return (
topLeftSceneCoords.x <= x2 &&
topLeftSceneCoords.y <= y2 &&
+2 -5
View File
@@ -3,7 +3,6 @@ import { NormalizedZoomValue, PointerCoords, Zoom } from "../types";
export const getNewZoom = (
newZoomValue: NormalizedZoomValue,
prevZoom: Zoom,
canvasOffset: { left: number; top: number },
zoomOnViewportPoint: PointerCoords = { x: 0, y: 0 },
): Zoom => {
return {
@@ -11,13 +10,11 @@ export const getNewZoom = (
translation: {
x:
zoomOnViewportPoint.x -
canvasOffset.left -
(zoomOnViewportPoint.x - canvasOffset.left - prevZoom.translation.x) *
(zoomOnViewportPoint.x - prevZoom.translation.x) *
(newZoomValue / prevZoom.value),
y:
zoomOnViewportPoint.y -
canvasOffset.top -
(zoomOnViewportPoint.y - canvasOffset.top - prevZoom.translation.y) *
(zoomOnViewportPoint.y - prevZoom.translation.y) *
(newZoomValue / prevZoom.value),
},
};
@@ -1,20 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`tryParseSpreadsheet works for numbers with comma in them 1`] = `
Object {
"spreadsheet": Object {
"labels": Array [
"Week 1",
"Week 2",
"Week 3",
],
"title": "Users",
"values": Array [
814,
10301,
4264,
],
},
"type": "VALID_SPREADSHEET",
}
`;
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,13 +0,0 @@
import { tryParseSpreadsheet } from "../charts";
describe("tryParseSpreadsheet", () => {
it("works for numbers with comma in them", () => {
const result = tryParseSpreadsheet(
`Week Index${"\t"}Users
Week 1${"\t"}814
Week 2${"\t"}10,301
Week 3${"\t"}4,264`,
);
expect(result).toMatchSnapshot();
});
});
+8 -2
View File
@@ -81,6 +81,12 @@ export class API {
verticalAlign?: T extends "text"
? ExcalidrawTextElement["verticalAlign"]
: never;
startArrowhead?: T extends "arrow" | "line" | "draw"
? ExcalidrawLinearElement["startArrowhead"]
: never;
endArrowhead?: T extends "arrow" | "line" | "draw"
? ExcalidrawLinearElement["endArrowhead"]
: never;
}): T extends "arrow" | "line" | "draw"
? ExcalidrawLinearElement
: T extends "text"
@@ -130,8 +136,8 @@ export class API {
case "draw":
element = newLinearElement({
type: type as "arrow" | "line" | "draw",
startArrowhead: null,
endArrowhead: null,
startArrowhead: rest.startArrowhead ?? null,
endArrowhead: rest.endArrowhead ?? null,
...base,
});
break;
+203 -61
View File
@@ -1,7 +1,7 @@
import { queryAllByText, queryByText } from "@testing-library/react";
import { queryByText } from "@testing-library/react";
import React from "react";
import ReactDOM from "react-dom";
import { copiedStyles } from "../actions/actionStyles";
import { getDefaultAppState } from "../appState";
import { ShortcutName } from "../actions/shortcuts";
import { ExcalidrawElement } from "../element/types";
import { setLanguage } from "../i18n";
@@ -635,8 +635,8 @@ describe("regression tests", () => {
const contextMenu = document.querySelector(".context-menu");
const expectedShortcutNames: ShortcutName[] = [
"selectAll",
"gridMode",
"stats",
"toggleGridMode",
"toggleStats",
];
expect(contextMenu).not.toBeNull();
@@ -775,82 +775,224 @@ describe("regression tests", () => {
});
});
it("selecting 'Copy styles' in context menu copies styles", () => {
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(20, 20);
it("copy-styles updates appState defaults", () => {
h.app.updateScene({
elements: [
API.createElement({
type: "rectangle",
id: "A",
x: 0,
y: 0,
opacity: 90,
strokeColor: "#FF0000",
strokeStyle: "solid",
strokeWidth: 10,
roughness: 2,
backgroundColor: "#00FF00",
fillStyle: "solid",
strokeSharpness: "sharp",
}),
API.createElement({
type: "arrow",
id: "B",
x: 200,
y: 200,
startArrowhead: "bar",
endArrowhead: "bar",
}),
API.createElement({
type: "text",
id: "C",
x: 200,
y: 200,
fontFamily: 3,
fontSize: 200,
textAlign: "center",
}),
],
});
h.app.setState({
selectedElementIds: { A: true, B: true, C: true },
});
const defaultAppState = getDefaultAppState();
expect(h.state).toEqual(
expect.objectContaining({
currentItemOpacity: defaultAppState.currentItemOpacity,
currentItemStrokeColor: defaultAppState.currentItemStrokeColor,
currentItemStrokeStyle: defaultAppState.currentItemStrokeStyle,
currentItemStrokeWidth: defaultAppState.currentItemStrokeWidth,
currentItemRoughness: defaultAppState.currentItemRoughness,
currentItemBackgroundColor: defaultAppState.currentItemBackgroundColor,
currentItemFillStyle: defaultAppState.currentItemFillStyle,
currentItemStrokeSharpness: defaultAppState.currentItemStrokeSharpness,
currentItemStartArrowhead: defaultAppState.currentItemStartArrowhead,
currentItemEndArrowhead: defaultAppState.currentItemEndArrowhead,
currentItemFontFamily: defaultAppState.currentItemFontFamily,
currentItemFontSize: defaultAppState.currentItemFontSize,
currentItemTextAlign: defaultAppState.currentItemTextAlign,
}),
);
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 1,
clientY: 1,
});
const contextMenu = document.querySelector(".context-menu");
expect(copiedStyles).toBe("{}");
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
expect(copiedStyles).not.toBe("{}");
const element = JSON.parse(copiedStyles);
expect(element).toEqual(API.getSelectedElement());
expect(h.state).toEqual(
expect.objectContaining({
currentItemOpacity: 90,
currentItemStrokeColor: "#FF0000",
currentItemStrokeStyle: "solid",
currentItemStrokeWidth: 10,
currentItemRoughness: 2,
currentItemBackgroundColor: "#00FF00",
currentItemFillStyle: "solid",
currentItemStrokeSharpness: "sharp",
currentItemStartArrowhead: "bar",
currentItemEndArrowhead: "bar",
currentItemFontFamily: 3,
currentItemFontSize: 200,
currentItemTextAlign: "center",
}),
);
});
it("selecting 'Paste styles' in context menu pastes styles", () => {
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(20, 20);
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(20, 20);
// Change some styles of second rectangle
clickLabeledElement("Stroke");
clickLabeledElement("#c92a2a");
clickLabeledElement("Background");
clickLabeledElement("#e64980");
// Fill style
fireEvent.click(screen.getByTitle("Cross-hatch"));
// Stroke width
fireEvent.click(screen.getByTitle("Bold"));
// Stroke style
fireEvent.click(screen.getByTitle("Dotted"));
// Roughness
fireEvent.click(screen.getByTitle("Cartoonist"));
// Opacity
fireEvent.change(screen.getByLabelText("Opacity"), {
target: { value: "60" },
it("paste-styles action", () => {
h.app.updateScene({
elements: [
API.createElement({
type: "rectangle",
id: "A",
x: 0,
y: 0,
opacity: 90,
strokeColor: "#FF0000",
strokeStyle: "solid",
strokeWidth: 10,
roughness: 2,
backgroundColor: "#00FF00",
fillStyle: "solid",
strokeSharpness: "sharp",
}),
API.createElement({
type: "arrow",
id: "B",
x: 0,
y: 0,
startArrowhead: "bar",
endArrowhead: "bar",
}),
API.createElement({
type: "text",
id: "C",
x: 0,
y: 0,
fontFamily: 3,
fontSize: 200,
textAlign: "center",
}),
API.createElement({
type: "rectangle",
id: "D",
x: 200,
y: 200,
}),
API.createElement({
type: "arrow",
id: "E",
x: 200,
y: 200,
}),
API.createElement({
type: "text",
id: "F",
x: 200,
y: 200,
}),
],
});
h.app.setState({
selectedElementIds: { A: true, B: true, C: true },
});
mouse.reset();
// Copy styles of second rectangle
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 40,
clientY: 40,
clientX: 1,
clientY: 1,
});
let contextMenu = document.querySelector(".context-menu");
fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
const secondRect = JSON.parse(copiedStyles);
expect(secondRect.id).toBe(h.elements[1].id);
fireEvent.click(
queryByText(
document.querySelector(".context-menu") as HTMLElement,
"Copy styles",
)!,
);
mouse.reset();
// Paste styles to first rectangle
h.app.setState({
selectedElementIds: { D: true, E: true, F: true },
});
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 10,
clientY: 10,
clientX: 201,
clientY: 201,
});
contextMenu = document.querySelector(".context-menu");
fireEvent.click(queryByText(contextMenu as HTMLElement, "Paste styles")!);
fireEvent.click(
queryByText(
document.querySelector(".context-menu") as HTMLElement,
"Paste styles",
)!,
);
const firstRect = API.getSelectedElement();
expect(firstRect.id).toBe(h.elements[0].id);
expect(firstRect.strokeColor).toBe("#c92a2a");
expect(firstRect.backgroundColor).toBe("#e64980");
expect(firstRect.fillStyle).toBe("cross-hatch");
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
expect(firstRect.strokeStyle).toBe("dotted");
expect(firstRect.roughness).toBe(2); // Cartoonist: 2
expect(firstRect.opacity).toBe(60);
const defaultAppState = getDefaultAppState();
expect(h.elements.find((element) => element.id === "D")).toEqual(
expect.objectContaining({
opacity: 90,
strokeColor: "#FF0000",
strokeStyle: "solid",
strokeWidth: 10,
roughness: 2,
backgroundColor: "#00FF00",
fillStyle: "solid",
strokeSharpness: "sharp",
}),
);
expect(h.elements.find((element) => element.id === "E")).toEqual(
expect.objectContaining({
opacity: defaultAppState.currentItemOpacity,
strokeColor: defaultAppState.currentItemStrokeColor,
strokeStyle: defaultAppState.currentItemStrokeStyle,
strokeWidth: defaultAppState.currentItemStrokeWidth,
roughness: defaultAppState.currentItemRoughness,
backgroundColor: "#00FF00",
fillStyle: "solid",
strokeSharpness: "sharp",
startArrowhead: "bar",
endArrowhead: "bar",
}),
);
expect(h.elements.find((element) => element.id === "F")).toEqual(
expect.objectContaining({
opacity: defaultAppState.currentItemOpacity,
strokeColor: defaultAppState.currentItemStrokeColor,
strokeStyle: defaultAppState.currentItemStrokeStyle,
strokeWidth: 10,
roughness: 2,
backgroundColor: "#00FF00",
fillStyle: "solid",
strokeSharpness: "sharp",
fontFamily: 3,
fontSize: 200,
textAlign: "center",
}),
);
});
it("selecting 'Delete' in context menu deletes element", () => {
@@ -864,7 +1006,7 @@ describe("regression tests", () => {
clientY: 1,
});
const contextMenu = document.querySelector(".context-menu");
fireEvent.click(queryAllByText(contextMenu as HTMLElement, "Delete")[0]);
fireEvent.click(queryByText(contextMenu as HTMLElement, "Delete")!);
expect(API.getSelectedElements()).toHaveLength(0);
expect(h.elements[0].isDeleted).toBe(true);
});
-48
View File
@@ -1,48 +0,0 @@
import React from "react";
import { render, waitFor } from "./test-utils";
import Excalidraw from "../packages/excalidraw/index";
import { API } from "./helpers/api";
const { h } = window;
describe("appState", () => {
it("scroll-to-center on init works with non-zero offsets", async () => {
const WIDTH = 600;
const HEIGHT = 700;
const OFFSET_LEFT = 200;
const OFFSET_TOP = 100;
const ELEM_WIDTH = 100;
const ELEM_HEIGHT = 60;
await render(
<Excalidraw
width={WIDTH}
height={HEIGHT}
offsetLeft={OFFSET_LEFT}
offsetTop={OFFSET_TOP}
initialData={{
elements: [
API.createElement({
type: "rectangle",
id: "A",
width: ELEM_WIDTH,
height: ELEM_HEIGHT,
}),
],
}}
/>,
);
await waitFor(() => {
expect(h.state.width).toBe(WIDTH);
expect(h.state.height).toBe(HEIGHT);
expect(h.state.offsetLeft).toBe(OFFSET_LEFT);
expect(h.state.offsetTop).toBe(OFFSET_TOP);
// assert scroll is in center
expect(h.state.scrollX).toBe(WIDTH / 2 - ELEM_WIDTH / 2);
expect(h.state.scrollY).toBe(HEIGHT / 2 - ELEM_HEIGHT / 2);
});
});
});
+2 -7
View File
@@ -55,7 +55,7 @@ export type AppState = {
currentItemFillStyle: ExcalidrawElement["fillStyle"];
currentItemStrokeWidth: number;
currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
currentItemRoughness: number;
currentItemRoughness: ExcalidrawElement["roughness"];
currentItemOpacity: number;
currentItemFontFamily: FontFamily;
currentItemFontSize: number;
@@ -83,7 +83,7 @@ export type AppState = {
showShortcutsDialog: boolean;
zenModeEnabled: boolean;
appearance: "light" | "dark";
showGrid: boolean;
gridSize: number | null;
/** top-most selected groups (i.e. does not include nested groups) */
selectedGroupIds: { [groupId: string]: boolean };
@@ -166,11 +166,6 @@ export interface ExcalidrawProps {
button: "down" | "up";
pointersMap: Gesture["pointers"];
}) => void;
onExportToBackend?: (
exportedElements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
canvas: HTMLCanvasElement | null,
) => void;
}
export type SceneData = {

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