Merge remote-tracking branch 'origin/master' into feat-custom-actions

This commit is contained in:
Daniel J. Geiger 2023-01-26 17:54:32 -06:00
commit e6fb7e3016
94 changed files with 1231 additions and 641 deletions

View File

@ -167,9 +167,6 @@
body, body,
html { html {
margin: 0; margin: 0;
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system,
Segoe UI, Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font);
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
width: 100%; width: 100%;

View File

@ -1,7 +1,7 @@
import { ColorPicker } from "../components/ColorPicker"; import { ColorPicker } from "../components/ColorPicker";
import { eraser, ZoomInIcon, ZoomOutIcon } from "../components/icons"; import { ZoomInIcon, ZoomOutIcon } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { MIN_ZOOM, THEME, ZOOM_STEP } from "../constants"; import { CURSOR_TYPE, MIN_ZOOM, THEME, ZOOM_STEP } from "../constants";
import { getCommonBounds, getNonDeletedElements } from "../element"; import { getCommonBounds, getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
@ -10,12 +10,15 @@ import { getNormalizedZoom, getSelectedElements } from "../scene";
import { centerScrollOn } from "../scene/scroll"; import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom"; import { getStateForZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types"; import { AppState, NormalizedZoomValue } from "../types";
import { getShortcutKey, updateActiveTool } from "../utils"; import { getShortcutKey, setCursor, updateActiveTool } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { Tooltip } from "../components/Tooltip"; import { Tooltip } from "../components/Tooltip";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { getDefaultAppState, isEraserActive } from "../appState"; import {
import clsx from "clsx"; getDefaultAppState,
isEraserActive,
isHandToolActive,
} from "../appState";
export const actionChangeViewBackgroundColor = register({ export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor", name: "changeViewBackgroundColor",
@ -306,15 +309,15 @@ export const actionToggleTheme = register({
}, },
}); });
export const actionErase = register({ export const actionToggleEraserTool = register({
name: "eraser", name: "toggleEraserTool",
trackEvent: { category: "toolbar" }, trackEvent: { category: "toolbar" },
perform: (elements, appState) => { perform: (elements, appState) => {
let activeTool: AppState["activeTool"]; let activeTool: AppState["activeTool"];
if (isEraserActive(appState)) { if (isEraserActive(appState)) {
activeTool = updateActiveTool(appState, { activeTool = updateActiveTool(appState, {
...(appState.activeTool.lastActiveToolBeforeEraser || { ...(appState.activeTool.lastActiveTool || {
type: "selection", type: "selection",
}), }),
lastActiveToolBeforeEraser: null, lastActiveToolBeforeEraser: null,
@ -337,17 +340,38 @@ export const actionErase = register({
}; };
}, },
keyTest: (event) => event.key === KEYS.E, keyTest: (event) => event.key === KEYS.E,
PanelComponent: ({ elements, appState, updateData, data }) => ( });
<ToolButton
type="button" export const actionToggleHandTool = register({
icon={eraser} name: "toggleHandTool",
className={clsx("eraser", { active: isEraserActive(appState) })} trackEvent: { category: "toolbar" },
title={`${t("toolBar.eraser")}-${getShortcutKey("E")}`} perform: (elements, appState, _, app) => {
aria-label={t("toolBar.eraser")} let activeTool: AppState["activeTool"];
onClick={() => {
updateData(null); if (isHandToolActive(appState)) {
}} activeTool = updateActiveTool(appState, {
size={data?.size || "medium"} ...(appState.activeTool.lastActiveTool || {
></ToolButton> type: "selection",
), }),
lastActiveToolBeforeEraser: null,
});
} else {
activeTool = updateActiveTool(appState, {
type: "hand",
lastActiveToolBeforeEraser: appState.activeTool,
});
setCursor(app.canvas, CURSOR_TYPE.GRAB);
}
return {
appState: {
...appState,
selectedElementIds: {},
selectedGroupIds: {},
activeTool,
},
commitToHistory: true,
};
},
keyTest: (event) => event.key === KEYS.H,
}); });

View File

@ -145,7 +145,7 @@ export const actionFinalize = register({
let activeTool: AppState["activeTool"]; let activeTool: AppState["activeTool"];
if (appState.activeTool.type === "eraser") { if (appState.activeTool.type === "eraser") {
activeTool = updateActiveTool(appState, { activeTool = updateActiveTool(appState, {
...(appState.activeTool.lastActiveToolBeforeEraser || { ...(appState.activeTool.lastActiveTool || {
type: "selection", type: "selection",
}), }),
lastActiveToolBeforeEraser: null, lastActiveToolBeforeEraser: null,

View File

@ -5,10 +5,11 @@ import { t } from "../i18n";
import History, { HistoryEntry } from "../history"; import History, { HistoryEntry } from "../history";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types"; import { AppState } from "../types";
import { isWindows, KEYS } from "../keys"; import { KEYS } from "../keys";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { fixBindingsAfterDeletion } from "../element/binding"; import { fixBindingsAfterDeletion } from "../element/binding";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import { isWindows } from "../constants";
const writeData = ( const writeData = (
prevElements: readonly ExcalidrawElement[], prevElements: readonly ExcalidrawElement[],

View File

@ -5,7 +5,7 @@ import {
moveAllLeft, moveAllLeft,
moveAllRight, moveAllRight,
} from "../zindex"; } from "../zindex";
import { KEYS, isDarwin, CODES } from "../keys"; import { KEYS, CODES } from "../keys";
import { t } from "../i18n"; import { t } from "../i18n";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
@ -15,6 +15,7 @@ import {
SendBackwardIcon, SendBackwardIcon,
SendToBackIcon, SendToBackIcon,
} from "../components/icons"; } from "../components/icons";
import { isDarwin } from "../constants";
export const actionSendBackward = register({ export const actionSendBackward = register({
name: "sendBackward", name: "sendBackward",

View File

@ -1,5 +1,5 @@
import { isDarwin } from "../constants";
import { t } from "../i18n"; import { t } from "../i18n";
import { isDarwin } from "../keys";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { ActionName } from "./types"; import { ActionName } from "./types";

View File

@ -125,10 +125,11 @@ const actionNames = [
"decreaseFontSize", "decreaseFontSize",
"unbindText", "unbindText",
"hyperlink", "hyperlink",
"eraser",
"bindText", "bindText",
"toggleLock", "toggleLock",
"toggleLinearEditor", "toggleLinearEditor",
"toggleEraserTool",
"toggleHandTool",
] as const; ] as const;
// So we can have the `isActionName` type guard // So we can have the `isActionName` type guard

View File

@ -45,7 +45,7 @@ export const getDefaultAppState = (): Omit<
type: "selection", type: "selection",
customType: null, customType: null,
locked: false, locked: false,
lastActiveToolBeforeEraser: null, lastActiveTool: null,
}, },
penMode: false, penMode: false,
penDetected: false, penDetected: false,
@ -228,3 +228,11 @@ export const isEraserActive = ({
}: { }: {
activeTool: AppState["activeTool"]; activeTool: AppState["activeTool"];
}) => activeTool.type === "eraser"; }) => activeTool.type === "eraser";
export const isHandToolActive = ({
activeTool,
}: {
activeTool: AppState["activeTool"];
}) => {
return activeTool.type === "hand";
};

View File

@ -180,16 +180,16 @@ export const parseClipboard = async (
}; };
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => { export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
let promise;
try { try {
// in Safari so far we need to construct the ClipboardItem synchronously // in Safari so far we need to construct the ClipboardItem synchronously
// (i.e. in the same tick) otherwise browser will complain for lack of // (i.e. in the same tick) otherwise browser will complain for lack of
// user intent. Using a Promise ClipboardItem constructor solves this. // user intent. Using a Promise ClipboardItem constructor solves this.
// https://bugs.webkit.org/show_bug.cgi?id=222262 // https://bugs.webkit.org/show_bug.cgi?id=222262
// //
// not await so that we can detect whether the thrown error likely relates // Note that Firefox (and potentially others) seems to support Promise
// to a lack of support for the Promise ClipboardItem constructor // ClipboardItem constructor, but throws on an unrelated MIME type error.
promise = navigator.clipboard.write([ // So we need to await this and fallback to awaiting the blob if applicable.
await navigator.clipboard.write([
new window.ClipboardItem({ new window.ClipboardItem({
[MIME_TYPES.png]: blob, [MIME_TYPES.png]: blob,
}), }),
@ -207,7 +207,6 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
throw error; throw error;
} }
} }
await promise;
}; };
export const copyTextToSystemClipboard = async (text: string | null) => { export const copyTextToSystemClipboard = async (text: string | null) => {

View File

@ -224,9 +224,10 @@ export const ShapesSwitcher = ({
<> <>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => { {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
const label = t(`toolBar.${value}`); const label = t(`toolBar.${value}`);
const letter = key && (typeof key === "string" ? key : key[0]); const letter =
key && capitalizeString(typeof key === "string" ? key : key[0]);
const shortcut = letter const shortcut = letter
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${numericKey}` ? `${letter} ${t("helpDialog.or")} ${numericKey}`
: `${numericKey}`; : `${numericKey}`;
return ( return (
<ToolButton <ToolButton
@ -237,7 +238,7 @@ export const ShapesSwitcher = ({
checked={activeTool.type === value} checked={activeTool.type === value}
name="editor-current-shape" name="editor-current-shape"
title={`${capitalizeString(label)}${shortcut}`} title={`${capitalizeString(label)}${shortcut}`}
keyBindingLabel={numericKey} keyBindingLabel={numericKey || letter}
aria-label={capitalizeString(label)} aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut} aria-keyshortcuts={shortcut}
data-testid={`toolbar-${value}`} data-testid={`toolbar-${value}`}

View File

@ -0,0 +1,35 @@
import { atom, useAtom } from "jotai";
import { actionClearCanvas } from "../actions";
import { t } from "../i18n";
import { useExcalidrawActionManager } from "./App";
import ConfirmDialog from "./ConfirmDialog";
export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
export const ActiveConfirmDialog = () => {
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
activeConfirmDialogAtom,
);
const actionManager = useExcalidrawActionManager();
if (!activeConfirmDialog) {
return null;
}
if (activeConfirmDialog === "clearCanvas") {
return (
<ConfirmDialog
onConfirm={() => {
actionManager.executeAction(actionClearCanvas);
setActiveConfirmDialog(null);
}}
onCancel={() => setActiveConfirmDialog(null)}
title={t("clearCanvasDialog.title")}
>
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
</ConfirmDialog>
);
}
return null;
};

View File

@ -41,7 +41,11 @@ import { ActionManager } from "../actions/manager";
import { getActions } from "../actions/register"; import { getActions } from "../actions/register";
import { ActionResult } from "../actions/types"; import { ActionResult } from "../actions/types";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { getDefaultAppState, isEraserActive } from "../appState"; import {
getDefaultAppState,
isEraserActive,
isHandToolActive,
} from "../appState";
import { parseClipboard } from "../clipboard"; import { parseClipboard } from "../clipboard";
import { import {
APP_NAME, APP_NAME,
@ -57,6 +61,7 @@ import {
EVENT, EVENT,
GRID_SIZE, GRID_SIZE,
IMAGE_RENDER_TIMEOUT, IMAGE_RENDER_TIMEOUT,
isAndroid,
LINE_CONFIRM_THRESHOLD, LINE_CONFIRM_THRESHOLD,
MAX_ALLOWED_FILE_BYTES, MAX_ALLOWED_FILE_BYTES,
MIME_TYPES, MIME_TYPES,
@ -166,7 +171,6 @@ import {
shouldRotateWithDiscreteAngle, shouldRotateWithDiscreteAngle,
isArrowKey, isArrowKey,
KEYS, KEYS,
isAndroid,
} from "../keys"; } from "../keys";
import { distance2d, getGridPoint, isPathALoop } from "../math"; import { distance2d, getGridPoint, isPathALoop } from "../math";
import { renderScene } from "../renderer/renderScene"; import { renderScene } from "../renderer/renderScene";
@ -274,6 +278,7 @@ import {
import { shouldShowBoundingBox } from "../element/transformHandles"; import { shouldShowBoundingBox } from "../element/transformHandles";
import { Fonts } from "../scene/Fonts"; import { Fonts } from "../scene/Fonts";
import { actionPaste } from "../actions/actionClipboard"; import { actionPaste } from "../actions/actionClipboard";
import { actionToggleHandTool } from "../actions/actionCanvas";
const deviceContextInitialValue = { const deviceContextInitialValue = {
isSmScreen: false, isSmScreen: false,
@ -577,6 +582,7 @@ class App extends React.Component<AppProps, AppState> {
elements={this.scene.getNonDeletedElements()} elements={this.scene.getNonDeletedElements()}
onLockToggle={this.toggleLock} onLockToggle={this.toggleLock}
onPenModeToggle={this.togglePenMode} onPenModeToggle={this.togglePenMode}
onHandToolToggle={this.onHandToolToggle}
onInsertElements={(elements) => onInsertElements={(elements) =>
this.addElementsFromPasteOrLibrary({ this.addElementsFromPasteOrLibrary({
elements, elements,
@ -1815,6 +1821,10 @@ class App extends React.Component<AppProps, AppState> {
}); });
}; };
onHandToolToggle = () => {
this.actionManager.executeAction(actionToggleHandTool);
};
scrollToContent = ( scrollToContent = (
target: target:
| ExcalidrawElement | ExcalidrawElement
@ -2232,11 +2242,13 @@ class App extends React.Component<AppProps, AppState> {
private setActiveTool = ( private setActiveTool = (
tool: tool:
| { type: typeof SHAPES[number]["value"] | "eraser" } | { type: typeof SHAPES[number]["value"] | "eraser" | "hand" }
| { type: "custom"; customType: string }, | { type: "custom"; customType: string },
) => { ) => {
const nextActiveTool = updateActiveTool(this.state, tool); const nextActiveTool = updateActiveTool(this.state, tool);
if (!isHoldingSpace) { if (nextActiveTool.type === "hand") {
setCursor(this.canvas, CURSOR_TYPE.GRAB);
} else if (!isHoldingSpace) {
setCursorForShape(this.canvas, this.state); setCursorForShape(this.canvas, this.state);
} }
if (isToolIcon(document.activeElement)) { if (isToolIcon(document.activeElement)) {
@ -2907,7 +2919,12 @@ class App extends React.Component<AppProps, AppState> {
null; null;
} }
if (isHoldingSpace || isPanning || isDraggingScrollBar) { if (
isHoldingSpace ||
isPanning ||
isDraggingScrollBar ||
isHandToolActive(this.state)
) {
return; return;
} }
@ -3499,7 +3516,10 @@ class App extends React.Component<AppProps, AppState> {
); );
} else if (this.state.activeTool.type === "custom") { } else if (this.state.activeTool.type === "custom") {
setCursor(this.canvas, CURSOR_TYPE.AUTO); setCursor(this.canvas, CURSOR_TYPE.AUTO);
} else if (this.state.activeTool.type !== "eraser") { } else if (
this.state.activeTool.type !== "eraser" &&
this.state.activeTool.type !== "hand"
) {
this.createGenericElementOnPointerDown( this.createGenericElementOnPointerDown(
this.state.activeTool.type, this.state.activeTool.type,
pointerDownState, pointerDownState,
@ -3610,6 +3630,7 @@ class App extends React.Component<AppProps, AppState> {
gesture.pointers.size <= 1 && gesture.pointers.size <= 1 &&
(event.button === POINTER_BUTTON.WHEEL || (event.button === POINTER_BUTTON.WHEEL ||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) || (event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
isHandToolActive(this.state) ||
this.state.viewModeEnabled) this.state.viewModeEnabled)
) || ) ||
isTextElement(this.state.editingElement) isTextElement(this.state.editingElement)

View File

@ -96,6 +96,10 @@
width: 5rem; width: 5rem;
height: 5rem; height: 5rem;
margin: 0 0.2em; margin: 0 0.2em;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 1rem; border-radius: 1rem;
background-color: var(--button-color); background-color: var(--button-color);

View File

@ -0,0 +1,32 @@
import "./ToolIcon.scss";
import clsx from "clsx";
import { ToolButton } from "./ToolButton";
import { handIcon } from "./icons";
import { KEYS } from "../keys";
type LockIconProps = {
title?: string;
name?: string;
checked: boolean;
onChange?(): void;
isMobile?: boolean;
};
export const HandButton = (props: LockIconProps) => {
return (
<ToolButton
className={clsx("Shape", { fillable: false })}
type="radio"
icon={handIcon}
name="editor-current-shape"
checked={props.checked}
title={`${props.title} — H`}
keyBindingLabel={!props.isMobile ? KEYS.H.toLocaleUpperCase() : undefined}
aria-label={`${props.title} — H`}
aria-keyshortcuts={KEYS.H}
data-testid={`toolbar-hand`}
onChange={() => props.onChange?.()}
/>
);
};

View File

@ -1,10 +1,12 @@
import React from "react"; import React from "react";
import { t } from "../i18n"; import { t } from "../i18n";
import { isDarwin, isWindows, KEYS } from "../keys"; import { KEYS } from "../keys";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import "./HelpDialog.scss"; import "./HelpDialog.scss";
import { ExternalLinkIcon } from "./icons"; import { ExternalLinkIcon } from "./icons";
import { probablySupportsClipboardBlob } from "../clipboard";
import { isDarwin, isFirefox, isWindows } from "../constants";
const Header = () => ( const Header = () => (
<div className="HelpDialog__header"> <div className="HelpDialog__header">
@ -67,6 +69,10 @@ function* intersperse(as: JSX.Element[][], delim: string | null) {
} }
} }
const upperCaseSingleChars = (str: string) => {
return str.replace(/\b[a-z]\b/, (c) => c.toUpperCase());
};
const Shortcut = ({ const Shortcut = ({
label, label,
shortcuts, shortcuts,
@ -81,7 +87,9 @@ const Shortcut = ({
? [...shortcut.slice(0, -2).split("+"), "+"] ? [...shortcut.slice(0, -2).split("+"), "+"]
: shortcut.split("+"); : shortcut.split("+");
return keys.map((key) => <ShortcutKey key={key}>{key}</ShortcutKey>); return keys.map((key) => (
<ShortcutKey key={key}>{upperCaseSingleChars(key)}</ShortcutKey>
));
}); });
return ( return (
@ -118,6 +126,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
className="HelpDialog__island--tools" className="HelpDialog__island--tools"
caption={t("helpDialog.tools")} caption={t("helpDialog.tools")}
> >
<Shortcut label={t("toolBar.hand")} shortcuts={[KEYS.H]} />
<Shortcut <Shortcut
label={t("toolBar.selection")} label={t("toolBar.selection")}
shortcuts={[KEYS.V, KEYS["1"]]} shortcuts={[KEYS.V, KEYS["1"]]}
@ -304,10 +313,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.pasteAsPlaintext")} label={t("labels.pasteAsPlaintext")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]} shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
/> />
{/* firefox supports clipboard API under a flag, so we'll
show users what they can do in the error message */}
{(probablySupportsClipboardBlob || isFirefox) && (
<Shortcut <Shortcut
label={t("labels.copyAsPng")} label={t("labels.copyAsPng")}
shortcuts={[getShortcutKey("Shift+Alt+C")]} shortcuts={[getShortcutKey("Shift+Alt+C")]}
/> />
)}
<Shortcut <Shortcut
label={t("labels.copyStyles")} label={t("labels.copyStyles")}
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]} shortcuts={[getShortcutKey("CtrlOrCmd+Alt+C")]}

View File

@ -12,7 +12,7 @@ import Stack from "./Stack";
import "./ExportDialog.scss"; import "./ExportDialog.scss";
import OpenColor from "open-color"; import OpenColor from "open-color";
import { CheckboxItem } from "./CheckboxItem"; import { CheckboxItem } from "./CheckboxItem";
import { DEFAULT_EXPORT_PADDING } from "../constants"; import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
import { nativeFileSystemSupported } from "../data/filesystem"; import { nativeFileSystemSupported } from "../data/filesystem";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
@ -190,7 +190,9 @@ const ImageExportModal = ({
> >
SVG SVG
</ExportButton> </ExportButton>
{probablySupportsClipboardBlob && ( {/* firefox supports clipboard API under a flag,
so let's throw and tell people what they can do */}
{(probablySupportsClipboardBlob || isFirefox) && (
<ExportButton <ExportButton
title={t("buttons.copyPngToClipboard")} title={t("buttons.copyPngToClipboard")}
onClick={() => onExportToClipboard(exportedElements)} onClick={() => onExportToClipboard(exportedElements)}

View File

@ -50,6 +50,9 @@ import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import MainMenu from "./main-menu/MainMenu"; import MainMenu from "./main-menu/MainMenu";
import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
@ -59,6 +62,7 @@ interface LayerUIProps {
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
onLockToggle: () => void; onLockToggle: () => void;
onHandToolToggle: () => void;
onPenModeToggle: () => void; onPenModeToggle: () => void;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
showExitZenModeBtn: boolean; showExitZenModeBtn: boolean;
@ -86,6 +90,7 @@ const LayerUI = ({
elements, elements,
canvas, canvas,
onLockToggle, onLockToggle,
onHandToolToggle,
onPenModeToggle, onPenModeToggle,
onInsertElements, onInsertElements,
showExitZenModeBtn, showExitZenModeBtn,
@ -306,13 +311,20 @@ const LayerUI = ({
penDetected={appState.penDetected} penDetected={appState.penDetected}
/> />
<LockButton <LockButton
zenModeEnabled={appState.zenModeEnabled}
checked={appState.activeTool.locked} checked={appState.activeTool.locked}
onChange={() => onLockToggle()} onChange={onLockToggle}
title={t("toolBar.lock")} title={t("toolBar.lock")}
/> />
<div className="App-toolbar__divider"></div> <div className="App-toolbar__divider"></div>
<HandButton
checked={isHandToolActive(appState)}
onChange={() => onHandToolToggle()}
title={t("toolBar.hand")}
isMobile
/>
<ShapesSwitcher <ShapesSwitcher
appState={appState} appState={appState}
canvas={canvas} canvas={canvas}
@ -325,9 +337,6 @@ const LayerUI = ({
}} }}
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
/> />
{/* {actionManager.renderAction("eraser", {
// size: "small",
})} */}
</Stack.Row> </Stack.Row>
</Island> </Island>
</Stack.Row> </Stack.Row>
@ -389,6 +398,7 @@ const LayerUI = ({
}} }}
/> />
)} )}
<ActiveConfirmDialog />
{renderImageExportDialog()} {renderImageExportDialog()}
{renderJSONExportDialog()} {renderJSONExportDialog()}
{appState.pasteDialog.shown && ( {appState.pasteDialog.shown && (
@ -411,7 +421,8 @@ const LayerUI = ({
renderJSONExportDialog={renderJSONExportDialog} renderJSONExportDialog={renderJSONExportDialog}
renderImageExportDialog={renderImageExportDialog} renderImageExportDialog={renderImageExportDialog}
setAppState={setAppState} setAppState={setAppState}
onLockToggle={() => onLockToggle()} onLockToggle={onLockToggle}
onHandToolToggle={onHandToolToggle}
onPenModeToggle={onPenModeToggle} onPenModeToggle={onPenModeToggle}
canvas={canvas} canvas={canvas}
onImageAction={onImageAction} onImageAction={onImageAction}

View File

@ -187,6 +187,7 @@ export const LibraryMenuHeader: React.FC<{
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content <DropdownMenu.Content
onClickOutside={() => setIsLibraryMenuOpen(false)} onClickOutside={() => setIsLibraryMenuOpen(false)}
onSelect={() => setIsLibraryMenuOpen(false)}
className="library-menu" className="library-menu"
> >
{!itemsSelected && ( {!itemsSelected && (

View File

@ -9,7 +9,6 @@ type LockIconProps = {
name?: string; name?: string;
checked: boolean; checked: boolean;
onChange?(): void; onChange?(): void;
zenModeEnabled?: boolean;
isMobile?: boolean; isMobile?: boolean;
}; };

View File

@ -22,6 +22,8 @@ import { LibraryButton } from "./LibraryButton";
import { PenModeButton } from "./PenModeButton"; import { PenModeButton } from "./PenModeButton";
import { Stats } from "./Stats"; import { Stats } from "./Stats";
import { actionToggleStats } from "../actions"; import { actionToggleStats } from "../actions";
import { HandButton } from "./HandButton";
import { isHandToolActive } from "../appState";
type MobileMenuProps = { type MobileMenuProps = {
appState: AppState; appState: AppState;
@ -31,6 +33,7 @@ type MobileMenuProps = {
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
onLockToggle: () => void; onLockToggle: () => void;
onHandToolToggle: () => void;
onPenModeToggle: () => void; onPenModeToggle: () => void;
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
@ -53,6 +56,7 @@ export const MobileMenu = ({
actionManager, actionManager,
setAppState, setAppState,
onLockToggle, onLockToggle,
onHandToolToggle,
onPenModeToggle, onPenModeToggle,
canvas, canvas,
onImageAction, onImageAction,
@ -91,6 +95,13 @@ export const MobileMenu = ({
</Island> </Island>
{renderTopRightUI && renderTopRightUI(true, appState)} {renderTopRightUI && renderTopRightUI(true, appState)}
<div className="mobile-misc-tools-container"> <div className="mobile-misc-tools-container">
{!appState.viewModeEnabled && (
<LibraryButton
appState={appState}
setAppState={setAppState}
isMobile
/>
)}
<PenModeButton <PenModeButton
checked={appState.penMode} checked={appState.penMode}
onChange={onPenModeToggle} onChange={onPenModeToggle}
@ -104,13 +115,12 @@ export const MobileMenu = ({
title={t("toolBar.lock")} title={t("toolBar.lock")}
isMobile isMobile
/> />
{!appState.viewModeEnabled && ( <HandButton
<LibraryButton checked={isHandToolActive(appState)}
appState={appState} onChange={() => onHandToolToggle()}
setAppState={setAppState} title={t("toolBar.hand")}
isMobile isMobile
/> />
)}
</div> </div>
</Stack.Row> </Stack.Row>
</Stack.Col> </Stack.Col>

View File

@ -19,7 +19,7 @@ type ToolButtonBaseProps = {
name?: string; name?: string;
id?: string; id?: string;
size?: ToolButtonSize; size?: ToolButtonSize;
keyBindingLabel?: string; keyBindingLabel?: string | null;
showAriaLabel?: boolean; showAriaLabel?: boolean;
hidden?: boolean; hidden?: boolean;
visible?: boolean; visible?: boolean;

View File

@ -4,16 +4,23 @@ import { Island } from "../Island";
import { useDevice } from "../App"; import { useDevice } from "../App";
import clsx from "clsx"; import clsx from "clsx";
import Stack from "../Stack"; import Stack from "../Stack";
import React from "react";
import { DropdownMenuContentPropsContext } from "./common";
const MenuContent = ({ const MenuContent = ({
children, children,
onClickOutside, onClickOutside,
className = "", className = "",
onSelect,
style, style,
}: { }: {
children?: React.ReactNode; children?: React.ReactNode;
onClickOutside?: () => void; onClickOutside?: () => void;
className?: string; className?: string;
/**
* Called when any menu item is selected (clicked on).
*/
onSelect?: (event: Event) => void;
style?: React.CSSProperties; style?: React.CSSProperties;
}) => { }) => {
const device = useDevice(); const device = useDevice();
@ -24,7 +31,9 @@ const MenuContent = ({
const classNames = clsx(`dropdown-menu ${className}`, { const classNames = clsx(`dropdown-menu ${className}`, {
"dropdown-menu--mobile": device.isMobile, "dropdown-menu--mobile": device.isMobile,
}).trim(); }).trim();
return ( return (
<DropdownMenuContentPropsContext.Provider value={{ onSelect }}>
<div <div
ref={menuRef} ref={menuRef}
className={classNames} className={classNames}
@ -45,7 +54,9 @@ const MenuContent = ({
</Island> </Island>
)} )}
</div> </div>
</DropdownMenuContentPropsContext.Provider>
); );
}; };
export default MenuContent;
MenuContent.displayName = "DropdownMenuContent"; MenuContent.displayName = "DropdownMenuContent";
export default MenuContent;

View File

@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import {
getDrodownMenuItemClassName,
useHandleDropdownMenuItemClick,
} from "./common";
import MenuItemContent from "./DropdownMenuItemContent"; import MenuItemContent from "./DropdownMenuItemContent";
export const getDrodownMenuItemClassName = (className = "") => {
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
};
const DropdownMenuItem = ({ const DropdownMenuItem = ({
icon, icon,
onSelect, onSelect,
@ -14,15 +14,17 @@ const DropdownMenuItem = ({
...rest ...rest
}: { }: {
icon?: JSX.Element; icon?: JSX.Element;
onSelect: () => void; onSelect: (event: Event) => void;
children: React.ReactNode; children: React.ReactNode;
shortcut?: string; shortcut?: string;
className?: string; className?: string;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => { } & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
return ( return (
<button <button
{...rest} {...rest}
onClick={onSelect} onClick={handleClick}
type="button" type="button"
className={getDrodownMenuItemClassName(className)} className={getDrodownMenuItemClassName(className)}
title={rest.title ?? rest["aria-label"]} title={rest.title ?? rest["aria-label"]}

View File

@ -1,20 +1,28 @@
import MenuItemContent from "./DropdownMenuItemContent"; import MenuItemContent from "./DropdownMenuItemContent";
import React from "react"; import React from "react";
import { getDrodownMenuItemClassName } from "./DropdownMenuItem"; import {
getDrodownMenuItemClassName,
useHandleDropdownMenuItemClick,
} from "./common";
const DropdownMenuItemLink = ({ const DropdownMenuItemLink = ({
icon, icon,
shortcut, shortcut,
href, href,
children, children,
onSelect,
className = "", className = "",
...rest ...rest
}: { }: {
href: string;
icon?: JSX.Element; icon?: JSX.Element;
children: React.ReactNode; children: React.ReactNode;
shortcut?: string; shortcut?: string;
className?: string; className?: string;
href: string; onSelect?: (event: Event) => void;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => { } & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
return ( return (
<a <a
{...rest} {...rest}
@ -23,6 +31,7 @@ const DropdownMenuItemLink = ({
rel="noreferrer" rel="noreferrer"
className={getDrodownMenuItemClassName(className)} className={getDrodownMenuItemClassName(className)}
title={rest.title ?? rest["aria-label"]} title={rest.title ?? rest["aria-label"]}
onClick={handleClick}
> >
<MenuItemContent icon={icon} shortcut={shortcut}> <MenuItemContent icon={icon} shortcut={shortcut}>
{children} {children}

View File

@ -0,0 +1,31 @@
import React, { useContext } from "react";
import { EVENT } from "../../constants";
import { composeEventHandlers } from "../../utils";
export const DropdownMenuContentPropsContext = React.createContext<{
onSelect?: (event: Event) => void;
}>({});
export const getDrodownMenuItemClassName = (className = "") => {
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
};
export const useHandleDropdownMenuItemClick = (
origOnClick:
| React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>
| undefined,
onSelect: ((event: Event) => void) | undefined,
) => {
const DropdownMenuContentProps = useContext(DropdownMenuContentPropsContext);
return composeEventHandlers(origOnClick, (event) => {
const itemSelectEvent = new CustomEvent(EVENT.MENU_ITEM_SELECT, {
bubbles: true,
cancelable: true,
});
onSelect?.(itemSelectEvent);
if (!itemSelectEvent.defaultPrevented) {
DropdownMenuContentProps.onSelect?.(itemSelectEvent);
}
});
};

View File

@ -1532,3 +1532,14 @@ export const publishIcon = createIcon(
export const eraser = createIcon( export const eraser = createIcon(
<path d="M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z" />, <path d="M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z" />,
); );
export const handIcon = createIcon(
<g strokeWidth={1.25}>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 13v-7.5a1.5 1.5 0 0 1 3 0v6.5"></path>
<path d="M11 5.5v-2a1.5 1.5 0 1 1 3 0v8.5"></path>
<path d="M14 5.5a1.5 1.5 0 0 1 3 0v6.5"></path>
<path d="M17 7.5a1.5 1.5 0 0 1 3 0v8.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47"></path>
</g>,
tablerIconProps,
);

View File

@ -28,9 +28,9 @@ import {
} from "../../actions"; } from "../../actions";
import "./DefaultItems.scss"; import "./DefaultItems.scss";
import { useState } from "react";
import ConfirmDialog from "../ConfirmDialog";
import clsx from "clsx"; import clsx from "clsx";
import { useSetAtom } from "jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
export const LoadScene = () => { export const LoadScene = () => {
// FIXME Hack until we tie "t" to lang state // FIXME Hack until we tie "t" to lang state
@ -122,41 +122,22 @@ export const ClearCanvas = () => {
// FIXME Hack until we tie "t" to lang state // FIXME Hack until we tie "t" to lang state
// eslint-disable-next-line // eslint-disable-next-line
const appState = useExcalidrawAppState(); const appState = useExcalidrawAppState();
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
const actionManager = useExcalidrawActionManager(); const actionManager = useExcalidrawActionManager();
const [showDialog, setShowDialog] = useState(false);
const toggleDialog = () => setShowDialog(!showDialog);
if (!actionManager.isActionEnabled(actionClearCanvas)) { if (!actionManager.isActionEnabled(actionClearCanvas)) {
return null; return null;
} }
return ( return (
<>
<DropdownMenuItem <DropdownMenuItem
icon={TrashIcon} icon={TrashIcon}
onSelect={toggleDialog} onSelect={() => setActiveConfirmDialog("clearCanvas")}
data-testid="clear-canvas-button" data-testid="clear-canvas-button"
aria-label={t("buttons.clearReset")} aria-label={t("buttons.clearReset")}
> >
{t("buttons.clearReset")} {t("buttons.clearReset")}
</DropdownMenuItem> </DropdownMenuItem>
{/* FIXME this should live outside MainMenu so it stays open
if menu is closed */}
{showDialog && (
<ConfirmDialog
onConfirm={() => {
actionManager.executeAction(actionClearCanvas);
toggleDialog();
}}
onCancel={toggleDialog}
title={t("clearCanvasDialog.title")}
>
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
</ConfirmDialog>
)}
</>
); );
}; };
ClearCanvas.displayName = "ClearCanvas"; ClearCanvas.displayName = "ClearCanvas";
@ -171,7 +152,9 @@ export const ToggleTheme = () => {
return ( return (
<DropdownMenuItem <DropdownMenuItem
onSelect={() => { onSelect={(event) => {
// do not close the menu when changing theme
event.preventDefault();
return actionManager.executeAction(actionToggleTheme); return actionManager.executeAction(actionToggleTheme);
}} }}
icon={appState.theme === "dark" ? SunIcon : MoonIcon} icon={appState.theme === "dark" ? SunIcon : MoonIcon}

View File

@ -11,14 +11,25 @@ import * as DefaultItems from "./DefaultItems";
import { UserList } from "../UserList"; import { UserList } from "../UserList";
import { t } from "../../i18n"; import { t } from "../../i18n";
import { HamburgerMenuIcon } from "../icons"; import { HamburgerMenuIcon } from "../icons";
import { composeEventHandlers } from "../../utils";
const MainMenu = ({ children }: { children?: React.ReactNode }) => { const MainMenu = ({
children,
onSelect,
}: {
children?: React.ReactNode;
/**
* Called when any menu item is selected (clicked on).
*/
onSelect?: (event: Event) => void;
}) => {
const device = useDevice(); const device = useDevice();
const appState = useExcalidrawAppState(); const appState = useExcalidrawAppState();
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
const onClickOutside = device.isMobile const onClickOutside = device.isMobile
? undefined ? undefined
: () => setAppState({ openMenu: null }); : () => setAppState({ openMenu: null });
return ( return (
<DropdownMenu open={appState.openMenu === "canvas"}> <DropdownMenu open={appState.openMenu === "canvas"}>
<DropdownMenu.Trigger <DropdownMenu.Trigger
@ -30,7 +41,12 @@ const MainMenu = ({ children }: { children?: React.ReactNode }) => {
> >
{HamburgerMenuIcon} {HamburgerMenuIcon}
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content onClickOutside={onClickOutside}> <DropdownMenu.Content
onClickOutside={onClickOutside}
onSelect={composeEventHandlers(onSelect, () => {
setAppState({ openMenu: null });
})}
>
{children} {children}
{device.isMobile && appState.collaborators.size > 0 && ( {device.isMobile && appState.collaborators.size > 0 && (
<fieldset className="UserList-Wrapper"> <fieldset className="UserList-Wrapper">

View File

@ -2,6 +2,14 @@ import cssVariables from "./css/variables.module.scss";
import { AppProps } from "./types"; import { AppProps } from "./types";
import { FontFamilyValues } from "./element/types"; import { FontFamilyValues } from "./element/types";
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
export const isWindows = /^Win/.test(navigator.platform);
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
export const isFirefox =
"netscape" in window &&
navigator.userAgent.indexOf("rv:") > 1 &&
navigator.userAgent.indexOf("Gecko") > 1;
export const APP_NAME = "Excalidraw"; export const APP_NAME = "Excalidraw";
export const DRAGGING_THRESHOLD = 10; // px export const DRAGGING_THRESHOLD = 10; // px
@ -54,6 +62,7 @@ export enum EVENT {
SCROLL = "scroll", SCROLL = "scroll",
// custom events // custom events
EXCALIDRAW_LINK = "excalidraw-link", EXCALIDRAW_LINK = "excalidraw-link",
MENU_ITEM_SELECT = "menu.itemSelect",
} }
export const ENV = { export const ENV = {

View File

@ -8,6 +8,10 @@
} }
.excalidraw { .excalidraw {
--ui-font: Assistant, system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
color: var(--text-primary-color); color: var(--text-primary-color);
@ -549,6 +553,7 @@
border-top-left-radius: var(--border-radius-lg); border-top-left-radius: var(--border-radius-lg);
border-bottom-left-radius: var(--border-radius-lg); border-bottom-left-radius: var(--border-radius-lg);
border-right: 0; border-right: 0;
overflow: hidden;
background-color: var(--island-bg-color); background-color: var(--island-bg-color);

View File

@ -65,13 +65,18 @@
background-color: var(--button-bg, var(--island-bg-color)); background-color: var(--button-bg, var(--island-bg-color));
color: var(--button-color, var(--text-primary-color)); color: var(--button-color, var(--text-primary-color));
svg {
width: var(--button-width, var(--lg-icon-size));
height: var(--button-height, var(--lg-icon-size));
}
&:hover { &:hover {
background-color: var(--button-hover-bg); background-color: var(--button-hover-bg, var(--island-bg-color));
border-color: var(--button-hover-border, var(--default-border-color)); border-color: var(--button-hover-border, var(--default-border-color));
} }
&:active { &:active {
background-color: var(--button-active-bg); background-color: var(--button-active-bg, var(--island-bg-color));
border-color: var(--button-active-border, var(--color-primary-darkest)); border-color: var(--button-active-border, var(--color-primary-darkest));
} }
@ -85,9 +90,6 @@
svg { svg {
color: var(--button-color, var(--color-primary-darker)); color: var(--button-color, var(--color-primary-darker));
width: var(--button-width, var(--lg-icon-size));
height: var(--button-height, var(--lg-icon-size));
} }
} }
} }

View File

@ -2,7 +2,7 @@ import {
copyBlobToClipboardAsPng, copyBlobToClipboardAsPng,
copyTextToSystemClipboard, copyTextToSystemClipboard,
} from "../clipboard"; } from "../clipboard";
import { DEFAULT_EXPORT_PADDING, MIME_TYPES } from "../constants"; import { DEFAULT_EXPORT_PADDING, isFirefox, MIME_TYPES } from "../constants";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { exportToCanvas, exportToSvg } from "../scene/export"; import { exportToCanvas, exportToSvg } from "../scene/export";
@ -97,10 +97,21 @@ export const exportCanvas = async (
const blob = canvasToBlob(tempCanvas); const blob = canvasToBlob(tempCanvas);
await copyBlobToClipboardAsPng(blob); await copyBlobToClipboardAsPng(blob);
} catch (error: any) { } catch (error: any) {
console.warn(error);
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") { if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
throw error; throw error;
} }
// TypeError *probably* suggests ClipboardItem not defined, which
// people on Firefox can enable through a flag, so let's tell them.
if (isFirefox && error.name === "TypeError") {
throw new Error(
`${t("alerts.couldNotCopyToClipboard")}\n\n${t(
"hints.firefox_clipboard_write",
)}`,
);
} else {
throw new Error(t("alerts.couldNotCopyToClipboard")); throw new Error(t("alerts.couldNotCopyToClipboard"));
}
} finally { } finally {
tempCanvas.remove(); tempCanvas.remove();
} }

View File

@ -55,6 +55,7 @@ export const AllowedExcalidrawActiveTools: Record<
freedraw: true, freedraw: true,
eraser: false, eraser: false,
custom: true, custom: true,
hand: true,
}; };
export type RestoredDataState = { export type RestoredDataState = {
@ -465,7 +466,7 @@ export const restoreAppState = (
? nextAppState.activeTool ? nextAppState.activeTool
: { type: "selection" }, : { type: "selection" },
), ),
lastActiveToolBeforeEraser: null, lastActiveTool: null,
locked: nextAppState.activeTool.locked ?? false, locked: nextAppState.activeTool.locked ?? false,
}, },
// Migrates from previous version where appState.zoom was a number // Migrates from previous version where appState.zoom was a number

View File

@ -11,6 +11,7 @@ export const showSelectedShapeActions = (
appState.activeTool.type !== "custom" && appState.activeTool.type !== "custom" &&
(appState.editingElement || (appState.editingElement ||
(appState.activeTool.type !== "selection" && (appState.activeTool.type !== "selection" &&
appState.activeTool.type !== "eraser"))) || appState.activeTool.type !== "eraser" &&
appState.activeTool.type !== "hand"))) ||
getSelectedElements(elements, appState).length, getSelectedElements(elements, appState).length,
); );

View File

@ -12,6 +12,20 @@ describe("Test wrapText", () => {
expect(res).toBe("Hello whats up "); expect(res).toBe("Hello whats up ");
}); });
it("should work with emojis", () => {
const text = "😀";
const maxWidth = 1;
const res = wrapText(text, font, maxWidth);
expect(res).toBe("😀");
});
it("should show the text correctly when min width reached", () => {
const text = "Hello😀";
const maxWidth = 10;
const res = wrapText(text, font, maxWidth);
expect(res).toBe("H\ne\nl\nl\no\n😀");
});
describe("When text doesn't contain new lines", () => { describe("When text doesn't contain new lines", () => {
const text = "Hello whats up"; const text = "Hello whats up";
[ [

View File

@ -359,7 +359,8 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
// This means its newline so push it // This means its newline so push it
if (words.length === 1 && words[0] === "") { if (words.length === 1 && words[0] === "") {
lines.push(words[0]); lines.push(words[0]);
} else { return; // continue
}
let currentLine = ""; let currentLine = "";
let currentLineWidthTillNow = 0; let currentLineWidthTillNow = 0;
@ -375,10 +376,12 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
currentLine = ""; currentLine = "";
currentLineWidthTillNow = 0; currentLineWidthTillNow = 0;
while (words[index].length > 0) { while (words[index].length > 0) {
const currentChar = words[index][0]; const currentChar = String.fromCodePoint(
words[index].codePointAt(0)!,
);
const width = charWidth.calculate(currentChar, font); const width = charWidth.calculate(currentChar, font);
currentLineWidthTillNow += width; currentLineWidthTillNow += width;
words[index] = words[index].slice(1); words[index] = words[index].slice(currentChar.length);
if (currentLineWidthTillNow >= maxWidth) { if (currentLineWidthTillNow >= maxWidth) {
// only remove last trailing space which we have added when joining words // only remove last trailing space which we have added when joining words
@ -388,10 +391,6 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
push(currentLine); push(currentLine);
currentLine = currentChar; currentLine = currentChar;
currentLineWidthTillNow = width; currentLineWidthTillNow = width;
if (currentLineWidthTillNow === maxWidth) {
currentLine = "";
currentLineWidthTillNow = 0;
}
} else { } else {
currentLine += currentChar; currentLine += currentChar;
} }
@ -448,7 +447,6 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
} }
push(currentLine); push(currentLine);
} }
}
}); });
return lines.join("\n"); return lines.join("\n");
}; };

View File

@ -1,6 +1,4 @@
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform); import { isDarwin } from "./constants";
export const isWindows = /^Win/.test(window.navigator.platform);
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
export const CODES = { export const CODES = {
EQUAL: "Equal", EQUAL: "Equal",

View File

@ -1,7 +1,7 @@
{ {
"labels": { "labels": {
"paste": "لصق", "paste": "لصق",
"pasteAsPlaintext": "", "pasteAsPlaintext": "اللصق كنص عادي",
"pasteCharts": "لصق الرسوم البيانية", "pasteCharts": "لصق الرسوم البيانية",
"selectAll": "تحديد الكل", "selectAll": "تحديد الكل",
"multiSelect": "إضافة عنصر للتحديد", "multiSelect": "إضافة عنصر للتحديد",
@ -66,13 +66,13 @@
"cartoonist": "كرتوني", "cartoonist": "كرتوني",
"fileTitle": "إسم الملف", "fileTitle": "إسم الملف",
"colorPicker": "منتقي اللون", "colorPicker": "منتقي اللون",
"canvasColors": "", "canvasColors": "تستخدم على القماش",
"canvasBackground": "خلفية اللوحة", "canvasBackground": "خلفية اللوحة",
"drawingCanvas": "لوحة الرسم", "drawingCanvas": "لوحة الرسم",
"layers": "الطبقات", "layers": "الطبقات",
"actions": "الإجراءات", "actions": "الإجراءات",
"language": "اللغة", "language": "اللغة",
"liveCollaboration": "", "liveCollaboration": "التعاون المباشر...",
"duplicateSelection": "تكرار", "duplicateSelection": "تكرار",
"untitled": "غير معنون", "untitled": "غير معنون",
"name": "الاسم", "name": "الاسم",
@ -108,7 +108,7 @@
"excalidrawLib": "مكتبتنا", "excalidrawLib": "مكتبتنا",
"decreaseFontSize": "تصغير حجم الخط", "decreaseFontSize": "تصغير حجم الخط",
"increaseFontSize": "تكبير حجم الخط", "increaseFontSize": "تكبير حجم الخط",
"unbindText": "", "unbindText": "فك ربط النص",
"bindText": "", "bindText": "",
"link": { "link": {
"edit": "تعديل الرابط", "edit": "تعديل الرابط",
@ -145,7 +145,7 @@
"scale": "مقاس", "scale": "مقاس",
"save": "احفظ للملف الحالي", "save": "احفظ للملف الحالي",
"saveAs": "حفظ كـ", "saveAs": "حفظ كـ",
"load": "", "load": "فتح",
"getShareableLink": "احصل على رابط المشاركة", "getShareableLink": "احصل على رابط المشاركة",
"close": "غلق", "close": "غلق",
"selectLanguage": "اختر اللغة", "selectLanguage": "اختر اللغة",
@ -447,10 +447,16 @@
"d9480f": "برتقالي 9" "d9480f": "برتقالي 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "" "d9480f": ""
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "" "d9480f": ""
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Taronja 9" "d9480f": "Taronja 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Oranzova" "d9480f": "Oranzova"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "" "d9480f": ""
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -99,14 +99,14 @@
"flipHorizontal": "Horizontal spiegeln", "flipHorizontal": "Horizontal spiegeln",
"flipVertical": "Vertikal spiegeln", "flipVertical": "Vertikal spiegeln",
"viewMode": "Ansichtsmodus", "viewMode": "Ansichtsmodus",
"toggleExportColorScheme": "Exportfarbschema umschalten", "toggleExportColorScheme": "Farbschema für Export umschalten",
"share": "Teilen", "share": "Teilen",
"showStroke": "Auswahl für Strichfarbe anzeigen", "showStroke": "Auswahl für Strichfarbe anzeigen",
"showBackground": "Hintergrundfarbe auswählen", "showBackground": "Hintergrundfarbe auswählen",
"toggleTheme": "Thema umschalten", "toggleTheme": "Design umschalten",
"personalLib": "Persönliche Bibliothek", "personalLib": "Persönliche Bibliothek",
"excalidrawLib": "Excalidraw-Bibliothek", "excalidrawLib": "Excalidraw Bibliothek",
"decreaseFontSize": "Schrift verkleinern", "decreaseFontSize": "Schriftgröße verkleinern",
"increaseFontSize": "Schrift vergrößern", "increaseFontSize": "Schrift vergrößern",
"unbindText": "Text lösen", "unbindText": "Text lösen",
"bindText": "Text an Container binden", "bindText": "Text an Container binden",
@ -161,8 +161,8 @@
"resetLibrary": "Bibliothek zurücksetzen", "resetLibrary": "Bibliothek zurücksetzen",
"createNewRoom": "Neuen Raum erstellen", "createNewRoom": "Neuen Raum erstellen",
"fullScreen": "Vollbildanzeige", "fullScreen": "Vollbildanzeige",
"darkMode": "Dunkler Modus", "darkMode": "Dunkles Design",
"lightMode": "Heller Modus", "lightMode": "Helles Design",
"zenMode": "Zen-Modus", "zenMode": "Zen-Modus",
"exitZenMode": "Zen-Modus verlassen", "exitZenMode": "Zen-Modus verlassen",
"cancel": "Abbrechen", "cancel": "Abbrechen",
@ -447,10 +447,16 @@
"d9480f": "Orange 9" "d9480f": "Orange 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Alle Daten werden lokal in Deinem Browser gespeichert.", "app": {
"switchToPlusApp": "Möchtest du stattdessen zu Excalidraw+ gehen?", "center_heading": "Alle Daten werden lokal in Deinem Browser gespeichert.",
"menuHints": "Exportieren, Einstellungen, Sprachen, ...", "center_heading_plus": "Möchtest du stattdessen zu Excalidraw+ gehen?",
"toolbarHints": "Wähle ein Werkzeug & beginne zu zeichnen!", "menuHint": "Exportieren, Einstellungen, Sprachen, ..."
"helpHints": "Kurzbefehle & Hilfe" },
"defaults": {
"menuHint": "Exportieren, Einstellungen und mehr...",
"center_heading": "Diagramme. Einfach. Gemacht.",
"toolbarHint": "Wähle ein Werkzeug & beginne zu zeichnen!",
"helpHint": "Kurzbefehle & Hilfe"
}
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"labels": { "labels": {
"paste": "Επικόλληση", "paste": "Επικόλληση",
"pasteAsPlaintext": "", "pasteAsPlaintext": "Επικόλληση ως απλό κείμενο",
"pasteCharts": "Επικόλληση γραφημάτων", "pasteCharts": "Επικόλληση γραφημάτων",
"selectAll": "Επιλογή όλων", "selectAll": "Επιλογή όλων",
"multiSelect": "Προσθέστε το στοιχείο στην επιλογή", "multiSelect": "Προσθέστε το στοιχείο στην επιλογή",
@ -72,7 +72,7 @@
"layers": "Στρώματα", "layers": "Στρώματα",
"actions": "Ενέργειες", "actions": "Ενέργειες",
"language": "Γλώσσα", "language": "Γλώσσα",
"liveCollaboration": "", "liveCollaboration": "Live συνεργασία...",
"duplicateSelection": "Δημιουργία αντιγράφου", "duplicateSelection": "Δημιουργία αντιγράφου",
"untitled": "Χωρίς τίτλο", "untitled": "Χωρίς τίτλο",
"name": "Όνομα", "name": "Όνομα",
@ -116,8 +116,8 @@
"label": "Σύνδεσμος" "label": "Σύνδεσμος"
}, },
"lineEditor": { "lineEditor": {
"edit": "", "edit": "Επεξεργασία γραμμής",
"exit": "" "exit": "Έξοδος επεξεργαστή κειμένου"
}, },
"elementLock": { "elementLock": {
"lock": "Κλείδωμα", "lock": "Κλείδωμα",
@ -136,8 +136,8 @@
"buttons": { "buttons": {
"clearReset": "Επαναφορά του καμβά", "clearReset": "Επαναφορά του καμβά",
"exportJSON": "Εξαγωγή σε αρχείο", "exportJSON": "Εξαγωγή σε αρχείο",
"exportImage": "", "exportImage": "Εξαγωγή εικόνας...",
"export": "", "export": "Αποθήκευση ως...",
"exportToPng": "Εξαγωγή σε PNG", "exportToPng": "Εξαγωγή σε PNG",
"exportToSvg": "Εξαγωγή σε SVG", "exportToSvg": "Εξαγωγή σε SVG",
"copyToClipboard": "Αντιγραφή στο πρόχειρο", "copyToClipboard": "Αντιγραφή στο πρόχειρο",
@ -145,7 +145,7 @@
"scale": "Κλίμακα", "scale": "Κλίμακα",
"save": "Αποθήκευση στο τρέχον αρχείο", "save": "Αποθήκευση στο τρέχον αρχείο",
"saveAs": "Αποθήκευση ως", "saveAs": "Αποθήκευση ως",
"load": "", "load": "Άνοιγμα",
"getShareableLink": "Δημόσιος σύνδεσμος", "getShareableLink": "Δημόσιος σύνδεσμος",
"close": "Κλείσιμο", "close": "Κλείσιμο",
"selectLanguage": "Επιλογή γλώσσας", "selectLanguage": "Επιλογή γλώσσας",
@ -202,8 +202,8 @@
"invalidSVGString": "Μη έγκυρο SVG.", "invalidSVGString": "Μη έγκυρο SVG.",
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.", "cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης", "importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης",
"collabSaveFailed": "", "collabSaveFailed": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή. Αν το προβλήματα παραμείνει, θα πρέπει να αποθηκεύσετε το αρχείο σας τοπικά για να βεβαιωθείτε ότι δεν χάνετε την εργασία σας.",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή, ο καμβάς φαίνεται να είναι πολύ μεγάλος. Θα πρέπει να αποθηκεύσετε το αρχείο τοπικά για να βεβαιωθείτε ότι δεν θα χάσετε την εργασία σας."
}, },
"toolBar": { "toolBar": {
"selection": "Επιλογή", "selection": "Επιλογή",
@ -217,7 +217,7 @@
"text": "Κείμενο", "text": "Κείμενο",
"library": "Βιβλιοθήκη", "library": "Βιβλιοθήκη",
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο", "lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
"penMode": "", "penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα", "link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
"eraser": "Γόμα" "eraser": "Γόμα"
}, },
@ -238,7 +238,7 @@
"resize": "Μπορείς να περιορίσεις τις αναλογίες κρατώντας το SHIFT ενώ αλλάζεις μέγεθος,\nκράτησε πατημένο το ALT για αλλαγή μεγέθους από το κέντρο", "resize": "Μπορείς να περιορίσεις τις αναλογίες κρατώντας το SHIFT ενώ αλλάζεις μέγεθος,\nκράτησε πατημένο το ALT για αλλαγή μεγέθους από το κέντρο",
"resizeImage": "Μπορείτε να αλλάξετε το μέγεθος ελεύθερα κρατώντας πατημένο το SHIFT,\nκρατήστε πατημένο το ALT για να αλλάξετε το μέγεθος από το κέντρο", "resizeImage": "Μπορείτε να αλλάξετε το μέγεθος ελεύθερα κρατώντας πατημένο το SHIFT,\nκρατήστε πατημένο το ALT για να αλλάξετε το μέγεθος από το κέντρο",
"rotate": "Μπορείς να περιορίσεις τις γωνίες κρατώντας πατημένο το πλήκτρο SHIFT κατά την περιστροφή", "rotate": "Μπορείς να περιορίσεις τις γωνίες κρατώντας πατημένο το πλήκτρο SHIFT κατά την περιστροφή",
"lineEditor_info": "", "lineEditor_info": "Κρατήστε πατημένο Ctrl ή Cmd και πατήστε το πλήκτρο Ctrl ή Cmd + Enter για επεξεργασία σημείων",
"lineEditor_pointSelected": "Πατήστε Διαγραφή για αφαίρεση σημείου(ων),\nCtrlOrCmd+D για αντιγραφή, ή σύρετε για μετακίνηση", "lineEditor_pointSelected": "Πατήστε Διαγραφή για αφαίρεση σημείου(ων),\nCtrlOrCmd+D για αντιγραφή, ή σύρετε για μετακίνηση",
"lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για να επεξεργαστείτε (κρατήστε πατημένο το SHIFT για να επιλέξετε πολλαπλά),\nή κρατήστε πατημένο το Alt και κάντε κλικ για να προσθέσετε νέα σημεία", "lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για να επεξεργαστείτε (κρατήστε πατημένο το SHIFT για να επιλέξετε πολλαπλά),\nή κρατήστε πατημένο το Alt και κάντε κλικ για να προσθέσετε νέα σημεία",
"placeImage": "Κάντε κλικ για να τοποθετήσετε την εικόνα ή κάντε κλικ και σύρετε για να ορίσετε το μέγεθός της χειροκίνητα", "placeImage": "Κάντε κλικ για να τοποθετήσετε την εικόνα ή κάντε κλικ και σύρετε για να ορίσετε το μέγεθός της χειροκίνητα",
@ -314,8 +314,8 @@
"zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία", "zoomToFit": "Zoom ώστε να χωρέσουν όλα τα στοιχεία",
"zoomToSelection": "Ζουμ στην επιλογή", "zoomToSelection": "Ζουμ στην επιλογή",
"toggleElementLock": "Κλείδωμα/Ξεκλείδωμα επιλογής", "toggleElementLock": "Κλείδωμα/Ξεκλείδωμα επιλογής",
"movePageUpDown": "", "movePageUpDown": "Μετακίνηση σελίδας πάνω/κάτω",
"movePageLeftRight": "" "movePageLeftRight": "Μετακίνηση σελίδας αριστερά/δεξιά"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Καθαρισμός καμβά" "title": "Καθαρισμός καμβά"
@ -397,7 +397,7 @@
"fileSavedToFilename": "Αποθηκεύτηκε στο {filename}", "fileSavedToFilename": "Αποθηκεύτηκε στο {filename}",
"canvas": "καμβάς", "canvas": "καμβάς",
"selection": "επιλογή", "selection": "επιλογή",
"pasteAsSingleElement": "" "pasteAsSingleElement": "Χρησιμοποίησε το {{shortcut}} για να επικολλήσεις ως ένα μόνο στοιχείο,\nή να επικολλήσεις σε έναν υπάρχοντα επεξεργαστή κειμένου"
}, },
"colors": { "colors": {
"ffffff": "Λευκό", "ffffff": "Λευκό",
@ -447,10 +447,16 @@
"d9480f": "Πορτοκαλί 9" "d9480f": "Πορτοκαλί 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "Όλα τα δεδομένα σας αποθηκεύονται τοπικά στο πρόγραμμα περιήγησης.",
"menuHints": "", "center_heading_plus": "Μήπως θέλατε να πάτε στο Excalidraw+;",
"toolbarHints": "", "menuHint": "Εξαγωγή, προτιμήσεις, γλώσσες, ..."
"helpHints": "" },
"defaults": {
"menuHint": "Εξαγωγή, προτιμήσεις και άλλες επιλογές...",
"center_heading": "Διαγράμματα. Εύκολα. Γρήγορα.",
"toolbarHint": "Επιλέξτε ένα εργαλείο και ξεκινήστε να σχεδιάζεται!",
"helpHint": "Συντομεύσεις και βοήθεια"
}
} }
} }

View File

@ -220,7 +220,8 @@
"lock": "Keep selected tool active after drawing", "lock": "Keep selected tool active after drawing",
"penMode": "Pen mode - prevent touch", "penMode": "Pen mode - prevent touch",
"link": "Add/ Update link for a selected shape", "link": "Add/ Update link for a selected shape",
"eraser": "Eraser" "eraser": "Eraser",
"hand": "Hand (panning tool)"
}, },
"headings": { "headings": {
"canvasActions": "Canvas actions", "canvasActions": "Canvas actions",
@ -228,7 +229,7 @@
"shapes": "Shapes" "shapes": "Shapes"
}, },
"hints": { "hints": {
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging", "canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
"linearElement": "Click to start multiple points, drag for single line", "linearElement": "Click to start multiple points, drag for single line",
"freeDraw": "Click and drag, release when you're finished", "freeDraw": "Click and drag, release when you're finished",
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool", "text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
@ -246,7 +247,8 @@
"publishLibrary": "Publish your own library", "publishLibrary": "Publish your own library",
"bindTextToElement": "Press enter to add text", "bindTextToElement": "Press enter to add text",
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging", "deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
"eraserRevert": "Hold Alt to revert the elements marked for deletion" "eraserRevert": "Hold Alt to revert the elements marked for deletion",
"firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page."
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Cannot show preview", "cannotShowPreview": "Cannot show preview",

View File

@ -447,10 +447,16 @@
"d9480f": "Naranja 9" "d9480f": "Naranja 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Toda su información es guardada localmente en su navegador.", "app": {
"switchToPlusApp": "¿Quieres ir a Excalidraw+ en su lugar?", "center_heading": "Toda su información es guardada localmente en su navegador.",
"menuHints": "Exportar, preferencias, idiomas, ...", "center_heading_plus": "¿Quieres ir a Excalidraw+?",
"toolbarHints": "¡Escoge una herramienta & Empiece a dibujar!", "menuHint": "Exportar, preferencias, idiomas, ..."
"helpHints": "Atajos & ayuda" },
"defaults": {
"menuHint": "Exportar, preferencias y más...",
"center_heading": "Diagramas. Hecho. Simplemente.",
"toolbarHint": "¡Elige una herramienta y empieza a dibujar!",
"helpHint": "Atajos & ayuda"
}
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"labels": { "labels": {
"paste": "Itsatsi", "paste": "Itsatsi",
"pasteAsPlaintext": "", "pasteAsPlaintext": "Itsatsi testu arrunt gisa",
"pasteCharts": "Itsatsi grafikoak", "pasteCharts": "Itsatsi grafikoak",
"selectAll": "Hautatu dena", "selectAll": "Hautatu dena",
"multiSelect": "Gehitu elementua hautapenera", "multiSelect": "Gehitu elementua hautapenera",
@ -202,8 +202,8 @@
"invalidSVGString": "SVG baliogabea.", "invalidSVGString": "SVG baliogabea.",
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.", "cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
"importLibraryError": "Ezin izan da liburutegia kargatu", "importLibraryError": "Ezin izan da liburutegia kargatu",
"collabSaveFailed": "", "collabSaveFailed": "Ezin izan da backend datu-basean gorde. Arazoak jarraitzen badu, zure fitxategia lokalean gorde beharko zenuke zure lana ez duzula galtzen ziurtatzeko.",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "Ezin izan da backend datu-basean gorde, ohiala handiegia dela dirudi. Fitxategia lokalean gorde beharko zenuke zure lana galtzen ez duzula ziurtatzeko."
}, },
"toolBar": { "toolBar": {
"selection": "Hautapena", "selection": "Hautapena",
@ -238,7 +238,7 @@
"resize": "Proportzioak mantendu ditzakezu SHIFT sakatuta tamaina aldatzen duzun bitartean.\nsakatu ALT erditik tamaina aldatzeko", "resize": "Proportzioak mantendu ditzakezu SHIFT sakatuta tamaina aldatzen duzun bitartean.\nsakatu ALT erditik tamaina aldatzeko",
"resizeImage": "Tamaina libreki alda dezakezu SHIFT sakatuta,\nsakatu ALT erditik tamaina aldatzeko", "resizeImage": "Tamaina libreki alda dezakezu SHIFT sakatuta,\nsakatu ALT erditik tamaina aldatzeko",
"rotate": "Angeluak mantendu ditzakezu SHIFT sakatuta biratzen duzun bitartean", "rotate": "Angeluak mantendu ditzakezu SHIFT sakatuta biratzen duzun bitartean",
"lineEditor_info": "", "lineEditor_info": "Eutsi sakatuta Ctrl edo Cmd eta egin klik bikoitza edo sakatu Ctrl edo Cmd + Sartu puntuak editatzeko",
"lineEditor_pointSelected": "Sakatu Ezabatu puntuak kentzeko,\nKtrl+D bikoizteko, edo arrastatu mugitzeko", "lineEditor_pointSelected": "Sakatu Ezabatu puntuak kentzeko,\nKtrl+D bikoizteko, edo arrastatu mugitzeko",
"lineEditor_nothingSelected": "Hautatu editatzeko puntu bat (SHIFT sakatuta anitz hautatzeko),\nedo eduki Alt sakatuta eta egin klik puntu berriak gehitzeko", "lineEditor_nothingSelected": "Hautatu editatzeko puntu bat (SHIFT sakatuta anitz hautatzeko),\nedo eduki Alt sakatuta eta egin klik puntu berriak gehitzeko",
"placeImage": "Egin klik irudia kokatzeko, edo egin klik eta arrastatu bere tamaina eskuz ezartzeko", "placeImage": "Egin klik irudia kokatzeko, edo egin klik eta arrastatu bere tamaina eskuz ezartzeko",
@ -314,8 +314,8 @@
"zoomToFit": "Egin zoom elementu guztiak ikusteko", "zoomToFit": "Egin zoom elementu guztiak ikusteko",
"zoomToSelection": "Zooma hautapenera", "zoomToSelection": "Zooma hautapenera",
"toggleElementLock": "Blokeatu/desbloketatu hautapena", "toggleElementLock": "Blokeatu/desbloketatu hautapena",
"movePageUpDown": "", "movePageUpDown": "Mugitu orria gora/behera",
"movePageLeftRight": "" "movePageLeftRight": "Mugitu orria ezker/eskuin"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Garbitu oihala" "title": "Garbitu oihala"
@ -397,7 +397,7 @@
"fileSavedToFilename": "{filename}-n gorde da", "fileSavedToFilename": "{filename}-n gorde da",
"canvas": "oihala", "canvas": "oihala",
"selection": "hautapena", "selection": "hautapena",
"pasteAsSingleElement": "" "pasteAsSingleElement": "Erabili {{shortcut}} elementu bakar gisa itsasteko,\nedo itsatsi lehendik dagoen testu-editore batean"
}, },
"colors": { "colors": {
"ffffff": "Zuria", "ffffff": "Zuria",
@ -447,10 +447,16 @@
"d9480f": "Laranja 9" "d9480f": "Laranja 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Zure datu guztiak modu lokalean gordetzen dira zure nabigatzailean.", "app": {
"switchToPlusApp": "Horren ordez Excalidraw+-ra joan nahi al zenuen?", "center_heading": "Zure datu guztiak lokalean gordetzen dira zure nabigatzailean.",
"menuHints": "Esportatu, hobespenak, hizkuntzak,...", "center_heading_plus": "Horren ordez Excalidraw+-era joan nahi al zenuen?",
"toolbarHints": "Aukeratu tresna bat eta hasi marrazten!", "menuHint": "Esportatu, hobespenak, hizkuntzak..."
"helpHints": "Lasterbideak eta laguntza" },
"defaults": {
"menuHint": "Esportatu, hobespenak eta gehiago...",
"center_heading": "Diagramak. Egina. Sinplea.",
"toolbarHint": "Aukeratu tresna bat eta hasi marrazten!",
"helpHint": "Lasterbideak eta laguntza"
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "نارنجی 9" "d9480f": "نارنجی 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "همه ی داده های شما به صورت محلی در مرورگر ذخیره میشود.", "app": {
"switchToPlusApp": "آیا ترجیح میدهید به Excalidraw+ بروید؟", "center_heading": "",
"menuHints": "خروجی گرفتن، تنظیمات، زبانها، ...", "center_heading_plus": "",
"toolbarHints": "یک ابزار را انتخاب کنید و ترسیم را شروع کنید!", "menuHint": ""
"helpHints": "میانبرها و کمک" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Oranssi 9" "d9480f": "Oranssi 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Orange 9" "d9480f": "Orange 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Toutes vos données sont sauvegardées en local dans votre navigateur.", "app": {
"switchToPlusApp": "Vous vouliez plutôt aller à Excalidraw+ ?", "center_heading": "Toutes vos données sont sauvegardées en local dans votre navigateur.",
"menuHints": "Exportation, préférences, langues, ...", "center_heading_plus": "Vouliez-vous plutôt aller à Excalidraw+ à la place ?",
"toolbarHints": "Choisissez un outil et commencez à dessiner !", "menuHint": "Exportation, préférences, langues, ..."
"helpHints": "Raccourcis et aide" },
"defaults": {
"menuHint": "Exportation, préférences et plus...",
"center_heading": "Diagrammes. Rendus. Simples.",
"toolbarHint": "Choisissez un outil et commencez à dessiner !",
"helpHint": "Raccourcis et aide"
}
} }
} }

View File

@ -202,8 +202,8 @@
"invalidSVGString": "SVG inválido.", "invalidSVGString": "SVG inválido.",
"cannotResolveCollabServer": "Non se puido conectar ao servidor de colaboración. Por favor recargue a páxina e probe de novo.", "cannotResolveCollabServer": "Non se puido conectar ao servidor de colaboración. Por favor recargue a páxina e probe de novo.",
"importLibraryError": "Non se puido cargar a biblioteca", "importLibraryError": "Non se puido cargar a biblioteca",
"collabSaveFailed": "", "collabSaveFailed": "Non se puido gardar na base de datos. Se o problema persiste, deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo.",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "Non se puido gardar na base de datos, o lenzo semella demasiado grande. Deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo."
}, },
"toolBar": { "toolBar": {
"selection": "Selección", "selection": "Selección",
@ -447,10 +447,16 @@
"d9480f": "Laranxa 9" "d9480f": "Laranxa 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Toda a información é gardada de maneira local no seu navegador.", "app": {
"switchToPlusApp": "Queres ir a Excalidraw+ no seu lugar?", "center_heading": "Toda a información é gardada de maneira local no seu navegador.",
"menuHints": "Exportar, preferencias, idiomas, ...", "center_heading_plus": "Queres ir a Excalidraw+ no seu lugar?",
"toolbarHints": "Escolle unha ferramenta & Comeza a debuxar!", "menuHint": "Exportar, preferencias, idiomas, ..."
"helpHints": "Atallos & axuda" },
"defaults": {
"menuHint": "Exportar, preferencias, e máis...",
"center_heading": "Diagramas. Feito. Sinxelo.",
"toolbarHint": "Escolle unha ferramenta & Comeza a debuxar!",
"helpHint": "Atallos & axuda"
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "כתום 9" "d9480f": "כתום 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "नारंगी" "d9480f": "नारंगी"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "आपका सर्व डेटा ब्राउज़र के भीतर स्थानिक जगह पे सुरक्षित किया गया.", "app": {
"switchToPlusApp": "बजाय आपको Excalidraw+ जगह जाना है?", "center_heading": "",
"menuHints": "निर्यात, पसंद, भाषायें, ...", "center_heading_plus": "",
"toolbarHints": "औजार चुने और चित्रकारी प्रारंभ करे!", "menuHint": ""
"helpHints": "शॉर्ट्कट & सहाय्य" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Narancs 9" "d9480f": "Narancs 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -202,8 +202,8 @@
"invalidSVGString": "SVG tidak valid.", "invalidSVGString": "SVG tidak valid.",
"cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.", "cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.",
"importLibraryError": "Tidak dapat memuat pustaka", "importLibraryError": "Tidak dapat memuat pustaka",
"collabSaveFailed": "", "collabSaveFailed": "Tidak dapat menyimpan ke dalam basis data server. Jika masih berlanjut, Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang.",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "Tidak dapat menyimpan ke dalam basis data server, tampaknya ukuran kanvas terlalu besar. Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang."
}, },
"toolBar": { "toolBar": {
"selection": "Pilihan", "selection": "Pilihan",
@ -447,10 +447,16 @@
"d9480f": "Jingga 9" "d9480f": "Jingga 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Semua data Anda tersimpan secara lokal di browser.", "app": {
"switchToPlusApp": "Apa Anda ingin berpindah ke Excalidraw+?", "center_heading": "Semua data Anda disimpan secara lokal di peramban Anda.",
"menuHints": "Ekspor, preferensi, bahasa, ...", "center_heading_plus": "Apa Anda ingin berpindah ke Excalidraw+?",
"toolbarHints": "Ambil alat & mulai menggambar!", "menuHint": "Ekspor, preferensi, bahasa, ..."
"helpHints": "Pintasan & bantuan" },
"defaults": {
"menuHint": "Ekspor, preferensi, dan selebihnya...",
"center_heading": "Diagram. Menjadi. Mudah.",
"toolbarHint": "Pilih alat & mulai menggambar!",
"helpHint": "Pintasan & bantuan"
}
} }
} }

View File

@ -202,8 +202,8 @@
"invalidSVGString": "SVG non valido.", "invalidSVGString": "SVG non valido.",
"cannotResolveCollabServer": "Impossibile connettersi al server di collab. Ricarica la pagina e riprova.", "cannotResolveCollabServer": "Impossibile connettersi al server di collab. Ricarica la pagina e riprova.",
"importLibraryError": "Impossibile caricare la libreria", "importLibraryError": "Impossibile caricare la libreria",
"collabSaveFailed": "", "collabSaveFailed": "Impossibile salvare nel database di backend. Se i problemi persistono, dovresti salvare il tuo file localmente per assicurarti di non perdere il tuo lavoro.",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "Impossibile salvare nel database di backend, la tela sembra essere troppo grande. Dovresti salvare il file localmente per assicurarti di non perdere il tuo lavoro."
}, },
"toolBar": { "toolBar": {
"selection": "Selezione", "selection": "Selezione",
@ -447,10 +447,16 @@
"d9480f": "Arancio 9" "d9480f": "Arancio 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Tutti i tuoi dati sono salvati localmente nel browser.", "app": {
"switchToPlusApp": "Volevi invece andare su Excalidraw+?", "center_heading": "Tutti i tuoi dati sono salvati localmente nel browser.",
"menuHints": "Esporta, preferenze, lingue, ...", "center_heading_plus": "Volevi invece andare su Excalidraw+?",
"toolbarHints": "Scegli uno strumento & Inizia a disegnare!", "menuHint": "Esporta, preferenze, lingue, ..."
"helpHints": "Scorciatoie & aiuto" },
"defaults": {
"menuHint": "Esporta, preferenze, e altro...",
"center_heading": "Diagrammi. Fatto. Semplice.",
"toolbarHint": "Scegli uno strumento & Inizia a disegnare!",
"helpHint": "Scorciatoie & aiuto"
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "オレンジ 9" "d9480f": "オレンジ 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "すべてのデータはブラウザにローカル保存されます。", "app": {
"switchToPlusApp": "代わりにExcalidraw+を開きますか?", "center_heading": "すべてのデータはブラウザにローカル保存されます。",
"menuHints": "エクスポート, 設定, 言語, ...", "center_heading_plus": "代わりにExcalidraw+を開きますか?",
"toolbarHints": "ツールを選んで描き始めよう!", "menuHint": "エクスポート、設定、言語..."
"helpHints": "ショートカットとヘルプ" },
"defaults": {
"menuHint": "エクスポート、設定、その他...",
"center_heading": "ダイアグラムを簡単に。",
"toolbarHint": "ツールを選んで描き始めよう!",
"helpHint": "ショートカットとヘルプ"
}
} }
} }

View File

@ -72,7 +72,7 @@
"layers": "Tissiyin", "layers": "Tissiyin",
"actions": "Tigawin", "actions": "Tigawin",
"language": "Tutlayt", "language": "Tutlayt",
"liveCollaboration": "", "liveCollaboration": "Amɛiwen s srid...",
"duplicateSelection": "Sisleg", "duplicateSelection": "Sisleg",
"untitled": "War azwel", "untitled": "War azwel",
"name": "Isem", "name": "Isem",
@ -314,8 +314,8 @@
"zoomToFit": "Simɣur akken ad twliḍ akk iferdisen", "zoomToFit": "Simɣur akken ad twliḍ akk iferdisen",
"zoomToSelection": "Simɣur ɣer tefrayt", "zoomToSelection": "Simɣur ɣer tefrayt",
"toggleElementLock": "Sekkeṛ/kkes asekker i tefrayt", "toggleElementLock": "Sekkeṛ/kkes asekker i tefrayt",
"movePageUpDown": "", "movePageUpDown": "Smutti asebter d asawen/akessar",
"movePageLeftRight": "" "movePageLeftRight": "Smutti asebter s azelmaḍ/ayfus"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Sfeḍ taɣzut n usuneɣ" "title": "Sfeḍ taɣzut n usuneɣ"
@ -447,10 +447,16 @@
"d9480f": "Aččinawi 9" "d9480f": "Aččinawi 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Akk isefka-inek•inem ttwakelsen s wudem adigan deg yiminig-inek•inem.", "app": {
"switchToPlusApp": "Tebɣiḍ ad tedduḍ ɣer Excalidraw+ deg umḍiq?", "center_heading": "",
"menuHints": "Asifeḍ, ismenyifen, tutlayin, ...", "center_heading_plus": "",
"toolbarHints": "Fren afecku tebduḍ asuneɣ!", "menuHint": ""
"helpHints": "Inegzumen akked tallelt" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "" "d9480f": ""
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"labels": { "labels": {
"paste": "붙여넣기", "paste": "붙여넣기",
"pasteAsPlaintext": "", "pasteAsPlaintext": "일반 텍스트로 붙여넣기",
"pasteCharts": "차트 붙여넣기", "pasteCharts": "차트 붙여넣기",
"selectAll": "전체 선택", "selectAll": "전체 선택",
"multiSelect": "선택 영역에 추가하기", "multiSelect": "선택 영역에 추가하기",
@ -72,7 +72,7 @@
"layers": "레이어", "layers": "레이어",
"actions": "동작", "actions": "동작",
"language": "언어", "language": "언어",
"liveCollaboration": "", "liveCollaboration": "실시간 협업...",
"duplicateSelection": "복제", "duplicateSelection": "복제",
"untitled": "제목 없음", "untitled": "제목 없음",
"name": "이름", "name": "이름",
@ -136,8 +136,8 @@
"buttons": { "buttons": {
"clearReset": "캔버스 초기화", "clearReset": "캔버스 초기화",
"exportJSON": "파일로 내보내기", "exportJSON": "파일로 내보내기",
"exportImage": "", "exportImage": "이미지 내보내기",
"export": "", "export": "다른 이름으로 저장...",
"exportToPng": "PNG로 내보내기", "exportToPng": "PNG로 내보내기",
"exportToSvg": "SVG로 내보내기", "exportToSvg": "SVG로 내보내기",
"copyToClipboard": "클립보드로 복사", "copyToClipboard": "클립보드로 복사",
@ -145,7 +145,7 @@
"scale": "크기", "scale": "크기",
"save": "현재 파일에 저장", "save": "현재 파일에 저장",
"saveAs": "다른 이름으로 저장", "saveAs": "다른 이름으로 저장",
"load": "", "load": "열기",
"getShareableLink": "공유 가능한 링크 생성", "getShareableLink": "공유 가능한 링크 생성",
"close": "닫기", "close": "닫기",
"selectLanguage": "언어 선택", "selectLanguage": "언어 선택",
@ -202,8 +202,8 @@
"invalidSVGString": "유효하지 않은 SVG입니다.", "invalidSVGString": "유효하지 않은 SVG입니다.",
"cannotResolveCollabServer": "협업 서버에 접속하는데 실패했습니다. 페이지를 새로고침하고 다시 시도해보세요.", "cannotResolveCollabServer": "협업 서버에 접속하는데 실패했습니다. 페이지를 새로고침하고 다시 시도해보세요.",
"importLibraryError": "라이브러리를 불러오지 못했습니다.", "importLibraryError": "라이브러리를 불러오지 못했습니다.",
"collabSaveFailed": "", "collabSaveFailed": "데이터베이스에 저장하지 못했습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "데이터베이스에 저장하지 못했습니다. 캔버스가 너무 큰 거 같습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요."
}, },
"toolBar": { "toolBar": {
"selection": "선택", "selection": "선택",
@ -217,7 +217,7 @@
"text": "텍스트", "text": "텍스트",
"library": "라이브러리", "library": "라이브러리",
"lock": "선택된 도구 유지하기", "lock": "선택된 도구 유지하기",
"penMode": "", "penMode": "펜 모드 - 터치 방지",
"link": "선택한 도형에 대해서 링크를 추가/업데이트", "link": "선택한 도형에 대해서 링크를 추가/업데이트",
"eraser": "지우개" "eraser": "지우개"
}, },
@ -238,7 +238,7 @@
"resize": "SHIFT 키를 누르면서 조정하면 크기의 비율이 제한됩니다.\nALT를 누르면서 조정하면 중앙을 기준으로 크기를 조정합니다.", "resize": "SHIFT 키를 누르면서 조정하면 크기의 비율이 제한됩니다.\nALT를 누르면서 조정하면 중앙을 기준으로 크기를 조정합니다.",
"resizeImage": "SHIFT를 눌러서 자유롭게 크기를 변경하거나,\nALT를 눌러서 중앙을 고정하고 크기를 변경하기", "resizeImage": "SHIFT를 눌러서 자유롭게 크기를 변경하거나,\nALT를 눌러서 중앙을 고정하고 크기를 변경하기",
"rotate": "SHIFT 키를 누르면서 회전하면 각도를 제한할 수 있습니다.", "rotate": "SHIFT 키를 누르면서 회전하면 각도를 제한할 수 있습니다.",
"lineEditor_info": "", "lineEditor_info": "포인트를 편집하려면 Ctrl/Cmd을 누르고 더블 클릭을 하거나 Ctrl/Cmd + Enter를 누르세요",
"lineEditor_pointSelected": "Delete 키로 꼭짓점을 제거하거나,\nCtrlOrCmd+D 로 복제하거나, 드래그 해서 이동시키기", "lineEditor_pointSelected": "Delete 키로 꼭짓점을 제거하거나,\nCtrlOrCmd+D 로 복제하거나, 드래그 해서 이동시키기",
"lineEditor_nothingSelected": "꼭짓점을 선택해서 수정하거나 (SHIFT를 눌러서 여러개 선택),\nAlt를 누르고 클릭해서 새로운 꼭짓점 추가하기", "lineEditor_nothingSelected": "꼭짓점을 선택해서 수정하거나 (SHIFT를 눌러서 여러개 선택),\nAlt를 누르고 클릭해서 새로운 꼭짓점 추가하기",
"placeImage": "클릭해서 이미지를 배치하거나, 클릭하고 드래그해서 사이즈를 조정하기", "placeImage": "클릭해서 이미지를 배치하거나, 클릭하고 드래그해서 사이즈를 조정하기",
@ -314,8 +314,8 @@
"zoomToFit": "모든 요소가 보이도록 확대/축소", "zoomToFit": "모든 요소가 보이도록 확대/축소",
"zoomToSelection": "선택 영역으로 확대/축소", "zoomToSelection": "선택 영역으로 확대/축소",
"toggleElementLock": "선택한 항목을 잠금/잠금 해제", "toggleElementLock": "선택한 항목을 잠금/잠금 해제",
"movePageUpDown": "", "movePageUpDown": "페이지 움직이기 위/아래",
"movePageLeftRight": "" "movePageLeftRight": "페이지 움직이기 좌/우"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "캔버스 지우기" "title": "캔버스 지우기"
@ -397,7 +397,7 @@
"fileSavedToFilename": "{filename} 로 저장되었습니다", "fileSavedToFilename": "{filename} 로 저장되었습니다",
"canvas": "캔버스", "canvas": "캔버스",
"selection": "선택한 요소", "selection": "선택한 요소",
"pasteAsSingleElement": "" "pasteAsSingleElement": "단일 요소로 붙여넣거나, 기존 텍스트 에디터에 붙여넣으려면 {{shortcut}} 을 사용하세요."
}, },
"colors": { "colors": {
"ffffff": "화이트", "ffffff": "화이트",
@ -447,10 +447,16 @@
"d9480f": "주황색 9" "d9480f": "주황색 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "당신의 모든 데이터는 브라우저에 저장되었습니다.",
"menuHints": "", "center_heading_plus": "대신 Excalidraw+로 이동하시겠습니까?",
"toolbarHints": "", "menuHint": "내보내기, 설정, 언어, ..."
"helpHints": "" },
"defaults": {
"menuHint": "내보내기, 설정, 더 보기...",
"center_heading": "",
"toolbarHint": "도구 선택 & 그리기 시작",
"helpHint": "단축키 & 도움말"
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "پرتەقاڵی 9" "d9480f": "پرتەقاڵی 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "هەموو داتاکانت لە ناو بڕاوزەرەکەتدا پاشەکەوت کراوە.", "app": {
"switchToPlusApp": "دەتویست بچیت بۆ Excalidraw+؟", "center_heading": "",
"menuHints": "هەناردەکردن، پەسندکردنەکان، زمانەکان، ...", "center_heading_plus": "",
"toolbarHints": "ئامرازێک هەڵبژێرە و دەستبکە بە وێنەکێشان!", "menuHint": ""
"helpHints": "قەدبڕەکان و یارمەتی" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Oranžinė 9" "d9480f": "Oranžinė 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Visi tavo duomenys išsaugoti lokaliai naršyklėje.", "app": {
"switchToPlusApp": "Ar vietoj to norėjai patekti į Excalidraw+?", "center_heading": "",
"menuHints": "Eksportavimas, parinktys, kalbos, ...", "center_heading_plus": "",
"toolbarHints": "Pasirink įrankį ir Pradėk piešti!", "menuHint": ""
"helpHints": "Spartieji klavišai ir pagalba" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Oranžs 9" "d9480f": "Oranžs 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Visi jūsu dati tiek glabāti uz vietas jūsu pārlūkā.", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "Eksportēšana, iestatījumi, valodas...", "center_heading_plus": "",
"toolbarHints": "Izvēlieties rīku un sāciet zīmēt!", "menuHint": ""
"helpHints": "Saīsnes un palīdzība" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "नारंगी 9" "d9480f": "नारंगी 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "तुमचा सर्व डेटा ब्राउज़र मधे स्थानिक जागेत सुरक्षित झाला.", "app": {
"switchToPlusApp": "त्याएवजी तुम्हाला Excalidraw+ पर्याय हवा आहे का?", "center_heading": "",
"menuHints": "निर्यात, पसंती, भाषा, ...", "center_heading_plus": "",
"toolbarHints": "साधन निवडा आणि चित्रीकरण सुरु करा!", "menuHint": ""
"helpHints": "शॉर्टकट & सहाय" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "" "d9480f": ""
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Oransje 9" "d9480f": "Oransje 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Alle dine data lagres lokalt i din nettleser.", "app": {
"switchToPlusApp": "Ønsker du å gå til Excalidraw+ i stedet?", "center_heading": "Alle dine data lagres lokalt i din nettleser.",
"menuHints": "Eksporter, innstillinger, språk, ...", "center_heading_plus": "Ønsker du å gå til Excalidraw+ i stedet?",
"toolbarHints": "Velg et verktøy og start å tegne!", "menuHint": "Eksporter, innstillinger, språk, ..."
"helpHints": "Snarveier & hjelp" },
"defaults": {
"menuHint": "Eksporter, innstillinger og mer...",
"center_heading": "Diagrammer. Gjort. Enkelt.",
"toolbarHint": "Velg et verktøy og start å tegne!",
"helpHint": "Snarveier & hjelp"
}
} }
} }

View File

@ -343,7 +343,7 @@
}, },
"noteDescription": { "noteDescription": {
"pre": "", "pre": "",
"link": "", "link": "openbare repository",
"post": "" "post": ""
}, },
"noteGuidelines": { "noteGuidelines": {
@ -447,10 +447,16 @@
"d9480f": "Oranje 9" "d9480f": "Oranje 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Oransj 9" "d9480f": "Oransj 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Irange 9" "d9480f": "Irange 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Totas las donadas son enregistradas dins vòstre navegador.", "app": {
"switchToPlusApp": "Volètz puslèu utilizar Excalidraw+?", "center_heading": "Totas las donadas son enregistradas dins vòstre navegador.",
"menuHints": "Exportar, preferéncias, lengas, ...", "center_heading_plus": "Voliatz puslèu utilizar Excalidraw+ a la plaça?",
"toolbarHints": "Prenètz un esplech e començatz de dessenhar!", "menuHint": "Exportar, preferéncias, lengas, ..."
"helpHints": "Acorchis e ajuda" },
"defaults": {
"menuHint": "Exportar, preferéncias, e mai...",
"center_heading": "Diagram. Tot. Simplament.",
"toolbarHint": "Prenètz un esplech e començatz de dessenhar!",
"helpHint": "Acorchis e ajuda"
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "ਸੰਤਰੀ 9" "d9480f": "ਸੰਤਰੀ 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -1,50 +1,50 @@
{ {
"ar-SA": 86, "ar-SA": 87,
"bg-BG": 55, "bg-BG": 55,
"bn-BD": 60, "bn-BD": 60,
"ca-ES": 94, "ca-ES": 94,
"cs-CZ": 75, "cs-CZ": 75,
"da-DK": 33, "da-DK": 33,
"de-DE": 100, "de-DE": 100,
"el-GR": 95, "el-GR": 100,
"en": 100, "en": 100,
"es-ES": 100, "es-ES": 100,
"eu-ES": 98, "eu-ES": 100,
"fa-IR": 98, "fa-IR": 96,
"fi-FI": 93, "fi-FI": 93,
"fr-FR": 100, "fr-FR": 100,
"gl-ES": 99, "gl-ES": 100,
"he-IL": 91, "he-IL": 90,
"hi-IN": 71, "hi-IN": 69,
"hu-HU": 90, "hu-HU": 89,
"id-ID": 99, "id-ID": 100,
"it-IT": 99, "it-IT": 100,
"ja-JP": 100, "ja-JP": 100,
"kab-KAB": 94, "kab-KAB": 93,
"kk-KZ": 21, "kk-KZ": 21,
"ko-KR": 95, "ko-KR": 99,
"ku-TR": 98, "ku-TR": 96,
"lt-LT": 66, "lt-LT": 64,
"lv-LV": 99, "lv-LV": 98,
"mr-IN": 100, "mr-IN": 98,
"my-MM": 42, "my-MM": 41,
"nb-NO": 100, "nb-NO": 100,
"nl-NL": 91, "nl-NL": 91,
"nn-NO": 91, "nn-NO": 90,
"oc-FR": 98, "oc-FR": 98,
"pa-IN": 83, "pa-IN": 83,
"pl-PL": 85, "pl-PL": 85,
"pt-BR": 100, "pt-BR": 98,
"pt-PT": 98, "pt-PT": 100,
"ro-RO": 100, "ro-RO": 100,
"ru-RU": 100, "ru-RU": 98,
"si-LK": 8, "si-LK": 8,
"sk-SK": 99, "sk-SK": 100,
"sl-SI": 100, "sl-SI": 100,
"sv-SE": 98, "sv-SE": 96,
"ta-IN": 93, "ta-IN": 93,
"tr-TR": 98, "tr-TR": 98,
"uk-UA": 97, "uk-UA": 96,
"vi-VN": 20, "vi-VN": 20,
"zh-CN": 100, "zh-CN": 100,
"zh-HK": 26, "zh-HK": 26,

View File

@ -447,10 +447,16 @@
"d9480f": "Pomarańczowy 9" "d9480f": "Pomarańczowy 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Laranja 9" "d9480f": "Laranja 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Todos os dados são salvos localmente no seu navegador.", "app": {
"switchToPlusApp": "Você queria ir para o Excalidraw+ em vez disso?", "center_heading": "",
"menuHints": "Exportar, preferências, idiomas, ...", "center_heading_plus": "",
"toolbarHints": "Escolha uma ferramenta & Comece a desenhar!", "menuHint": ""
"helpHints": "Atalhos & ajuda" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"labels": { "labels": {
"paste": "Colar", "paste": "Colar",
"pasteAsPlaintext": "", "pasteAsPlaintext": "Colar como texto simples",
"pasteCharts": "Colar gráficos", "pasteCharts": "Colar gráficos",
"selectAll": "Selecionar tudo", "selectAll": "Selecionar tudo",
"multiSelect": "Adicionar elemento à seleção", "multiSelect": "Adicionar elemento à seleção",
@ -202,8 +202,8 @@
"invalidSVGString": "SVG inválido.", "invalidSVGString": "SVG inválido.",
"cannotResolveCollabServer": "Não foi possível fazer a ligação ao servidor colaborativo. Por favor, volte a carregar a página e tente novamente.", "cannotResolveCollabServer": "Não foi possível fazer a ligação ao servidor colaborativo. Por favor, volte a carregar a página e tente novamente.",
"importLibraryError": "Não foi possível carregar a biblioteca", "importLibraryError": "Não foi possível carregar a biblioteca",
"collabSaveFailed": "", "collabSaveFailed": "Não foi possível guardar na base de dados de backend. Se os problemas persistirem, guarde o ficheiro localmente para garantir que não perde o seu trabalho.",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "Não foi possível guardar na base de dados de backend, o ecrã parece estar muito grande. Deve guardar o ficheiro localmente para garantir que não perde o seu trabalho."
}, },
"toolBar": { "toolBar": {
"selection": "Seleção", "selection": "Seleção",
@ -238,7 +238,7 @@
"resize": "Pode restringir as proporções mantendo a tecla SHIFT premida enquanto redimensiona,\nmantenha a tecla ALT premida para redimensionar a partir do centro", "resize": "Pode restringir as proporções mantendo a tecla SHIFT premida enquanto redimensiona,\nmantenha a tecla ALT premida para redimensionar a partir do centro",
"resizeImage": "Pode redimensionar livremente mantendo pressionada a tecla SHIFT,\nmantenha pressionada a tecla ALT para redimensionar do centro", "resizeImage": "Pode redimensionar livremente mantendo pressionada a tecla SHIFT,\nmantenha pressionada a tecla ALT para redimensionar do centro",
"rotate": "Pode restringir os ângulos mantendo a tecla SHIFT premida enquanto roda", "rotate": "Pode restringir os ângulos mantendo a tecla SHIFT premida enquanto roda",
"lineEditor_info": "", "lineEditor_info": "Pressione CtrlOrCmd e faça um duplo-clique ou pressione CtrlOrCmd + Enter para editar pontos",
"lineEditor_pointSelected": "Carregue na tecla Delete para remover o(s) ponto(s), CtrlOuCmd+D para duplicar, ou arraste para mover", "lineEditor_pointSelected": "Carregue na tecla Delete para remover o(s) ponto(s), CtrlOuCmd+D para duplicar, ou arraste para mover",
"lineEditor_nothingSelected": "Seleccione um ponto para editar (carregue em SHIFT para seleccionar vários),\nou carregue em Alt e clique para acrescentar novos pontos", "lineEditor_nothingSelected": "Seleccione um ponto para editar (carregue em SHIFT para seleccionar vários),\nou carregue em Alt e clique para acrescentar novos pontos",
"placeImage": "Clique para colocar a imagem ou clique e arraste para definir o seu tamanho manualmente", "placeImage": "Clique para colocar a imagem ou clique e arraste para definir o seu tamanho manualmente",
@ -314,8 +314,8 @@
"zoomToFit": "Ajustar para todos os elementos caberem", "zoomToFit": "Ajustar para todos os elementos caberem",
"zoomToSelection": "Ampliar a seleção", "zoomToSelection": "Ampliar a seleção",
"toggleElementLock": "Trancar/destrancar selecção", "toggleElementLock": "Trancar/destrancar selecção",
"movePageUpDown": "", "movePageUpDown": "Mover página para cima / baixo",
"movePageLeftRight": "" "movePageLeftRight": "Mover página para esquerda / direita"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Apagar tela" "title": "Apagar tela"
@ -397,7 +397,7 @@
"fileSavedToFilename": "Guardado como {filename}", "fileSavedToFilename": "Guardado como {filename}",
"canvas": "área de desenho", "canvas": "área de desenho",
"selection": "seleção", "selection": "seleção",
"pasteAsSingleElement": "" "pasteAsSingleElement": "Usar {{shortcut}} para colar como um único elemento,\nou colar num editor de texto existente"
}, },
"colors": { "colors": {
"ffffff": "Branco", "ffffff": "Branco",
@ -447,10 +447,16 @@
"d9480f": "Laranja 9" "d9480f": "Laranja 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Todos os dados estão guardados no seu navegador local.", "app": {
"switchToPlusApp": "Queria antes ir para o Excalidraw+?", "center_heading": "Todos os dados são guardados no seu navegador local.",
"menuHints": "Exportar, preferências, idiomas...", "center_heading_plus": "Queria antes ir para o Excalidraw+?",
"toolbarHints": "Escolha uma ferramenta e comece a desenhar!", "menuHint": "Exportar, preferências, idiomas..."
"helpHints": "Atalhos e ajuda" },
"defaults": {
"menuHint": "Exportar, preferências e outros...",
"center_heading": "Diagramas. Feito. Simples.",
"toolbarHint": "Escolha uma ferramenta e comece a desenhar!",
"helpHint": "Atalhos e ajuda"
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Portocaliu 9" "d9480f": "Portocaliu 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Toate datele tale sunt salvate local în navigatorul tău.", "app": {
"switchToPlusApp": "Ai vrut să mergi în schimb la Excalidraw+?", "center_heading": "Toate datele tale sunt salvate local în navigatorul tău.",
"menuHints": "Exportare, preferințe, limbi, ...", "center_heading_plus": "Ai vrut să mergi în schimb la Excalidraw+?",
"toolbarHints": "Alege un instrument și începe să desenezi!", "menuHint": "Exportare, preferințe, limbi, ..."
"helpHints": "Comenzi rapide și ajutor" },
"defaults": {
"menuHint": "Exportare, preferințe și mai multe...",
"center_heading": "Diagrame. Făcute. Simple.",
"toolbarHint": "Alege un instrument și începe să desenezi!",
"helpHint": "Comenzi rapide și ajutor"
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Оранжевый 9" "d9480f": "Оранжевый 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Все ваши данные сохраняются локально в вашем браузере.", "app": {
"switchToPlusApp": "Хотите перейти на Excalidraw+?", "center_heading": "",
"menuHints": "Экспорт, настройки, языки, ...", "center_heading_plus": "",
"toolbarHints": "Выберите инструмент и начните рисовать!", "menuHint": ""
"helpHints": "Сочетания клавиш и помощь" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "" "d9480f": ""
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -202,8 +202,8 @@
"invalidSVGString": "Nevalidné SVG.", "invalidSVGString": "Nevalidné SVG.",
"cannotResolveCollabServer": "Nepodarilo sa pripojiť ku kolaboračnému serveru. Prosím obnovte stránku a skúste to znovu.", "cannotResolveCollabServer": "Nepodarilo sa pripojiť ku kolaboračnému serveru. Prosím obnovte stránku a skúste to znovu.",
"importLibraryError": "Nepodarilo sa načítať knižnicu", "importLibraryError": "Nepodarilo sa načítať knižnicu",
"collabSaveFailed": "", "collabSaveFailed": "Uloženie do databázy sa nepodarilo. Ak tento problém pretrváva uložte si váš súbor lokálne aby ste nestratili vašu prácu.",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "Uloženie do databázy sa nepodarilo, pretože veľkosť plátna je príliš veľká. Uložte si váš súbor lokálne aby ste nestratili vašu prácu."
}, },
"toolBar": { "toolBar": {
"selection": "Výber", "selection": "Výber",
@ -447,10 +447,16 @@
"d9480f": "Oranžová 9" "d9480f": "Oranžová 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Všetky vaše dáta sú uložené lokálne vo vašom prehliadači.", "app": {
"switchToPlusApp": "Chceli ste namiesto toho prejsť do Excalidraw+?", "center_heading": "Všetky vaše dáta sú uložené lokálne vo vašom prehliadači.",
"menuHints": "Exportovanie, nastavenia, jazyky, ...", "center_heading_plus": "Chceli ste namiesto toho prejsť do Excalidraw+?",
"toolbarHints": "Zvoľte nástroj a začnite kresliť!", "menuHint": "Exportovanie, nastavenia, jazyky, ..."
"helpHints": "Klávesové skratky a pomocník" },
"defaults": {
"menuHint": "Exportovanie, nastavenia a ďalšie...",
"center_heading": "Diagramy. Jednoducho.",
"toolbarHint": "Zvoľte nástroj a začnite kresliť!",
"helpHint": "Klávesové skratky a pomocník"
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Oranžna 9" "d9480f": "Oranžna 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Vsi vaši podatki so shranjeni lokalno v vašem brskalniku.", "app": {
"switchToPlusApp": "Ste namesto tega želeli odpreti Excalidraw+?", "center_heading": "Vsi vaši podatki so shranjeni lokalno v vašem brskalniku.",
"menuHints": "Izvoz, nastavitve, jeziki, ...", "center_heading_plus": "Ste namesto tega želeli odpreti Excalidraw+?",
"toolbarHints": "Izberi orodje in začni z risanjem!", "menuHint": "Izvoz, nastavitve, jeziki, ..."
"helpHints": "Bljižnice in pomoč" },
"defaults": {
"menuHint": "Izvoz, nastavitve in več ...",
"center_heading": "Diagrami. Enostavno.",
"toolbarHint": "Izberi orodje in začni z risanjem!",
"helpHint": "Bližnjice in pomoč"
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Orange 9" "d9480f": "Orange 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "All din data sparas lokalt i din webbläsare.", "app": {
"switchToPlusApp": "Ville du gå till Excalidraw+ istället?", "center_heading": "",
"menuHints": "Export, inställningar, språk, ...", "center_heading_plus": "",
"toolbarHints": "Välj ett verktyg och börja rita!", "menuHint": ""
"helpHints": "Genvägar och hjälp" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "ஆரஞ்சு 9" "d9480f": "ஆரஞ்சு 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -202,8 +202,8 @@
"invalidSVGString": "Geçersiz SVG.", "invalidSVGString": "Geçersiz SVG.",
"cannotResolveCollabServer": "İş birliği sunucusuna bağlanılamıyor. Lütfen sayfayı yenileyip tekrar deneyin.", "cannotResolveCollabServer": "İş birliği sunucusuna bağlanılamıyor. Lütfen sayfayı yenileyip tekrar deneyin.",
"importLibraryError": "Kütüphane yüklenemedi", "importLibraryError": "Kütüphane yüklenemedi",
"collabSaveFailed": "", "collabSaveFailed": "Backend veritabanına kaydedilemedi. Eğer problem devam ederse, çalışmanızı korumak için dosyayı yerel olarak kaydetmelisiniz.",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "Backend veritabanına kaydedilemedi; tuval çok büyük. Çalışmanızı korumak için dosyayı yerel olarak kaydetmelisiniz."
}, },
"toolBar": { "toolBar": {
"selection": "Seçme", "selection": "Seçme",
@ -314,8 +314,8 @@
"zoomToFit": "Tüm öğeleri sığdırmak için yakınlaştır", "zoomToFit": "Tüm öğeleri sığdırmak için yakınlaştır",
"zoomToSelection": "Seçime yakınlaş", "zoomToSelection": "Seçime yakınlaş",
"toggleElementLock": "Seçimi Kilitle/çöz", "toggleElementLock": "Seçimi Kilitle/çöz",
"movePageUpDown": "", "movePageUpDown": "Sayfayı yukarı/aşağı kaydır",
"movePageLeftRight": "" "movePageLeftRight": "Sayfayı sola/sağa kaydır"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Tuvali temizle" "title": "Tuvali temizle"
@ -397,7 +397,7 @@
"fileSavedToFilename": "{filename} kaydedildi", "fileSavedToFilename": "{filename} kaydedildi",
"canvas": "tuval", "canvas": "tuval",
"selection": "seçim", "selection": "seçim",
"pasteAsSingleElement": "" "pasteAsSingleElement": "Tekil obje olarak yapıştırmak için veya var olan bir metin editörüne yapıştırmak için {{shortcut}} kullanın"
}, },
"colors": { "colors": {
"ffffff": "Beyaz", "ffffff": "Beyaz",
@ -447,10 +447,16 @@
"d9480f": "Turuncu 9" "d9480f": "Turuncu 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "Tüm veri internet gezgininize yerel olarak kaydedildi.", "app": {
"switchToPlusApp": "Excalidraw+ kullanmak ister miydiniz?", "center_heading": "",
"menuHints": "Dışa aktar, seçenkeler, diller, ...", "center_heading_plus": "",
"toolbarHints": "Bir araç seçin & Çizime başlayın!", "menuHint": ""
"helpHints": "Kısayollar & yardım" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "Помаранчевий 9" "d9480f": "Помаранчевий 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "Оберіть інструмент і почніть малювати!", "menuHint": ""
"helpHints": "Комбінації клавіш і допомога" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "" "d9480f": ""
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "橙 9" "d9480f": "橙 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "您的所有数据都储存在浏览器本地。", "app": {
"switchToPlusApp": "是否前往 Excalidraw+ ", "center_heading": "您的所有数据都储存在浏览器本地。",
"menuHints": "导出、首选项、语言...", "center_heading_plus": "是否前往 Excalidraw+ ",
"toolbarHints": "选择工具并开始绘图!", "menuHint": "导出、首选项、语言……"
"helpHints": "快捷键和帮助" },
"defaults": {
"menuHint": "导出、首选项……",
"center_heading": "图,化繁为简。",
"toolbarHint": "选择工具并开始绘图!",
"helpHint": "快捷键和帮助"
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "" "d9480f": ""
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "", "app": {
"switchToPlusApp": "", "center_heading": "",
"menuHints": "", "center_heading_plus": "",
"toolbarHints": "", "menuHint": ""
"helpHints": "" },
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
} }
} }

View File

@ -447,10 +447,16 @@
"d9480f": "橘 9" "d9480f": "橘 9"
}, },
"welcomeScreen": { "welcomeScreen": {
"data": "您的所有資料都儲存在本機瀏覽器。", "app": {
"switchToPlusApp": "您是否是要前往 Excalidraw+ ", "center_heading": "所有資料皆已在瀏覽器中儲存於本機",
"menuHints": "輸出、偏好設定、語言...", "center_heading_plus": "您是否是要前往 Excalidraw+ ",
"toolbarHints": "選個工具開始畫圖吧!", "menuHint": "輸出、偏好設定、語言..."
"helpHints": "快速鍵與說明" },
"defaults": {
"menuHint": "輸出、偏好設定及其他...",
"center_heading": "圖表。製作。超簡單。",
"toolbarHint": "選個工具開始畫圖吧!",
"helpHint": "快速鍵與說明"
}
} }
} }

View File

@ -11,6 +11,12 @@ The change should be grouped under one of the below section and must contain PR
Please add the latest change on the top under the correct section. Please add the latest change on the top under the correct section.
--> -->
## Unreleased
### Features
- `MainMenu`, `MainMenu.Item`, and `MainMenu.ItemLink` components now all support `onSelect(event: Event): void` callback. If you call `event.preventDefault()`, it will prevent the menu from closing when an item is selected (clicked on). [#6152](https://github.com/excalidraw/excalidraw/pull/6152)
## 0.14.1 (2023-01-16) ## 0.14.1 (2023-01-16)
### Fixes ### Fixes

View File

@ -4,7 +4,7 @@ exports[`contextMenu element right-clicking on a group should select whole group
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -434,7 +434,7 @@ exports[`contextMenu element selecting 'Add to library' in context menu adds ele
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -620,7 +620,7 @@ exports[`contextMenu element selecting 'Bring forward' in context menu brings el
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -975,7 +975,7 @@ exports[`contextMenu element selecting 'Bring to front' in context menu brings e
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -1330,7 +1330,7 @@ exports[`contextMenu element selecting 'Copy styles' in context menu copies styl
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -1516,7 +1516,7 @@ exports[`contextMenu element selecting 'Delete' in context menu deletes element:
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -1738,7 +1738,7 @@ exports[`contextMenu element selecting 'Duplicate' in context menu duplicates el
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -2023,7 +2023,7 @@ exports[`contextMenu element selecting 'Group selection' in context menu groups
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -2396,7 +2396,7 @@ exports[`contextMenu element selecting 'Paste styles' in context menu pastes sty
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -3243,7 +3243,7 @@ exports[`contextMenu element selecting 'Send backward' in context menu sends ele
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -3598,7 +3598,7 @@ exports[`contextMenu element selecting 'Send to back' in context menu sends elem
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -3953,7 +3953,7 @@ exports[`contextMenu element selecting 'Ungroup selection' in context menu ungro
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -4392,7 +4392,7 @@ exports[`contextMenu element shows 'Group selection' in context menu for multipl
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -4933,7 +4933,7 @@ exports[`contextMenu element shows 'Ungroup selection' in context menu for group
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -5559,7 +5559,7 @@ exports[`contextMenu element shows context menu for canvas: [end of test] appSta
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -5773,7 +5773,7 @@ exports[`contextMenu element shows context menu for element: [end of test] appSt
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -6110,7 +6110,7 @@ exports[`contextMenu element shows context menu for element: [end of test] appSt
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },

View File

@ -4,7 +4,7 @@ exports[`given element A and group of elements B and given both are selected whe
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -540,7 +540,7 @@ exports[`given element A and group of elements B and given both are selected whe
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -1082,7 +1082,7 @@ exports[`regression tests Cmd/Ctrl-click exclusively select element under pointe
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -1989,7 +1989,7 @@ exports[`regression tests Drags selected element when hitting only bounding box
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -2219,7 +2219,7 @@ exports[`regression tests adjusts z order when grouping: [end of test] appState
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -2752,7 +2752,7 @@ exports[`regression tests alt-drag duplicates an element: [end of test] appState
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -3041,7 +3041,7 @@ exports[`regression tests arrow keys: [end of test] appState 1`] = `
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -3225,7 +3225,7 @@ exports[`regression tests can drag element that covers another element, while an
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -3741,7 +3741,7 @@ exports[`regression tests change the properties of a shape: [end of test] appSta
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -4009,7 +4009,7 @@ exports[`regression tests click on an element and drag it: [dragged] appState 1`
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -4239,7 +4239,7 @@ exports[`regression tests click on an element and drag it: [end of test] appStat
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -4515,7 +4515,7 @@ exports[`regression tests click to select a shape: [end of test] appState 1`] =
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -4803,7 +4803,7 @@ exports[`regression tests click-drag to select a group: [end of test] appState 1
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -5221,7 +5221,7 @@ exports[`regression tests deselects group of selected elements on pointer down w
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -5562,7 +5562,7 @@ exports[`regression tests deselects group of selected elements on pointer up whe
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -5876,7 +5876,7 @@ exports[`regression tests deselects selected element on pointer down when pointe
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -6114,7 +6114,7 @@ exports[`regression tests deselects selected element, on pointer up, when click
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -6300,7 +6300,7 @@ exports[`regression tests double click to edit a group: [end of test] appState 1
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -6828,7 +6828,7 @@ exports[`regression tests drags selected elements from point inside common bound
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -7193,7 +7193,7 @@ exports[`regression tests draw every type of shape: [end of test] appState 1`] =
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "freedraw", "type": "freedraw",
}, },
@ -9545,7 +9545,7 @@ exports[`regression tests given a group of selected elements with an element tha
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -9964,7 +9964,7 @@ exports[`regression tests given a selected element A and a not selected element
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -10253,7 +10253,7 @@ exports[`regression tests given selected element A with lower z-index than unsel
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -10501,7 +10501,7 @@ exports[`regression tests given selected element A with lower z-index than unsel
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -10822,7 +10822,7 @@ exports[`regression tests key 2 selects rectangle tool: [end of test] appState 1
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -11006,7 +11006,7 @@ exports[`regression tests key 3 selects diamond tool: [end of test] appState 1`]
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -11190,7 +11190,7 @@ exports[`regression tests key 4 selects ellipse tool: [end of test] appState 1`]
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -11374,7 +11374,7 @@ exports[`regression tests key 5 selects arrow tool: [end of test] appState 1`] =
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -11611,7 +11611,7 @@ exports[`regression tests key 6 selects line tool: [end of test] appState 1`] =
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -11848,7 +11848,7 @@ exports[`regression tests key 7 selects freedraw tool: [end of test] appState 1`
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "freedraw", "type": "freedraw",
}, },
@ -12076,7 +12076,7 @@ exports[`regression tests key a selects arrow tool: [end of test] appState 1`] =
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -12313,7 +12313,7 @@ exports[`regression tests key d selects diamond tool: [end of test] appState 1`]
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -12497,7 +12497,7 @@ exports[`regression tests key l selects line tool: [end of test] appState 1`] =
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -12734,7 +12734,7 @@ exports[`regression tests key o selects ellipse tool: [end of test] appState 1`]
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -12918,7 +12918,7 @@ exports[`regression tests key p selects freedraw tool: [end of test] appState 1`
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "freedraw", "type": "freedraw",
}, },
@ -13146,7 +13146,7 @@ exports[`regression tests key r selects rectangle tool: [end of test] appState 1
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -13330,7 +13330,7 @@ exports[`regression tests make a group and duplicate it: [end of test] appState
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -14169,7 +14169,7 @@ exports[`regression tests noop interaction after undo shouldn't create history e
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -14458,7 +14458,7 @@ exports[`regression tests pinch-to-zoom works: [end of test] appState 1`] = `
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -14569,7 +14569,7 @@ exports[`regression tests rerenders UI on language change: [end of test] appStat
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "rectangle", "type": "rectangle",
}, },
@ -14678,7 +14678,7 @@ exports[`regression tests shift click on selected element should deselect it on
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -14865,7 +14865,7 @@ exports[`regression tests shift-click to multiselect, then drag: [end of test] a
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -15233,7 +15233,7 @@ exports[`regression tests should group elements and ungroup them: [end of test]
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -15864,7 +15864,7 @@ exports[`regression tests should show fill icons when element has non transparen
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -16090,7 +16090,7 @@ exports[`regression tests single-clicking on a subgroup of a selected group shou
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -17053,7 +17053,7 @@ exports[`regression tests spacebar + drag scrolls the canvas: [end of test] appS
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -17162,7 +17162,7 @@ exports[`regression tests supports nested groups: [end of test] appState 1`] = `
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -18021,7 +18021,7 @@ exports[`regression tests switches from group of selected elements to another el
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -18493,7 +18493,7 @@ exports[`regression tests switches selected element on pointer down: [end of tes
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -18834,7 +18834,7 @@ exports[`regression tests two-finger scroll works: [end of test] appState 1`] =
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -18945,7 +18945,7 @@ exports[`regression tests undo/redo drawing an element: [end of test] appState 1
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },
@ -19516,7 +19516,7 @@ exports[`regression tests updates fontSize & fontFamily appState: [end of test]
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "text", "type": "text",
}, },
@ -19625,7 +19625,7 @@ exports[`regression tests zoom hotkeys: [end of test] appState 1`] = `
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },

View File

@ -4,7 +4,7 @@ exports[`exportToSvg with default arguments 1`] = `
Object { Object {
"activeTool": Object { "activeTool": Object {
"customType": null, "customType": null,
"lastActiveToolBeforeEraser": null, "lastActiveTool": null,
"locked": false, "locked": false,
"type": "selection", "type": "selection",
}, },

View File

@ -81,9 +81,9 @@ export type BinaryFileMetadata = Omit<BinaryFileData, "dataURL">;
export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>; export type BinaryFiles = Record<ExcalidrawElement["id"], BinaryFileData>;
export type LastActiveToolBeforeEraser = export type LastActiveTool =
| { | {
type: typeof SHAPES[number]["value"] | "eraser"; type: typeof SHAPES[number]["value"] | "eraser" | "hand";
customType: null; customType: null;
} }
| { | {
@ -112,19 +112,23 @@ export type AppState = {
// (e.g. text element when typing into the input) // (e.g. text element when typing into the input)
editingElement: NonDeletedExcalidrawElement | null; editingElement: NonDeletedExcalidrawElement | null;
editingLinearElement: LinearElementEditor | null; editingLinearElement: LinearElementEditor | null;
activeTool: activeTool: {
| { /**
type: typeof SHAPES[number]["value"] | "eraser"; * indicates a previous tool we should revert back to if we deselect the
lastActiveToolBeforeEraser: LastActiveToolBeforeEraser; * currently active tool. At the moment applies to `eraser` and `hand` tool.
*/
lastActiveTool: LastActiveTool;
locked: boolean; locked: boolean;
} & (
| {
type: typeof SHAPES[number]["value"] | "eraser" | "hand";
customType: null; customType: null;
} }
| { | {
type: "custom"; type: "custom";
customType: string; customType: string;
lastActiveToolBeforeEraser: LastActiveToolBeforeEraser; }
locked: boolean; );
};
penMode: boolean; penMode: boolean;
penDetected: boolean; penDetected: boolean;
exportBackground: boolean; exportBackground: boolean;

View File

@ -6,16 +6,17 @@ import {
DEFAULT_VERSION, DEFAULT_VERSION,
EVENT, EVENT,
FONT_FAMILY, FONT_FAMILY,
isDarwin,
MIME_TYPES, MIME_TYPES,
THEME, THEME,
WINDOWS_EMOJI_FALLBACK_FONT, WINDOWS_EMOJI_FALLBACK_FONT,
} from "./constants"; } from "./constants";
import { FontFamilyValues, FontString } from "./element/types"; import { FontFamilyValues, FontString } from "./element/types";
import { AppState, DataURL, LastActiveToolBeforeEraser, Zoom } from "./types"; import { AppState, DataURL, LastActiveTool, Zoom } from "./types";
import { unstable_batchedUpdates } from "react-dom"; import { unstable_batchedUpdates } from "react-dom";
import { isDarwin } from "./keys";
import { SHAPES } from "./shapes"; import { SHAPES } from "./shapes";
import React from "react"; import React from "react";
import { isEraserActive, isHandToolActive } from "./appState";
let mockDateTime: string | null = null; let mockDateTime: string | null = null;
@ -219,9 +220,9 @@ export const distance = (x: number, y: number) => Math.abs(x - y);
export const updateActiveTool = ( export const updateActiveTool = (
appState: Pick<AppState, "activeTool">, appState: Pick<AppState, "activeTool">,
data: ( data: (
| { type: typeof SHAPES[number]["value"] | "eraser" } | { type: typeof SHAPES[number]["value"] | "eraser" | "hand" }
| { type: "custom"; customType: string } | { type: "custom"; customType: string }
) & { lastActiveToolBeforeEraser?: LastActiveToolBeforeEraser }, ) & { lastActiveToolBeforeEraser?: LastActiveTool },
): AppState["activeTool"] => { ): AppState["activeTool"] => {
if (data.type === "custom") { if (data.type === "custom") {
return { return {
@ -233,9 +234,9 @@ export const updateActiveTool = (
return { return {
...appState.activeTool, ...appState.activeTool,
lastActiveToolBeforeEraser: lastActiveTool:
data.lastActiveToolBeforeEraser === undefined data.lastActiveToolBeforeEraser === undefined
? appState.activeTool.lastActiveToolBeforeEraser ? appState.activeTool.lastActiveTool
: data.lastActiveToolBeforeEraser, : data.lastActiveToolBeforeEraser,
type: data.type, type: data.type,
customType: null, customType: null,
@ -305,7 +306,9 @@ export const setCursorForShape = (
} }
if (appState.activeTool.type === "selection") { if (appState.activeTool.type === "selection") {
resetCursor(canvas); resetCursor(canvas);
} else if (appState.activeTool.type === "eraser") { } else if (isHandToolActive(appState)) {
canvas.style.cursor = CURSOR_TYPE.GRAB;
} else if (isEraserActive(appState)) {
setEraserCursor(canvas, appState.theme); setEraserCursor(canvas, appState.theme);
// do nothing if image tool is selected which suggests there's // do nothing if image tool is selected which suggests there's
// a image-preview set as the cursor // a image-preview set as the cursor
@ -740,3 +743,22 @@ export const isShallowEqual = <T extends Record<string, any>>(
} }
return aKeys.every((key) => objA[key] === objB[key]); return aKeys.every((key) => objA[key] === objB[key]);
}; };
// taken from Radix UI
// https://github.com/radix-ui/primitives/blob/main/packages/core/primitive/src/primitive.tsx
export const composeEventHandlers = <E>(
originalEventHandler?: (event: E) => void,
ourEventHandler?: (event: E) => void,
{ checkForDefaultPrevented = true } = {},
) => {
return function handleEvent(event: E) {
originalEventHandler?.(event);
if (
!checkForDefaultPrevented ||
!(event as unknown as Event).defaultPrevented
) {
return ourEventHandler?.(event);
}
};
};