Compare commits

..

17 Commits

Author SHA1 Message Date
c1b903395a wip: hitbox test visualizer 2023-02-15 16:40:10 +01:00
96b172ebfa support rotation 2023-02-15 17:36:46 +05:30
16db74cedd support ellipse 2023-02-15 15:20:12 +05:30
4d1b31a171 feat: bind text to container when clicked on filled shape or element stroke 2023-02-15 13:32:47 +05:30
0d7ee891e0 feat: Make repair and refreshDimensions configurable in restoreElements (#6238)
* fix: don't repair during reconcilation

* Add opts to restoreElement and enable refreshDimensions and repair via config

* remove

* update changelog

* fix tests

* rename to repairBindings
2023-02-15 10:41:11 +05:30
71fb60394a docs: enable Algolia for search (#6230) 2023-02-13 17:39:11 +05:30
c9d18ecab6 fix: don't allow blank space in collab name (#6211)
* don't allow blank space in collab name

* add spec

* prevent blank
2023-02-09 15:51:49 +05:30
8c1168ef33 refactor: Make the example React app reusable without duplication (#6188) 2023-02-07 12:41:20 +05:30
c3c45a8c37 fix: docker build architecture:linux/amd64 error occur on linux/arm64 instance (#6197)
fix docker build
when in linux/arm64 use docker buildx plugin to build linux/amd64 image, a build error will occur causing the build to break
2023-02-07 11:44:31 +05:30
a8e6028c33 feat: show error message when not connected to internet while collabo… (#6165)
Co-authored-by: dwelle <luzar.david@gmail.com>
Resolves https://github.com/excalidraw/excalidraw/issues/5994
2023-02-04 15:03:39 +01:00
11e2f90ca1 feat: shortcut for clearCanvas confirmDialog (#6114)
Co-authored-by: dwelle <luzar.david@gmail.com>
resolve https://github.com/excalidraw/excalidraw/issues/5818
2023-02-04 13:33:40 +01:00
4db87a0b6a chore: Update translations from Crowdin (#6150) 2023-02-04 10:04:15 +01:00
4414069617 feat: disable canvas smoothing (antialiasing) for right-angled elements (#6186)Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>
* feat: disable canvas smoothing for text and other types

* disable smoothing for all right-angled elements

* Update src/renderer/renderElement.ts

Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>

* Update src/renderer/renderElement.ts

Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>

* fix lint

* always enable smoothing while zooming

---------

Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>
2023-02-03 17:07:14 +01:00
a9c5bdb878 fix: sort bound text elements to fix text duplication z-index error (#5130)
* fix: sort bound text elements to fix text duplication z-index error

* improve & sort groups & add tests

* fix backtracking and discontiguous groups

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-02-02 16:23:39 +08:00
5a0334f37f fix: hide welcome screen on mobile once user interacts (#6185)
* fix: hide welcome screen on mobile once started drawing

* Add specs
2023-02-02 12:58:45 +05:30
d8a4ca6911 docs: show last updated time and author (#6183)
docs:show last updated time and author
2023-02-01 21:09:23 +05:30
eb9eeefc63 fix: edit link in docs (#6182) 2023-02-01 20:27:31 +05:30
90 changed files with 1801 additions and 420 deletions

View File

@ -3,7 +3,7 @@ FROM node:14-alpine AS build
WORKDIR /opt/node_app
COPY package.json yarn.lock ./
RUN yarn --ignore-optional
RUN yarn --ignore-optional --network-timeout 600000
ARG NODE_ENV=production

View File

@ -32,6 +32,8 @@ const config = {
// Please change this to your repo.
editUrl:
"https://github.com/excalidraw/excalidraw/tree/master/dev-docs/",
showLastUpdateAuthor: true,
showLastUpdateTime: true,
},
theme: {
customCss: [
@ -130,6 +132,11 @@ const config = {
tableOfContents: {
maxHeadingLevel: 4,
},
algolia: {
appId: "8FEAOD28DI",
apiKey: "4b07cca33ff2d2919bc95ff98f148e9e",
indexName: "excalidraw",
},
}),
themes: ["@docusaurus/theme-live-codeblock"],
plugins: ["docusaurus-plugin-sass"],

View File

@ -154,7 +154,9 @@ export const actionDeleteSelected = register({
};
},
contextItemLabel: "labels.delete",
keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
keyTest: (event, appState, elements) =>
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) &&
!event[KEYS.CTRL_OR_CMD],
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
type="button"

View File

@ -16,8 +16,12 @@ import { AppState } from "../types";
import { fixBindingsAfterDuplication } from "../element/binding";
import { ActionResult } from "./types";
import { GRID_SIZE } from "../constants";
import { bindTextToShapeAfterDuplication } from "../element/textElement";
import {
bindTextToShapeAfterDuplication,
getBoundTextElement,
} from "../element/textElement";
import { isBoundToContainer } from "../element/typeChecks";
import { normalizeElementOrder } from "../element/sortElements";
import { DuplicateIcon } from "../components/icons";
export const actionDuplicateSelection = register({
@ -64,6 +68,11 @@ const duplicateElements = (
elements: readonly ExcalidrawElement[],
appState: AppState,
): Partial<ActionResult> => {
// ---------------------------------------------------------------------------
// step (1)
const sortedElements = normalizeElementOrder(elements);
const groupIdMap = new Map();
const newElements: ExcalidrawElement[] = [];
const oldElements: ExcalidrawElement[] = [];
@ -85,42 +94,112 @@ const duplicateElements = (
return newElement;
};
const finalElements: ExcalidrawElement[] = [];
let index = 0;
const selectedElementIds = arrayToMap(
getSelectedElements(elements, appState, true),
getSelectedElements(sortedElements, appState, true),
);
while (index < elements.length) {
const element = elements[index];
// Ids of elements that have already been processed so we don't push them
// into the array twice if we end up backtracking when retrieving
// discontiguous group of elements (can happen due to a bug, or in edge
// cases such as a group containing deleted elements which were not selected).
//
// This is not enough to prevent duplicates, so we do a second loop afterwards
// to remove them.
//
// For convenience we mark even the newly created ones even though we don't
// loop over them.
const processedIds = new Map<ExcalidrawElement["id"], true>();
const markAsProcessed = (elements: ExcalidrawElement[]) => {
for (const element of elements) {
processedIds.set(element.id, true);
}
return elements;
};
const elementsWithClones: ExcalidrawElement[] = [];
let index = -1;
while (++index < sortedElements.length) {
const element = sortedElements[index];
if (processedIds.get(element.id)) {
continue;
}
const boundTextElement = getBoundTextElement(element);
if (selectedElementIds.get(element.id)) {
if (element.groupIds.length) {
// if a group or a container/bound-text, duplicate atomically
if (element.groupIds.length || boundTextElement) {
const groupId = getSelectedGroupForElement(appState, element);
// if group selected, duplicate it atomically
if (groupId) {
const groupElements = getElementsInGroup(elements, groupId);
finalElements.push(
...groupElements,
...groupElements.map((element) =>
duplicateAndOffsetElement(element),
),
const groupElements = getElementsInGroup(sortedElements, groupId);
elementsWithClones.push(
...markAsProcessed([
...groupElements,
...groupElements.map((element) =>
duplicateAndOffsetElement(element),
),
]),
);
continue;
}
if (boundTextElement) {
elementsWithClones.push(
...markAsProcessed([
element,
boundTextElement,
duplicateAndOffsetElement(element),
duplicateAndOffsetElement(boundTextElement),
]),
);
index = index + groupElements.length;
continue;
}
}
finalElements.push(element, duplicateAndOffsetElement(element));
elementsWithClones.push(
...markAsProcessed([element, duplicateAndOffsetElement(element)]),
);
} else {
finalElements.push(element);
elementsWithClones.push(...markAsProcessed([element]));
}
index++;
}
// step (2)
// second pass to remove duplicates. We loop from the end as it's likelier
// that the last elements are in the correct order (contiguous or otherwise).
// Thus we need to reverse as the last step (3).
const finalElementsReversed: ExcalidrawElement[] = [];
const finalElementIds = new Map<ExcalidrawElement["id"], true>();
index = elementsWithClones.length;
while (--index >= 0) {
const element = elementsWithClones[index];
if (!finalElementIds.get(element.id)) {
finalElementIds.set(element.id, true);
finalElementsReversed.push(element);
}
}
// step (3)
const finalElements = finalElementsReversed.reverse();
// ---------------------------------------------------------------------------
bindTextToShapeAfterDuplication(
finalElements,
elementsWithClones,
oldElements,
oldIdToDuplicatedId,
);
fixBindingsAfterDuplication(
elementsWithClones,
oldElements,
oldIdToDuplicatedId,
);
fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId);
return {
elements: finalElements,

View File

@ -8,6 +8,7 @@ export type ShortcutName =
ActionName,
| "toggleTheme"
| "loadScene"
| "clearCanvas"
| "cut"
| "copy"
| "paste"
@ -41,6 +42,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
toggleTheme: [getShortcutKey("Shift+Alt+D")],
saveScene: [getShortcutKey("CtrlOrCmd+S")],
loadScene: [getShortcutKey("CtrlOrCmd+O")],
clearCanvas: [getShortcutKey("CtrlOrCmd+Delete")],
imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")],
cut: [getShortcutKey("CtrlOrCmd+X")],
copy: [getShortcutKey("CtrlOrCmd+C")],

View File

@ -21,7 +21,7 @@ export const getClientColors = (clientId: string, appState: AppState) => {
};
export const getClientInitials = (userName?: string | null) => {
if (!userName) {
if (!userName?.trim()) {
return "?";
}
return userName.trim()[0].toUpperCase();

View File

@ -226,6 +226,7 @@ import {
setEraserCursor,
updateActiveTool,
getShortcutKey,
isTransparent,
} from "../utils";
import {
ContextMenu,
@ -264,6 +265,7 @@ import {
getContainerCenter,
getContainerDims,
getTextBindableContainerAtPosition,
isHittingContainerStroke,
isValidTextContainer,
} from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
@ -279,6 +281,8 @@ import { shouldShowBoundingBox } from "../element/transformHandles";
import { Fonts } from "../scene/Fonts";
import { actionPaste } from "../actions/actionClipboard";
import { actionToggleHandTool } from "../actions/actionCanvas";
import { jotaiStore } from "../jotai";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
const deviceContextInitialValue = {
isSmScreen: false,
@ -834,7 +838,7 @@ class App extends React.Component<AppProps, AppState> {
},
};
}
const scene = restore(initialData, null, null);
const scene = restore(initialData, null, null, { repairBindings: true });
scene.appState = {
...scene.appState,
theme: this.props.theme || scene.appState.theme,
@ -1952,7 +1956,6 @@ class App extends React.Component<AppProps, AppState> {
);
// Input handling
private onKeyDown = withBatchedUpdates(
(event: React.KeyboardEvent | KeyboardEvent) => {
// normalize `event.key` when CapsLock is pressed #2372
@ -2194,6 +2197,13 @@ class App extends React.Component<AppProps, AppState> {
event.stopPropagation();
}
}
if (
event[KEYS.CTRL_OR_CMD] &&
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
) {
jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
}
},
);
@ -2754,7 +2764,19 @@ class App extends React.Component<AppProps, AppState> {
sceneY,
);
if (container) {
if (isArrowElement(container) || hasBoundTextElement(container)) {
if (
isArrowElement(container) ||
hasBoundTextElement(container) ||
!isTransparent(
(container as ExcalidrawTextContainer).backgroundColor,
) ||
isHittingContainerStroke(
sceneX,
sceneY,
container,
this.state.zoom.value,
)
) {
const midPoint = getContainerCenter(container, this.state);
sceneX = midPoint.x;

View File

@ -273,22 +273,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
className="HelpDialog__island--editor"
caption={t("helpDialog.editor")}
>
<Shortcut
label={t("labels.selectAll")}
shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
/>
<Shortcut
label={t("labels.multiSelect")}
shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
/>
<Shortcut
label={t("helpDialog.deepSelect")}
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]}
/>
<Shortcut
label={t("helpDialog.deepBoxSelect")}
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]}
/>
<Shortcut
label={t("labels.moveCanvas")}
shortcuts={[
@ -297,6 +281,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
]}
isOr={true}
/>
<Shortcut
label={t("buttons.clearReset")}
shortcuts={[getShortcutKey("CtrlOrCmd+Delete")]}
/>
<Shortcut
label={t("labels.delete")}
shortcuts={[getShortcutKey("Delete")]}
/>
<Shortcut
label={t("labels.cut")}
shortcuts={[getShortcutKey("CtrlOrCmd+X")]}
@ -313,6 +305,22 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.pasteAsPlaintext")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
/>
<Shortcut
label={t("labels.selectAll")}
shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
/>
<Shortcut
label={t("labels.multiSelect")}
shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
/>
<Shortcut
label={t("helpDialog.deepSelect")}
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]}
/>
<Shortcut
label={t("helpDialog.deepBoxSelect")}
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]}
/>
{/* firefox supports clipboard API under a flag, so we'll
show users what they can do in the error message */}
{(probablySupportsClipboardBlob || isFirefox) && (
@ -329,10 +337,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.pasteStyles")}
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
/>
<Shortcut
label={t("labels.delete")}
shortcuts={[getShortcutKey("Delete")]}
/>
<Shortcut
label={t("labels.sendToBack")}
shortcuts={[

View File

@ -124,7 +124,6 @@ const LayerUI = ({
children,
}: LayerUIProps) => {
const device = useDevice();
const tunnels = useInitializeTunnels();
const renderJSONExportDialog = () => {
@ -409,6 +408,7 @@ const LayerUI = ({
renderCustomStats={renderCustomStats}
renderSidebars={renderSidebars}
device={device}
renderWelcomeScreen={renderWelcomeScreen}
/>
)}

View File

@ -41,6 +41,7 @@ type MobileMenuProps = {
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderSidebars: () => JSX.Element | null;
device: Device;
renderWelcomeScreen: boolean;
};
export const MobileMenu = ({
@ -57,12 +58,13 @@ export const MobileMenu = ({
renderCustomStats,
renderSidebars,
device,
renderWelcomeScreen,
}: MobileMenuProps) => {
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels();
const renderToolbar = () => {
return (
<FixedSideContainer side="top" className="App-top-bar">
<welcomeScreenCenterTunnel.Out />
{renderWelcomeScreen && <welcomeScreenCenterTunnel.Out />}
<Section heading="shapes">
{(heading: React.ReactNode) => (
<Stack.Col gap={4} align="center">

View File

@ -95,6 +95,9 @@
--color-gray-90: #1e1e1e;
--color-gray-100: #121212;
--color-warning: #fceeca;
--color-text-warning: var(--text-primary-color);
--color-danger: #db6965;
--color-promo: #e70078;
@ -163,6 +166,8 @@
--color-primary-darkest: #beb9ff;
--color-primary-light: #4f4d6f;
--color-text-warning: var(--color-gray-80);
--color-danger: #ffa8a5;
--color-promo: #d297ff;
}

View File

@ -156,6 +156,7 @@ export const loadSceneOrLibraryFromBlob = async (
},
localAppState,
localElements,
{ repairBindings: true },
),
};
} else if (isValidLibrary(data)) {

View File

@ -339,7 +339,7 @@ export const restoreElements = (
elements: ImportedDataState["elements"],
/** NOTE doesn't serve for reconciliation */
localElements: readonly ExcalidrawElement[] | null | undefined,
refreshDimensions = false,
opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined,
): ExcalidrawElement[] => {
const localElementsMap = localElements ? arrayToMap(localElements) : null;
const restoredElements = (elements || []).reduce((elements, element) => {
@ -348,7 +348,7 @@ export const restoreElements = (
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
let migratedElement: ExcalidrawElement | null = restoreElement(
element,
refreshDimensions,
opts?.refreshDimensions,
);
if (migratedElement) {
const localElement = localElementsMap?.get(element.id);
@ -361,6 +361,10 @@ export const restoreElements = (
return elements;
}, [] as ExcalidrawElement[]);
if (!opts?.repairBindings) {
return restoredElements;
}
// repair binding. Mutates elements.
const restoredElementsMap = arrayToMap(restoredElements);
for (const element of restoredElements) {
@ -497,9 +501,10 @@ export const restore = (
*/
localAppState: Partial<AppState> | null | undefined,
localElements: readonly ExcalidrawElement[] | null | undefined,
elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
): RestoredDataState => {
return {
elements: restoreElements(data?.elements, localElements),
elements: restoreElements(data?.elements, localElements, elementsConfig),
appState: restoreAppState(data?.appState, localAppState || null),
files: data?.files || {},
};

View File

@ -0,0 +1,402 @@
import { API } from "../tests/helpers/api";
import { mutateElement } from "./mutateElement";
import { normalizeElementOrder } from "./sortElements";
import { ExcalidrawElement } from "./types";
const assertOrder = (
elements: readonly ExcalidrawElement[],
expectedOrder: string[],
) => {
const actualOrder = elements.map((element) => element.id);
expect(actualOrder).toEqual(expectedOrder);
};
describe("normalizeElementsOrder", () => {
it("sort bound-text elements", () => {
const container = API.createElement({
id: "container",
type: "rectangle",
});
const boundText = API.createElement({
id: "boundText",
type: "text",
containerId: container.id,
});
const otherElement = API.createElement({
id: "otherElement",
type: "rectangle",
boundElements: [],
});
const otherElement2 = API.createElement({
id: "otherElement2",
type: "rectangle",
boundElements: [],
});
mutateElement(container, {
boundElements: [{ type: "text", id: boundText.id }],
});
assertOrder(normalizeElementOrder([container, boundText]), [
"container",
"boundText",
]);
assertOrder(normalizeElementOrder([boundText, container]), [
"container",
"boundText",
]);
assertOrder(
normalizeElementOrder([
boundText,
container,
otherElement,
otherElement2,
]),
["container", "boundText", "otherElement", "otherElement2"],
);
assertOrder(normalizeElementOrder([container, otherElement, boundText]), [
"container",
"boundText",
"otherElement",
]);
assertOrder(
normalizeElementOrder([
container,
otherElement,
otherElement2,
boundText,
]),
["container", "boundText", "otherElement", "otherElement2"],
);
assertOrder(
normalizeElementOrder([
boundText,
otherElement,
container,
otherElement2,
]),
["otherElement", "container", "boundText", "otherElement2"],
);
// noop
assertOrder(
normalizeElementOrder([
otherElement,
container,
boundText,
otherElement2,
]),
["otherElement", "container", "boundText", "otherElement2"],
);
// text has existing containerId, but container doesn't list is
// as a boundElement
assertOrder(
normalizeElementOrder([
API.createElement({
id: "boundText",
type: "text",
containerId: "container",
}),
API.createElement({
id: "container",
type: "rectangle",
}),
]),
["boundText", "container"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "boundText",
type: "text",
containerId: "container",
}),
]),
["boundText"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "container",
type: "rectangle",
boundElements: [],
}),
]),
["container"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "container",
type: "rectangle",
boundElements: [{ id: "x", type: "text" }],
}),
]),
["container"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "arrow",
type: "arrow",
}),
API.createElement({
id: "container",
type: "rectangle",
boundElements: [{ id: "arrow", type: "arrow" }],
}),
]),
["arrow", "container"],
);
});
it("normalize group order", () => {
assertOrder(
normalizeElementOrder([
API.createElement({
id: "A_rect1",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "rect2",
type: "rectangle",
}),
API.createElement({
id: "rect3",
type: "rectangle",
}),
API.createElement({
id: "A_rect4",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "A_rect5",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "rect6",
type: "rectangle",
}),
API.createElement({
id: "A_rect7",
type: "rectangle",
groupIds: ["A"],
}),
]),
["A_rect1", "A_rect4", "A_rect5", "A_rect7", "rect2", "rect3", "rect6"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "A_rect1",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "rect2",
type: "rectangle",
}),
API.createElement({
id: "B_rect3",
type: "rectangle",
groupIds: ["B"],
}),
API.createElement({
id: "A_rect4",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "B_rect5",
type: "rectangle",
groupIds: ["B"],
}),
API.createElement({
id: "rect6",
type: "rectangle",
}),
API.createElement({
id: "A_rect7",
type: "rectangle",
groupIds: ["A"],
}),
]),
["A_rect1", "A_rect4", "A_rect7", "rect2", "B_rect3", "B_rect5", "rect6"],
);
// nested groups
assertOrder(
normalizeElementOrder([
API.createElement({
id: "A_rect1",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "BA_rect2",
type: "rectangle",
groupIds: ["B", "A"],
}),
]),
["A_rect1", "BA_rect2"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "BA_rect1",
type: "rectangle",
groupIds: ["B", "A"],
}),
API.createElement({
id: "A_rect2",
type: "rectangle",
groupIds: ["A"],
}),
]),
["BA_rect1", "A_rect2"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "BA_rect1",
type: "rectangle",
groupIds: ["B", "A"],
}),
API.createElement({
id: "A_rect2",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "CBA_rect3",
type: "rectangle",
groupIds: ["C", "B", "A"],
}),
API.createElement({
id: "rect4",
type: "rectangle",
}),
API.createElement({
id: "A_rect5",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "BA_rect5",
type: "rectangle",
groupIds: ["B", "A"],
}),
API.createElement({
id: "BA_rect6",
type: "rectangle",
groupIds: ["B", "A"],
}),
API.createElement({
id: "CBA_rect7",
type: "rectangle",
groupIds: ["C", "B", "A"],
}),
API.createElement({
id: "X_rect8",
type: "rectangle",
groupIds: ["X"],
}),
API.createElement({
id: "rect9",
type: "rectangle",
}),
API.createElement({
id: "YX_rect10",
type: "rectangle",
groupIds: ["Y", "X"],
}),
API.createElement({
id: "X_rect11",
type: "rectangle",
groupIds: ["X"],
}),
]),
[
"BA_rect1",
"BA_rect5",
"BA_rect6",
"A_rect2",
"A_rect5",
"CBA_rect3",
"CBA_rect7",
"rect4",
"X_rect8",
"X_rect11",
"YX_rect10",
"rect9",
],
);
});
// TODO
it.skip("normalize boundElements array", () => {
const container = API.createElement({
id: "container",
type: "rectangle",
boundElements: [],
});
const boundText = API.createElement({
id: "boundText",
type: "text",
containerId: container.id,
});
mutateElement(container, {
boundElements: [
{ type: "text", id: boundText.id },
{ type: "text", id: "xxx" },
],
});
expect(normalizeElementOrder([container, boundText])).toEqual([
expect.objectContaining({
id: container.id,
}),
expect.objectContaining({ id: boundText.id }),
]);
});
// should take around <100ms for 10K iterations (@dwelle's PC 22-05-25)
it.skip("normalizeElementsOrder() perf", () => {
const makeElements = (iterations: number) => {
const elements: ExcalidrawElement[] = [];
while (iterations--) {
const container = API.createElement({
type: "rectangle",
boundElements: [],
groupIds: ["B", "A"],
});
const boundText = API.createElement({
type: "text",
containerId: container.id,
groupIds: ["A"],
});
const otherElement = API.createElement({
type: "rectangle",
boundElements: [],
groupIds: ["C", "A"],
});
mutateElement(container, {
boundElements: [{ type: "text", id: boundText.id }],
});
elements.push(boundText, otherElement, container);
}
return elements;
};
const elements = makeElements(10000);
const t0 = Date.now();
normalizeElementOrder(elements);
console.info(`${Date.now() - t0}ms`);
});
});

123
src/element/sortElements.ts Normal file
View File

@ -0,0 +1,123 @@
import { arrayToMapWithIndex } from "../utils";
import { ExcalidrawElement } from "./types";
const normalizeGroupElementOrder = (elements: readonly ExcalidrawElement[]) => {
const origElements: ExcalidrawElement[] = elements.slice();
const sortedElements = new Set<ExcalidrawElement>();
const orderInnerGroups = (
elements: readonly ExcalidrawElement[],
): ExcalidrawElement[] => {
const firstGroupSig = elements[0]?.groupIds?.join("");
const aGroup: ExcalidrawElement[] = [elements[0]];
const bGroup: ExcalidrawElement[] = [];
for (const element of elements.slice(1)) {
if (element.groupIds?.join("") === firstGroupSig) {
aGroup.push(element);
} else {
bGroup.push(element);
}
}
return bGroup.length ? [...aGroup, ...orderInnerGroups(bGroup)] : aGroup;
};
const groupHandledElements = new Map<string, true>();
origElements.forEach((element, idx) => {
if (groupHandledElements.has(element.id)) {
return;
}
if (element.groupIds?.length) {
const topGroup = element.groupIds[element.groupIds.length - 1];
const groupElements = origElements.slice(idx).filter((element) => {
const ret = element?.groupIds?.some((id) => id === topGroup);
if (ret) {
groupHandledElements.set(element!.id, true);
}
return ret;
});
for (const elem of orderInnerGroups(groupElements)) {
sortedElements.add(elem);
}
} else {
sortedElements.add(element);
}
});
// if there's a bug which resulted in losing some of the elements, return
// original instead as that's better than losing data
if (sortedElements.size !== elements.length) {
console.error("normalizeGroupElementOrder: lost some elements... bailing!");
return elements;
}
return [...sortedElements];
};
/**
* In theory, when we have text elements bound to a container, they
* should be right after the container element in the elements array.
* However, this is not guaranteed due to old and potential future bugs.
*
* This function sorts containers and their bound texts together. It prefers
* original z-index of container (i.e. it moves bound text elements after
* containers).
*/
const normalizeBoundElementsOrder = (
elements: readonly ExcalidrawElement[],
) => {
const elementsMap = arrayToMapWithIndex(elements);
const origElements: (ExcalidrawElement | null)[] = elements.slice();
const sortedElements = new Set<ExcalidrawElement>();
origElements.forEach((element, idx) => {
if (!element) {
return;
}
if (element.boundElements?.length) {
sortedElements.add(element);
origElements[idx] = null;
element.boundElements.forEach((boundElement) => {
const child = elementsMap.get(boundElement.id);
if (child && boundElement.type === "text") {
sortedElements.add(child[0]);
origElements[child[1]] = null;
}
});
} else if (element.type === "text" && element.containerId) {
const parent = elementsMap.get(element.containerId);
if (!parent?.[0].boundElements?.find((x) => x.id === element.id)) {
sortedElements.add(element);
origElements[idx] = null;
// if element has a container and container lists it, skip this element
// as it'll be taken care of by the container
}
} else {
sortedElements.add(element);
origElements[idx] = null;
}
});
// if there's a bug which resulted in losing some of the elements, return
// original instead as that's better than losing data
if (sortedElements.size !== elements.length) {
console.error(
"normalizeBoundElementsOrder: lost some elements... bailing!",
);
return elements;
}
return [...sortedElements];
};
export const normalizeElementOrder = (
elements: readonly ExcalidrawElement[],
) => {
// console.time();
const ret = normalizeBoundElementsOrder(normalizeGroupElementOrder(elements));
// console.timeEnd();
return ret;
};

View File

@ -28,6 +28,7 @@ import {
resetOriginalContainerCache,
updateOriginalContainerCache,
} from "./textWysiwyg";
import { rotatePoint } from "../math";
export const normalizeText = (text: string) => {
return (
@ -127,10 +128,16 @@ export const bindTextToShapeAfterDuplication = (
const newContainer = sceneElementMap.get(newElementId);
if (newContainer) {
mutateElement(newContainer, {
boundElements: (newContainer.boundElements || []).concat({
type: "text",
id: newTextElementId,
}),
boundElements: (element.boundElements || [])
.filter(
(boundElement) =>
boundElement.id !== newTextElementId &&
boundElement.id !== boundTextElementId,
)
.concat({
type: "text",
id: newTextElementId,
}),
});
}
const newTextElement = sceneElementMap.get(newTextElementId);
@ -717,3 +724,93 @@ export const isValidTextContainer = (element: ExcalidrawElement) => {
isArrowElement(element)
);
};
export const isHittingContainerStroke = (
x: number,
y: number,
container: ExcalidrawTextContainer,
zoom: number,
) => {
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(container);
const topLeft = [x1, y1];
const topRight = [x2, y1];
const bottomLeft = [x1, y2];
const bottomRight = [x2, y2];
const [counterRotateX, counterRotateY] = rotatePoint(
[x, y],
[cx, cy],
-container.angle,
);
const strokeWidth = container.strokeWidth;
if (container.type === "ellipse") {
const threshold = 10 * zoom;
const h = (topLeft[0] + topRight[0]) / 2;
const k = (topLeft[1] + bottomLeft[1]) / 2;
let a = container.width / 2 + threshold;
let b = container.height / 2 + threshold;
const checkPointOnOuterEllipse =
Math.pow(counterRotateX - h, 2) / Math.pow(a, 2) +
Math.pow(counterRotateY - k, 2) / Math.pow(b, 2);
a = container.width / 2 - strokeWidth - threshold;
b = container.height / 2 - strokeWidth - threshold;
const checkPointOnInnerEllipse =
Math.pow(counterRotateX - h, 2) / Math.pow(a, 2) +
Math.pow(counterRotateY - k, 2) / Math.pow(b, 2);
// The expression evaluates to 1 means point is on ellipse,
// < 1 means inside ellipse and > 1 means outside ellipse
if (
checkPointOnInnerEllipse === 1 ||
checkPointOnOuterEllipse === 1 ||
(checkPointOnInnerEllipse > 1 && checkPointOnOuterEllipse < 1)
) {
return true;
}
return false;
}
const threshold = 10 / zoom;
// Left Stroke
if (
counterRotateX >= topLeft[0] - threshold &&
counterRotateX <= topLeft[0] + strokeWidth + threshold &&
counterRotateY >= topLeft[1] - threshold &&
counterRotateY <= bottomRight[1] + threshold
) {
return true;
}
// Top stroke
if (
counterRotateX >= topLeft[0] - threshold &&
counterRotateX <= topRight[0] + threshold &&
counterRotateY >= topLeft[1] - threshold &&
counterRotateY <= topLeft[1] + threshold + strokeWidth
) {
return true;
}
// Right stroke
if (
counterRotateX >= topRight[0] - threshold - strokeWidth &&
counterRotateX <= topRight[0] + threshold &&
counterRotateY >= topRight[1] - threshold &&
counterRotateY <= bottomRight[1] + threshold
) {
return true;
}
// Bottom Stroke
if (
counterRotateX >= bottomLeft[0] - threshold &&
counterRotateX <= bottomRight[0] + threshold &&
counterRotateY >= bottomLeft[1] - threshold - strokeWidth &&
counterRotateY <= bottomLeft[1] + threshold
) {
return true;
}
return false;
};

View File

@ -75,6 +75,7 @@ import { jotaiStore } from "../../jotai";
export const collabAPIAtom = atom<CollabAPI | null>(null);
export const collabDialogShownAtom = atom(false);
export const isCollaboratingAtom = atom(false);
export const isOfflineAtom = atom(false);
interface CollabState {
errorMessage: string;
@ -152,6 +153,8 @@ class Collab extends PureComponent<Props, CollabState> {
componentDidMount() {
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
window.addEventListener("online", this.onOfflineStatusToggle);
window.addEventListener("offline", this.onOfflineStatusToggle);
window.addEventListener(EVENT.UNLOAD, this.onUnload);
const collabAPI: CollabAPI = {
@ -165,6 +168,7 @@ class Collab extends PureComponent<Props, CollabState> {
};
jotaiStore.set(collabAPIAtom, collabAPI);
this.onOfflineStatusToggle();
if (
process.env.NODE_ENV === ENV.TEST ||
@ -180,7 +184,13 @@ class Collab extends PureComponent<Props, CollabState> {
}
}
onOfflineStatusToggle = () => {
jotaiStore.set(isOfflineAtom, !window.navigator.onLine);
};
componentWillUnmount() {
window.removeEventListener("online", this.onOfflineStatusToggle);
window.removeEventListener("offline", this.onOfflineStatusToggle);
window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
window.removeEventListener(EVENT.UNLOAD, this.onUnload);
window.removeEventListener(EVENT.POINTER_MOVE, this.onPointerMove);
@ -600,7 +610,7 @@ class Collab extends PureComponent<Props, CollabState> {
const localElements = this.getSceneElementsIncludingDeleted();
const appState = this.excalidrawAPI.getAppState();
remoteElements = restoreElements(remoteElements, null, false);
remoteElements = restoreElements(remoteElements, null);
const reconciledElements = _reconcileElements(
localElements,

View File

@ -144,7 +144,7 @@ const RoomDialog = ({
<input
type="text"
id="username"
value={username || ""}
value={username.trim() || ""}
className="RoomDialog-username TextInput"
onChange={(event) => onUsernameChange(event.target.value)}
onKeyPress={(event) => event.key === "Enter" && handleClose()}

View File

@ -1,6 +1,7 @@
import { PRECEDING_ELEMENT_KEY } from "../../constants";
import { ExcalidrawElement } from "../../element/types";
import { AppState } from "../../types";
import { arrayToMapWithIndex } from "../../utils";
export type ReconciledElements = readonly ExcalidrawElement[] & {
_brand: "reconciledElements";
@ -33,30 +34,13 @@ const shouldDiscardRemoteElement = (
return false;
};
const getElementsMapWithIndex = <T extends ExcalidrawElement>(
elements: readonly T[],
) =>
elements.reduce(
(
acc: {
[key: string]: [element: T, index: number] | undefined;
},
element: T,
idx,
) => {
acc[element.id] = [element, idx];
return acc;
},
{},
);
export const reconcileElements = (
localElements: readonly ExcalidrawElement[],
remoteElements: readonly BroadcastedExcalidrawElement[],
localAppState: AppState,
): ReconciledElements => {
const localElementsData =
getElementsMapWithIndex<ExcalidrawElement>(localElements);
arrayToMapWithIndex<ExcalidrawElement>(localElements);
const reconciledElements: ExcalidrawElement[] = localElements.slice();
@ -69,7 +53,7 @@ export const reconcileElements = (
for (const remoteElement of remoteElements) {
remoteElementIdx++;
const local = localElementsData[remoteElement.id];
const local = localElementsData.get(remoteElement.id);
if (shouldDiscardRemoteElement(localAppState, local?.[0], remoteElement)) {
if (remoteElement[PRECEDING_ELEMENT_KEY]) {
@ -105,21 +89,21 @@ export const reconcileElements = (
offset++;
if (cursor === 0) {
reconciledElements.unshift(remoteElement);
localElementsData[remoteElement.id] = [
localElementsData.set(remoteElement.id, [
remoteElement,
cursor - offset,
];
]);
} else {
reconciledElements.splice(cursor + 1, 0, remoteElement);
localElementsData[remoteElement.id] = [
localElementsData.set(remoteElement.id, [
remoteElement,
cursor + 1 - offset,
];
]);
cursor++;
}
} else {
let idx = localElementsData[parent]
? localElementsData[parent]![1]
let idx = localElementsData.has(parent)
? localElementsData.get(parent)![1]
: null;
if (idx != null) {
idx += offset;
@ -127,38 +111,38 @@ export const reconcileElements = (
if (idx != null && idx >= cursor) {
reconciledElements.splice(idx + 1, 0, remoteElement);
offset++;
localElementsData[remoteElement.id] = [
localElementsData.set(remoteElement.id, [
remoteElement,
idx + 1 - offset,
];
]);
cursor = idx + 1;
} else if (idx != null) {
reconciledElements.splice(cursor + 1, 0, remoteElement);
offset++;
localElementsData[remoteElement.id] = [
localElementsData.set(remoteElement.id, [
remoteElement,
cursor + 1 - offset,
];
]);
cursor++;
} else {
reconciledElements.push(remoteElement);
localElementsData[remoteElement.id] = [
localElementsData.set(remoteElement.id, [
remoteElement,
reconciledElements.length - 1 - offset,
];
]);
}
}
// no parent z-index information, local element exists → replace in place
} else if (local) {
reconciledElements[local[1]] = remoteElement;
localElementsData[remoteElement.id] = [remoteElement, local[1]];
localElementsData.set(remoteElement.id, [remoteElement, local[1]]);
// otherwise push to the end
} else {
reconciledElements.push(remoteElement);
localElementsData[remoteElement.id] = [
localElementsData.set(remoteElement.id, [
remoteElement,
reconciledElements.length - 1 - offset,
];
]);
}
}

View File

@ -263,9 +263,12 @@ export const loadScene = async (
await importFromBackend(id, privateKey),
localDataState?.appState,
localDataState?.elements,
{ repairBindings: true },
);
} else {
data = restore(localDataState || null, null, null);
data = restore(localDataState || null, null, null, {
repairBindings: true,
});
}
return {

View File

@ -45,6 +45,23 @@
}
}
}
.collab-offline-warning {
pointer-events: none;
position: absolute;
top: 6.5rem;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem 1rem;
font-size: 0.875rem;
text-align: center;
line-height: 1.5;
border-radius: var(--border-radius-md);
background-color: var(--color-warning);
color: var(--color-text-warning);
z-index: 6;
white-space: pre;
}
}
.excalidraw-app.is-collaborating {

View File

@ -52,6 +52,7 @@ import Collab, {
collabAPIAtom,
collabDialogShownAtom,
isCollaboratingAtom,
isOfflineAtom,
} from "./collab/Collab";
import {
exportToBackend,
@ -66,10 +67,7 @@ import {
} from "./data/localStorage";
import CustomStats from "./CustomStats";
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
import "./index.scss";
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
import { updateStaleImageStatuses } from "./data/FileManager";
import { newElementWith } from "../element/mutateElement";
import { isInitializedImageElement } from "../element/typeChecks";
@ -77,7 +75,7 @@ import { loadFilesFromFirebase } from "./data/firebase";
import { LocalData } from "./data/LocalData";
import { isBrowserStorageStateNewer } from "./data/tabSync";
import clsx from "clsx";
import { atom, Provider, useAtom } from "jotai";
import { atom, Provider, useAtom, useAtomValue } from "jotai";
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
import { reconcileElements } from "./collab/reconciliation";
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
@ -85,6 +83,8 @@ import { AppMainMenu } from "./components/AppMainMenu";
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
import { AppFooter } from "./components/AppFooter";
import "./index.scss";
polyfill();
window.EXCALIDRAW_THROTTLE_RENDER = true;
@ -362,7 +362,7 @@ const ExcalidrawWrapper = () => {
if (data.scene) {
excalidrawAPI.updateScene({
...data.scene,
...restore(data.scene, null, null),
...restore(data.scene, null, null, { repairBindings: true }),
commitToHistory: true,
});
}
@ -599,6 +599,8 @@ const ExcalidrawWrapper = () => {
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
};
const isOffline = useAtomValue(isOfflineAtom);
return (
<div
style={{ height: "100%" }}
@ -661,6 +663,11 @@ const ExcalidrawWrapper = () => {
/>
<AppWelcomeScreen setCollabDialogShown={setCollabDialogShown} />
<AppFooter />
{isCollaborating && isOffline && (
<div className="collab-offline-warning">
{t("alerts.collabOfflineWarning")}
</div>
)}
</Excalidraw>
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
{errorMessage && (

View File

@ -109,35 +109,35 @@
"decreaseFontSize": "تصغير حجم الخط",
"increaseFontSize": "تكبير حجم الخط",
"unbindText": "فك ربط النص",
"bindText": "",
"bindText": "ربط النص بالحاوية",
"link": {
"edit": "تعديل الرابط",
"create": "إنشاء رابط",
"label": "رابط"
},
"lineEditor": {
"edit": "",
"exit": ""
"edit": "تحرير السطر",
"exit": "الخروج من المُحرر"
},
"elementLock": {
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
"lock": "قفل",
"unlock": "فتح",
"lockAll": "قفل الكل",
"unlockAll": "فتح الكل"
},
"statusPublished": "",
"sidebarLock": ""
"statusPublished": "نُشر",
"sidebarLock": "إبقاء الشريط الجانبي مفتوح"
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
"noItems": "لا توجد عناصر أضيفت بعد...",
"hint_emptyLibrary": "حدد عنصر على القماش لإضافته هنا، أو تثبيت مكتبة من المستودع العام أدناه.",
"hint_emptyPrivateLibrary": "حدد عنصر على القماش لإضافته هنا."
},
"buttons": {
"clearReset": "إعادة تعيين اللوحة",
"exportJSON": "صدر الملف",
"exportImage": "",
"export": "",
"exportImage": "تصدير الصورة...",
"export": "حفظ إلى...",
"exportToPng": "تصدير بصيغة PNG",
"exportToSvg": "تصدير بصيغة SVG",
"copyToClipboard": "نسخ إلى الحافظة",
@ -179,7 +179,7 @@
"couldNotLoadInvalidFile": "تعذر التحميل، الملف غير صالح",
"importBackendFailed": "فشل الاستيراد من الخادوم.",
"cannotExportEmptyCanvas": "لا يمكن تصدير لوحة فارغة.",
"couldNotCopyToClipboard": "",
"couldNotCopyToClipboard": "تعذر النسخ إلى الحافظة.",
"decryptFailed": "تعذر فك تشفير البيانات.",
"uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.",
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
@ -200,10 +200,10 @@
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
"invalidSVGString": "SVG غير صالح.",
"cannotResolveCollabServer": "",
"importLibraryError": "",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"cannotResolveCollabServer": "تعذر الاتصال بخادم التعاون. الرجاء إعادة تحميل الصفحة والمحاولة مرة أخرى.",
"importLibraryError": "تعذر تحميل المكتبة",
"collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.",
"collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك."
},
"toolBar": {
"selection": "تحديد",
@ -217,9 +217,10 @@
"text": "نص",
"library": "مكتبة",
"lock": "الحفاظ على أداة التحديد نشطة بعد الرسم",
"penMode": "",
"link": "",
"eraser": "ممحاة"
"penMode": "وضع القلم - امنع اللمس",
"link": "إضافة/تحديث الرابط للشكل المحدد",
"eraser": "ممحاة",
"hand": ""
},
"headings": {
"canvasActions": "إجراءات اللوحة",
@ -227,7 +228,7 @@
"shapes": "الأشكال"
},
"hints": {
"canvasPanning": "لتحريك لوحة الرسم ، استمر في الضغط على عجلة الماوس أو مفتاح المسافة أثناء السحب",
"canvasPanning": "لتحريك القماش، اضغط على عجلة الفأرة أو مفتاح المسافة أثناء السحب، أو استخدم أداة اليد",
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
"freeDraw": "انقر واسحب، افرج عند الانتهاء",
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
@ -238,14 +239,15 @@
"resize": "يمكنك تقييد النسب بالضغط على SHIFT أثناء تغيير الحجم،\nاضغط على ALT لتغيير الحجم من المركز",
"resizeImage": "يمكنك تغيير الحجم بحرية بالضغط بأستمرار على SHIFT،\nاضغط بأستمرار على ALT أيضا لتغيير الحجم من المركز",
"rotate": "يمكنك تقييد الزوايا من خلال الضغط على SHIFT أثناء الدوران",
"lineEditor_info": "",
"lineEditor_info": "اضغط على مفتاح (Ctrl أو Cmd) و انقر بشكل مزدوج، أو اضغط على مفتاحي (Ctrl أو Cmd) و (Enter) لتعديل النقاط",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "نشر مكتبتك",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "تعذر عرض المعاينة",

View File

@ -219,7 +219,8 @@
"lock": "Поддържайте избрания инструмент активен след рисуване",
"penMode": "",
"link": "",
"eraser": ""
"eraser": "",
"hand": ""
},
"headings": {
"canvasActions": "Действия по платното",
@ -245,7 +246,8 @@
"publishLibrary": "",
"bindTextToElement": "Натиснете Enter, за да добавите",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Невъзможност за показване на preview",

View File

@ -219,7 +219,8 @@
"lock": "আঁকার পরে নির্বাচিত টুল সক্রিয় রাখুন",
"penMode": "",
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
"eraser": "ঝাড়ন"
"eraser": "ঝাড়ন",
"hand": ""
},
"headings": {
"canvasActions": "ক্যানভাস কার্যকলাপ",
@ -227,7 +228,7 @@
"shapes": "আকার(গুলি)"
},
"hints": {
"canvasPanning": "ক্যানভাস সরানোর জন্য মাউস হুইল বা স্পেসবার ধরে টানুন",
"canvasPanning": "",
"linearElement": "একাধিক বিন্দু শুরু করতে ক্লিক করুন, একক লাইনের জন্য টেনে আনুন",
"freeDraw": "ক্লিক করুন এবং টেনে আনুন, আপনার কাজ শেষ হলে ছেড়ে দিন",
"text": "বিশেষ্য: আপনি নির্বাচন টুলের সাথে যে কোনো জায়গায় ডাবল-ক্লিক করে পাঠ্য যোগ করতে পারেন",
@ -245,7 +246,8 @@
"publishLibrary": "আপনার নিজস্ব সংগ্রহ প্রকাশ করুন",
"bindTextToElement": "লেখা যোগ করতে এন্টার টিপুন",
"deepBoxSelect": "",
"eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন"
"eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "প্রিভিউ দেখাতে অপারগ",

View File

@ -219,7 +219,8 @@
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
"penMode": "",
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
"eraser": "Esborrador"
"eraser": "Esborrador",
"hand": ""
},
"headings": {
"canvasActions": "Accions del llenç",
@ -227,7 +228,7 @@
"shapes": "Formes"
},
"hints": {
"canvasPanning": "Per a moure el llenç, mantingueu premuda la roda del ratolí o la tecla espai mentre l'arrossegueu",
"canvasPanning": "",
"linearElement": "Feu clic per a dibuixar múltiples punts; arrossegueu per a una sola línia",
"freeDraw": "Feu clic i arrossegueu, deixeu anar per a finalitzar",
"text": "Consell: també podeu afegir text fent doble clic en qualsevol lloc amb l'eina de selecció",
@ -245,7 +246,8 @@
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
"bindTextToElement": "Premeu enter per a afegir-hi text",
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament",
"eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar"
"eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "No es pot mostrar la previsualització",

View File

@ -219,7 +219,8 @@
"lock": "",
"penMode": "",
"link": "",
"eraser": "Guma"
"eraser": "Guma",
"hand": ""
},
"headings": {
"canvasActions": "",
@ -245,7 +246,8 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "",

View File

@ -219,7 +219,8 @@
"lock": "",
"penMode": "",
"link": "",
"eraser": ""
"eraser": "",
"hand": ""
},
"headings": {
"canvasActions": "",
@ -245,7 +246,8 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "",

View File

@ -219,7 +219,8 @@
"lock": "Ausgewähltes Werkzeug nach Zeichnen aktiv lassen",
"penMode": "Stift-Modus - Berührung verhindern",
"link": "Link für ausgewählte Form hinzufügen / aktualisieren",
"eraser": "Radierer"
"eraser": "Radierer",
"hand": "Hand (Schwenkwerkzeug)"
},
"headings": {
"canvasActions": "Aktionen für Zeichenfläche",
@ -227,7 +228,7 @@
"shapes": "Formen"
},
"hints": {
"canvasPanning": "Um die Zeichenfläche zu verschieben, halte das Mausrad oder die Leertaste während des Ziehens",
"canvasPanning": "Um die Zeichenfläche zu verschieben, halte das Mausrad oder die Leertaste während des Ziehens, oder verwende das Hand-Werkzeug",
"linearElement": "Klicken für Linie mit mehreren Punkten, Ziehen für einzelne Linie",
"freeDraw": "Klicke und ziehe. Lass los, wenn du fertig bist",
"text": "Tipp: Du kannst auch Text hinzufügen, indem du mit dem Auswahlwerkzeug auf eine beliebige Stelle doppelklickst",
@ -245,7 +246,8 @@
"publishLibrary": "Veröffentliche deine eigene Bibliothek",
"bindTextToElement": "Zum Hinzufügen Eingabetaste drücken",
"deepBoxSelect": "Halte CtrlOrCmd gedrückt, um innerhalb der Gruppe auszuwählen, und um Ziehen zu vermeiden",
"eraserRevert": "Halte Alt gedrückt, um die zum Löschen markierten Elemente zurückzusetzen"
"eraserRevert": "Halte Alt gedrückt, um die zum Löschen markierten Elemente zurückzusetzen",
"firefox_clipboard_write": "Diese Funktion kann wahrscheinlich aktiviert werden, indem die Einstellung \"dom.events.asyncClipboard.clipboardItem\" auf \"true\" gesetzt wird. Um die Browsereinstellungen in Firefox zu ändern, besuche die Seite \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Vorschau kann nicht angezeigt werden",

View File

@ -219,7 +219,8 @@
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
"eraser": "Γόμα"
"eraser": "Γόμα",
"hand": ""
},
"headings": {
"canvasActions": "Ενέργειες καμβά",
@ -227,7 +228,7 @@
"shapes": "Σχήματα"
},
"hints": {
"canvasPanning": "Για να μετακινήσετε καμβά, κρατήστε πατημένο τον τροχό του ποντικιού ή το πλήκτρο διαστήματος ενώ σύρετε",
"canvasPanning": "",
"linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή",
"freeDraw": "Κάντε κλικ και σύρτε, απελευθερώσατε όταν έχετε τελειώσει",
"text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών",
@ -245,7 +246,8 @@
"publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη",
"bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου",
"deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά",
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή"
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή",
"firefox_clipboard_write": "Αυτή η επιλογή μπορεί πιθανώς να ενεργοποιηθεί αλλάζοντας την ρύθμιση \"dom.events.asyncClipboard.clipboardItem\" σε \"true\". Για να αλλάξετε τις ρυθμίσεις του προγράμματος περιήγησης στο Firefox, επισκεφθείτε τη σελίδα \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",

View File

@ -193,7 +193,8 @@
"invalidSceneUrl": "Couldn't import scene from the supplied URL. It's either malformed, or doesn't contain valid Excalidraw JSON data.",
"resetLibrary": "This will clear your library. Are you sure?",
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled."
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
"collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!"
},
"errors": {
"unsupportedFileType": "Unsupported file type.",

View File

@ -219,7 +219,8 @@
"lock": "Mantener la herramienta seleccionada activa después de dibujar",
"penMode": "Modo Lápiz - previene toque",
"link": "Añadir/Actualizar enlace para una forma seleccionada",
"eraser": "Borrar"
"eraser": "Borrar",
"hand": "Mano (herramienta de panoramización)"
},
"headings": {
"canvasActions": "Acciones del lienzo",
@ -227,7 +228,7 @@
"shapes": "Formas"
},
"hints": {
"canvasPanning": "Para mover el lienzo, mantenga la rueda del ratón o la barra de espacio mientras arrastra",
"canvasPanning": "Para mover el lienzo, mantenga la rueda del ratón o la barra espaciadora mientras arrastra o utilice la herramienta de mano",
"linearElement": "Haz clic para dibujar múltiples puntos, arrastrar para solo una línea",
"freeDraw": "Haz clic y arrastra, suelta al terminar",
"text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección",
@ -245,7 +246,8 @@
"publishLibrary": "Publica tu propia biblioteca",
"bindTextToElement": "Presione Entrar para agregar",
"deepBoxSelect": "Mantén CtrlOrCmd para seleccionar en profundidad, y para evitar arrastrar",
"eraserRevert": "Mantenga pulsado Alt para revertir los elementos marcados para su eliminación"
"eraserRevert": "Mantenga pulsado Alt para revertir los elementos marcados para su eliminación",
"firefox_clipboard_write": "Esta característica puede ser habilitada estableciendo la bandera \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Para cambiar las banderas del navegador en Firefox, visite la página \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "No se puede mostrar la vista previa",

View File

@ -219,7 +219,8 @@
"lock": "Mantendu aktibo hautatutako tresna marraztu ondoren",
"penMode": "Luma modua - ukipena saihestu",
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
"eraser": "Borragoma"
"eraser": "Borragoma",
"hand": ""
},
"headings": {
"canvasActions": "Canvas ekintzak",
@ -227,7 +228,7 @@
"shapes": "Formak"
},
"hints": {
"canvasPanning": "Oihala mugitzeko, sakatu saguaren gurpila edo zuriune-barra arrastatzean",
"canvasPanning": "",
"linearElement": "Egin klik hainbat puntu hasteko, arrastatu lerro bakarrerako",
"freeDraw": "Egin klik eta arrastatu, askatu amaitutakoan",
"text": "Aholkua: testua gehitu dezakezu edozein lekutan klik bikoitza eginez hautapen tresnarekin",
@ -245,7 +246,8 @@
"publishLibrary": "Argitaratu zure liburutegia",
"bindTextToElement": "Sakatu Sartu testua gehitzeko",
"deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko",
"eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko"
"eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Ezin da oihala aurreikusi",

View File

@ -219,7 +219,8 @@
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
"penMode": "حالت قلم - جلوگیری از تماس",
"link": "افزودن/به‌روزرسانی پیوند برای شکل انتخابی",
"eraser": "پاک کن"
"eraser": "پاک کن",
"hand": ""
},
"headings": {
"canvasActions": "عملیات روی بوم",
@ -227,7 +228,7 @@
"shapes": "شکل‌ها"
},
"hints": {
"canvasPanning": "برای حرکت دادن بوم، چرخ ماوس یا فاصله را در حین کشیدن نگه دارید",
"canvasPanning": "",
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
@ -245,7 +246,8 @@
"publishLibrary": "کتابخانه خود را منتشر کنید",
"bindTextToElement": "برای افزودن اینتر را بزنید",
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند"
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",

View File

@ -219,7 +219,8 @@
"lock": "Pidä valittu työkalu aktiivisena piirron jälkeen",
"penMode": "",
"link": "Lisää/päivitä linkki valitulle muodolle",
"eraser": "Poistotyökalu"
"eraser": "Poistotyökalu",
"hand": ""
},
"headings": {
"canvasActions": "Piirtoalueen toiminnot",
@ -227,7 +228,7 @@
"shapes": "Muodot"
},
"hints": {
"canvasPanning": "Liikuttaaksesi piirtoaluetta, raahaa hiiren vieritysrulla tai välilyöntinäppäin alaspainettuna",
"canvasPanning": "",
"linearElement": "Klikkaa piirtääksesi useampi piste, raahaa piirtääksesi yksittäinen viiva",
"freeDraw": "Paina ja raahaa, päästä irti kun olet valmis",
"text": "Vinkki: voit myös lisätä tekstiä kaksoisnapsauttamalla mihin tahansa valintatyökalulla",
@ -245,7 +246,8 @@
"publishLibrary": "Julkaise oma kirjasto",
"bindTextToElement": "Lisää tekstiä painamalla enter",
"deepBoxSelect": "Käytä syvävalintaa ja estä raahaus painamalla CtrlOrCmd",
"eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen"
"eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Esikatselua ei voitu näyttää",

View File

@ -45,7 +45,7 @@
"exportEmbedScene": "Intégrer la scène",
"exportEmbedScene_details": "Les données de scène seront enregistrées dans le fichier PNG/SVG exporté, afin que la scène puisse être restaurée à partir de celui-ci.\nCela augmentera la taille du fichier exporté.",
"addWatermark": "Ajouter \"Réalisé avec Excalidraw\"",
"handDrawn": "Manuscrit",
"handDrawn": "À main levée",
"normal": "Normale",
"code": "Code",
"small": "Petite",
@ -219,7 +219,8 @@
"lock": "Garder l'outil sélectionné actif après le dessin",
"penMode": "Mode stylo - évite le toucher",
"link": "Ajouter/mettre à jour le lien pour une forme sélectionnée",
"eraser": "Gomme"
"eraser": "Gomme",
"hand": "Mains (outil de déplacement de la vue)"
},
"headings": {
"canvasActions": "Actions du canevas",
@ -227,7 +228,7 @@
"shapes": "Formes"
},
"hints": {
"canvasPanning": "Pour déplacer la zone de dessin, maintenez la molette de la souris enfoncée ou la barre d'espace tout en faisant glisser",
"canvasPanning": "Pour déplacer la zone de dessin, maintenez la molette de la souris enfoncée ou la barre d'espace tout en faisant glisser, ou utiliser l'outil main.",
"linearElement": "Cliquez pour démarrer plusieurs points, faites glisser pour une seule ligne",
"freeDraw": "Cliquez et faites glissez, relâchez quand vous avez terminé",
"text": "Astuce : vous pouvez aussi ajouter du texte en double-cliquant n'importe où avec l'outil de sélection",
@ -245,7 +246,8 @@
"publishLibrary": "Publier votre propre bibliothèque",
"bindTextToElement": "Appuyer sur Entrée pour ajouter du texte",
"deepBoxSelect": "Maintenir Ctrl ou Cmd pour sélectionner dans les groupes et empêcher le déplacement",
"eraserRevert": "Maintenez Alt enfoncé pour annuler les éléments marqués pour suppression"
"eraserRevert": "Maintenez Alt enfoncé pour annuler les éléments marqués pour suppression",
"firefox_clipboard_write": "Cette fonctionnalité devrait pouvoir être activée en définissant l'option \"dom.events.asyncClipboard.clipboard.clipboardItem\" à \"true\". Pour modifier les paramètres du navigateur dans Firefox, visitez la page \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Impossible dafficher laperçu",

View File

@ -219,7 +219,8 @@
"lock": "Manter a ferramenta seleccionada activa despois de debuxar",
"penMode": "Modo lapis - evitar o contacto",
"link": "Engadir/ Actualizar ligazón para a forma seleccionada",
"eraser": "Goma de borrar"
"eraser": "Goma de borrar",
"hand": "Man (ferramenta de desprazamento)"
},
"headings": {
"canvasActions": "Accións do lenzo",
@ -227,7 +228,7 @@
"shapes": "Formas"
},
"hints": {
"canvasPanning": "Para mover o lenzo, manteña a roda do rato ou a barra de espazo mentres arrastra",
"canvasPanning": "Para mover o lenzo, manteña pulsada a roda do rato ou a barra de espazo mentres arrastra, ou utilice a ferramenta da man",
"linearElement": "Faga clic para iniciar varios puntos, arrastre para unha sola liña",
"freeDraw": "Fai clic e arrastra, solta cando acabes",
"text": "Consello: tamén podes engadir texto facendo dobre-clic en calquera lugar coa ferramenta de selección",
@ -245,7 +246,8 @@
"publishLibrary": "Publica a túa propia biblioteca",
"bindTextToElement": "Prema a tecla enter para engadir texto",
"deepBoxSelect": "Manteña pulsado CtrlOrCmd para seleccionar en profundidade e evitar o arrastre",
"eraserRevert": "Manteña pulsado Alt para reverter os elementos marcados para a súa eliminación"
"eraserRevert": "Manteña pulsado Alt para reverter os elementos marcados para a súa eliminación",
"firefox_clipboard_write": "Esta función pódese activar establecendo a opción \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Para cambiar as opcións do navegador en Firefox, visita a páxina \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Non se pode mostrar a vista previa",

View File

@ -219,7 +219,8 @@
"lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור",
"penMode": "",
"link": "הוספה/עדכון של קישור עבור הצורה הנבחרת",
"eraser": "מחק"
"eraser": "מחק",
"hand": ""
},
"headings": {
"canvasActions": "פעולות הלוח",
@ -227,7 +228,7 @@
"shapes": "צורות"
},
"hints": {
"canvasPanning": "כדי להזיז את הקנבס לחצו על גלגל העכבר או על מקש הרווח תוך כדי גרירה",
"canvasPanning": "",
"linearElement": "הקלק בשביל לבחור נקודות מרובות, גרור בשביל קו בודד",
"freeDraw": "לחץ וגרור, שחרר כשסיימת",
"text": "טיפ: אפשר להוסיף טקסט על ידי לחיצה כפולה בכל מקום עם כלי הבחירה",
@ -245,7 +246,8 @@
"publishLibrary": "פירסום ספריה אישית",
"bindTextToElement": "יש להקיש Enter כדי להוסיף טקסט",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה",

View File

@ -219,7 +219,8 @@
"lock": "ड्राइंग के बाद चयनित टूल को सक्रिय रखें",
"penMode": "पेन का मोड - स्पर्श टाले",
"link": "",
"eraser": "रबड़"
"eraser": "रबड़",
"hand": "हाथ ( खिसकाने का औज़ार)"
},
"headings": {
"canvasActions": "कैनवास क्रिया",
@ -227,7 +228,7 @@
"shapes": "आकृतियाँ"
},
"hints": {
"canvasPanning": "",
"canvasPanning": "कैनवास को सरकाने के लिए, ड्रैग करते समय माउस व्हील को पकड़े रखे या स्पेसबार को दबाए रखे, अथवा हाथ वाले औज़ार का उपयोग करें",
"linearElement": "कई बिंदुओं को शुरू करने के लिए क्लिक करें, सिंगल लाइन के लिए खींचें",
"freeDraw": "क्लिक करें और खींचें। समाप्त करने के लिए, छोड़ो",
"text": "आप चयन टूल से कहीं भी डबल-क्लिक करके टेक्स्ट जोड़ सकते हैं",
@ -245,7 +246,8 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "मिटाने के लिए चुने हुए चीजों को ना चुनने के लिए Alt साथ में दबाए"
"eraserRevert": "मिटाने के लिए चुने हुए चीजों को ना चुनने के लिए Alt साथ में दबाए",
"firefox_clipboard_write": "\"dom.events.asyncClipboard.clipboardItem\" फ़्लैग को \"true\" पर सेट करके इस सुविधा को संभवतः सक्षम किया जा सकता है। Firefox में ब्राउज़र फ़्लैग बदलने के लिए, \"about:config\" पृष्ठ पर जाएँ।"
},
"canvasError": {
"cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं",
@ -448,15 +450,15 @@
},
"welcomeScreen": {
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
"center_heading": "आपका सर्व डेटा ब्राउज़र के भीतर स्थानिक जगह पे सुरक्षित किया गया.",
"center_heading_plus": "बजाय आपको Excalidraw+ पर जाना है?",
"menuHint": "निर्यात, पसंद, भाषायें, ..."
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
"menuHint": "निर्यात, पसंद, और भी...",
"center_heading": "चित्रांकन। बनाया गया। सरल।",
"toolbarHint": "एक औजार चुने और चित्रकारी प्रारंभ करे!",
"helpHint": "शॉर्ट्कट और सहाय्य"
}
}
}

View File

@ -219,7 +219,8 @@
"lock": "Rajzolás után az aktív eszközt tartsa kijelölve",
"penMode": "",
"link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz",
"eraser": ""
"eraser": "",
"hand": ""
},
"headings": {
"canvasActions": "Vászon műveletek",
@ -227,7 +228,7 @@
"shapes": "Alakzatok"
},
"hints": {
"canvasPanning": "A vászon mozgatásához tartsd lenyomva az egér görgőjét vagy a szóköz billentyűt húzás közben",
"canvasPanning": "",
"linearElement": "Kattintással görbe, az eger húzásával pedig egyenes nyilat rajzolhatsz",
"freeDraw": "Kattints és húzd, majd engedd el, amikor végeztél",
"text": "Tipp: A kijelölés eszközzel a dupla kattintás új szöveget hoz létre",
@ -245,7 +246,8 @@
"publishLibrary": "Tedd közzé saját könyvtáradat",
"bindTextToElement": "Nyomd meg az Entert szöveg hozzáadáshoz",
"deepBoxSelect": "Tartsd lenyomva a Ctrl/Cmd billentyűt a mély kijelöléshez és a húzás megakadályozásához",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Előnézet nem jeleníthető meg",

View File

@ -219,7 +219,8 @@
"lock": "Biarkan alat yang dipilih aktif setelah menggambar",
"penMode": "Mode pena - mencegah sentuhan",
"link": "Tambah/Perbarui tautan untuk bentuk yang dipilih",
"eraser": "Penghapus"
"eraser": "Penghapus",
"hand": ""
},
"headings": {
"canvasActions": "Opsi Kanvas",
@ -227,7 +228,7 @@
"shapes": "Bentuk"
},
"hints": {
"canvasPanning": "Untuk memindahkan kanvas, tekan roda mouse atau spasi ketika menarik",
"canvasPanning": "",
"linearElement": "Klik untuk memulai banyak poin, seret untuk satu baris",
"freeDraw": "Klik dan seret, lepaskan jika Anda selesai",
"text": "Tip: Anda juga dapat menambahkan teks dengan klik ganda di mana saja dengan alat pemilihan",
@ -245,7 +246,8 @@
"publishLibrary": "Terbitkan pustaka Anda",
"bindTextToElement": "Tekan enter untuk tambahkan teks",
"deepBoxSelect": "Tekan Ctrl atau Cmd untuk memilih yang di dalam, dan mencegah penggeseran",
"eraserRevert": "Tahan Alt untuk mengembalikan elemen yang ditandai untuk dihapus"
"eraserRevert": "Tahan Alt untuk mengembalikan elemen yang ditandai untuk dihapus",
"firefox_clipboard_write": "Fitur ini dapat diaktifkan melalui pengaturan flag \"dom.events.asyncClipboard.clipboardItem\" ke \"true\". Untuk mengganti flag di Firefox, pergi ke laman \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Tidak dapat menampilkan pratinjau",

View File

@ -219,7 +219,8 @@
"lock": "Mantieni lo strumento selezionato attivo dopo aver disegnato",
"penMode": "Modalità penna - previene il tocco",
"link": "Aggiungi/ aggiorna il link per una forma selezionata",
"eraser": "Gomma"
"eraser": "Gomma",
"hand": "Mano (strumento di panoramica)"
},
"headings": {
"canvasActions": "Azioni sulla Tela",
@ -227,7 +228,7 @@
"shapes": "Forme"
},
"hints": {
"canvasPanning": "Per spostare la tela, tieni premuta la rotella del mouse o la barra spaziatrice mentre la trascini",
"canvasPanning": "Per spostare la tela, tieni premuta la rotellina del mouse o la barra spaziatrice mentre trascini oppure usa lo strumento mano",
"linearElement": "Clicca per iniziare una linea in più punti, trascina per singola linea",
"freeDraw": "Clicca e trascina, rilascia quando avrai finito",
"text": "Suggerimento: puoi anche aggiungere del testo facendo doppio clic ovunque con lo strumento di selezione",
@ -245,7 +246,8 @@
"publishLibrary": "Pubblica la tua libreria",
"bindTextToElement": "Premi invio per aggiungere il testo",
"deepBoxSelect": "Tieni premuto CtrlOCmd per selezionare in profondità e per impedire il trascinamento",
"eraserRevert": "Tieni premuto Alt per ripristinare gli elementi contrassegnati per l'eliminazione"
"eraserRevert": "Tieni premuto Alt per ripristinare gli elementi contrassegnati per l'eliminazione",
"firefox_clipboard_write": "Questa funzione può essere abilitata impostando il flag \"dom.events.asyncClipboard.clipboardItem\" su \"true\". Per modificare i flag del browser in Firefox, visitare la pagina \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Impossibile visualizzare l'anteprima",

View File

@ -219,7 +219,8 @@
"lock": "描画後も使用中のツールを選択したままにする",
"penMode": "ペンモード - タッチ防止",
"link": "選択した図形のリンクを追加/更新",
"eraser": "消しゴム"
"eraser": "消しゴム",
"hand": ""
},
"headings": {
"canvasActions": "キャンバス操作",
@ -227,7 +228,7 @@
"shapes": "図形"
},
"hints": {
"canvasPanning": "キャンバスを移動するには、マウスホイールまたはスペースバーを押しながらドラッグします",
"canvasPanning": "",
"linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線",
"freeDraw": "クリックしてドラッグします。離すと終了します",
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
@ -245,7 +246,8 @@
"publishLibrary": "自分のライブラリを公開",
"bindTextToElement": "Enterを押してテキストを追加",
"deepBoxSelect": "CtrlOrCmd を押し続けることでドラッグを抑止し、深い選択を行います",
"eraserRevert": "Alt を押し続けることで削除マークされた要素を元に戻す"
"eraserRevert": "Alt を押し続けることで削除マークされた要素を元に戻す",
"firefox_clipboard_write": "この機能は、\"dom.events.asyncClipboard.clipboardItem\" フラグを \"true\" に設定することで有効になる可能性があります。Firefox でブラウザーの設定を変更するには、\"about:config\" ページを参照してください。"
},
"canvasError": {
"cannotShowPreview": "プレビューを表示できません",

View File

@ -202,8 +202,8 @@
"invalidSVGString": "SVG armeɣtu.",
"cannotResolveCollabServer": "Ulamek tuqqna s aqeddac n umyalel. Ma ulac uɣilif ales asali n usebter sakin eɛreḍ tikkelt-nniḍen.",
"importLibraryError": "Ur d-ssalay ara tamkarḍit",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"collabSaveFailed": "Ulamek asekles deg uzadur n yisefka deg ugilal. Ma ikemmel wugur, isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem.",
"collabSaveFailed_sizeExceeded": "Ulamek asekles deg uzadur n yisefka deg ugilal, taɣzut n usuneɣ tettban-d temqer aṭas. Isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem."
},
"toolBar": {
"selection": "Tafrayt",
@ -217,9 +217,10 @@
"text": "Aḍris",
"library": "Tamkarḍit",
"lock": "Eǧǧ afecku n tefrayt yermed mbaɛd asuneɣ",
"penMode": "",
"penMode": "Askar n yimru - gdel tanalit",
"link": "Rnu/leqqem aseɣwen i talɣa yettwafernen",
"eraser": "Sfeḍ"
"eraser": "Sfeḍ",
"hand": "Afus (afecku n usmutti n tmuɣli)"
},
"headings": {
"canvasActions": "Tigawin n teɣzut n usuneɣ",
@ -227,7 +228,7 @@
"shapes": "Talɣiwin"
},
"hints": {
"canvasPanning": "Akken ad tesmuttiḍ taɣzut n usuneɣ, ṭṭef ṛṛuda n umumed, neɣ afeggag n tallunt mi ara tzuɣreḍ",
"canvasPanning": "",
"linearElement": "Ssit akken ad tebduḍ aṭas n tenqiḍin, zuɣer i yiwen n yizirig",
"freeDraw": "Ssit yerna zuɣer, serreḥ ticki tfukeḍ",
"text": "Tixidest: tzemreḍ daɣen ad ternuḍ aḍris s usiti snat n tikkal anida tebɣiḍ s ufecku n tefrayt",
@ -245,7 +246,8 @@
"publishLibrary": "Siẓreg tamkarḍit-inek•inem",
"bindTextToElement": "Ssed ɣef kcem akken ad ternuḍ aḍris",
"deepBoxSelect": "Ṭṭef CtrlOrCmd akken ad tferneḍ s telqey, yerna ad trewleḍ i uzuɣer",
"eraserRevert": "Ssed Alt akken ad tsefsxeḍ iferdisen yettwacerḍen i tukksa"
"eraserRevert": "Ssed Alt akken ad tsefsxeḍ iferdisen yettwacerḍen i tukksa",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Ulamek abeqqeḍ n teskant",

View File

@ -219,7 +219,8 @@
"lock": "",
"penMode": "",
"link": "",
"eraser": ""
"eraser": "",
"hand": ""
},
"headings": {
"canvasActions": "",
@ -245,7 +246,8 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "",

View File

@ -219,7 +219,8 @@
"lock": "선택된 도구 유지하기",
"penMode": "펜 모드 - 터치 방지",
"link": "선택한 도형에 대해서 링크를 추가/업데이트",
"eraser": "지우개"
"eraser": "지우개",
"hand": ""
},
"headings": {
"canvasActions": "캔버스 동작",
@ -227,7 +228,7 @@
"shapes": "모양"
},
"hints": {
"canvasPanning": "캔버스를 옮기려면 마우스 휠이나 스페이스바를 누르고 드래그하기",
"canvasPanning": "",
"linearElement": "여러 점을 연결하려면 클릭하고, 직선을 그리려면 바로 드래그하세요.",
"freeDraw": "클릭 후 드래그하세요. 완료되면 놓으세요.",
"text": "팁: 선택 툴로 아무 곳이나 더블 클릭해 텍스트를 추가할 수도 있습니다.",
@ -245,7 +246,8 @@
"publishLibrary": "당신만의 라이브러리를 게시하기",
"bindTextToElement": "Enter 키를 눌러서 텍스트 추가하기",
"deepBoxSelect": "CtrlOrCmd 키를 눌러서 깊게 선택하고, 드래그하지 않도록 하기",
"eraserRevert": "Alt를 눌러서 삭제하도록 지정된 요소를 되돌리기"
"eraserRevert": "Alt를 눌러서 삭제하도록 지정된 요소를 되돌리기",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "미리보기를 볼 수 없습니다",

View File

@ -219,7 +219,8 @@
"lock": "ئامێرە دیاریکراوەکان چالاک بهێڵەوە دوای وێنەکێشان",
"penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە",
"link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو",
"eraser": "سڕەر"
"eraser": "سڕەر",
"hand": ""
},
"headings": {
"canvasActions": "کردارەکانی تابلۆ",
@ -227,7 +228,7 @@
"shapes": "شێوەکان"
},
"hints": {
"canvasPanning": "بۆ جوڵاندنی تابلۆ، لە کاتی ڕاکێشاندا ویلی ماوس یان شریتی بۆشایی دابگرە",
"canvasPanning": "",
"linearElement": "کرتە بکە بۆ دەستپێکردنی چەند خاڵێک، ڕایبکێشە بۆ یەک هێڵ",
"freeDraw": "کرتە بکە و ڕایبکێشە، کاتێک تەواو بوویت دەست هەڵگرە",
"text": "زانیاری: هەروەها دەتوانیت دەق زیادبکەیت بە دوو کرتەکردن لە هەر شوێنێک لەگەڵ ئامڕازی دەستنیشانکردن",
@ -245,7 +246,8 @@
"publishLibrary": "کتێبخانەی تایبەت بە خۆت بڵاوبکەرەوە",
"bindTextToElement": "بۆ زیادکردنی دەق enter بکە",
"deepBoxSelect": "CtrlOrCmd ڕابگرە بۆ هەڵبژاردنی قووڵ، و بۆ ڕێگریکردن لە ڕاکێشان",
"eraserRevert": "بۆ گەڕاندنەوەی ئەو توخمانەی کە بۆ سڕینەوە نیشانە کراون، Alt ڕابگرە"
"eraserRevert": "بۆ گەڕاندنەوەی ئەو توخمانەی کە بۆ سڕینەوە نیشانە کراون، Alt ڕابگرە",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "ناتوانرێ پێشبینین پیشان بدرێت",

View File

@ -219,7 +219,8 @@
"lock": "Baigus piešti, išlaikyti pasirinktą įrankį",
"penMode": "Rašyklio režimas - neleisti prisilietimų",
"link": "Pridėti / Atnaujinti pasirinktos figūros nuorodą",
"eraser": "Trintukas"
"eraser": "Trintukas",
"hand": ""
},
"headings": {
"canvasActions": "Veiksmai su drobe",
@ -227,7 +228,7 @@
"shapes": "Figūros"
},
"hints": {
"canvasPanning": "Norint judinti drobę, judink pelę kartu įspaudus pelės ratuką arba tarpo klavišą",
"canvasPanning": "",
"linearElement": "Paspaudimai sukurs papildomus taškus, nepertraukiamas tempimas sukurs liniją",
"freeDraw": "Spausk ir tempk, paleisk kai norėsi pabaigti",
"text": "Užuomina: tekstą taip pat galima pridėti bet kur su dvigubu pelės paspaudimu, kol parinkas žymėjimo įrankis",
@ -245,7 +246,8 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "",

View File

@ -219,7 +219,8 @@
"lock": "Paturēt izvēlēto rīku pēc darbības",
"penMode": "Pildspalvas režīms novērst pieskaršanos",
"link": "Pievienot/rediģēt atlasītās figūras saiti",
"eraser": "Dzēšgumija"
"eraser": "Dzēšgumija",
"hand": ""
},
"headings": {
"canvasActions": "Tāfeles darbības",
@ -227,7 +228,7 @@
"shapes": "Formas"
},
"hints": {
"canvasPanning": "Lai bīdītu tāfeli, turiet nospiestu ritināšanas vai atstarpes taustiņu, velkot ar peli",
"canvasPanning": "",
"linearElement": "Klikšķiniet, lai sāktu zīmēt vairākus punktus; velciet, lai zīmētu līniju",
"freeDraw": "Spiediet un velciet; atlaidiet, kad pabeidzat",
"text": "Ieteikums: lai pievienotu tekstu, varat arī jebkur dubultklikšķināt ar atlases rīku",
@ -245,7 +246,8 @@
"publishLibrary": "Publicēt savu bibliotēku",
"bindTextToElement": "Spiediet ievades taustiņu, lai pievienotu tekstu",
"deepBoxSelect": "Turient nospiestu Ctrl vai Cmd, lai atlasītu dziļumā un lai nepieļautu objektu pavilkšanu",
"eraserRevert": "Turiet Alt, lai noņemtu elementus no dzēsšanas atlases"
"eraserRevert": "Turiet Alt, lai noņemtu elementus no dzēsšanas atlases",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Nevar rādīt priekšskatījumu",

View File

@ -219,7 +219,8 @@
"lock": "निवडलेले यंत्र चित्रकरण झाल्या नंतर ही सक्रिय ठेवा",
"penMode": "पेन चा मोड - स्पर्श टाळा",
"link": "निवडलेल्या आकारासाठी दुवा जोडा/बदल करा",
"eraser": "खोड रबर"
"eraser": "खोड रबर",
"hand": "हात ( सरकवण्या चे उपकरण)"
},
"headings": {
"canvasActions": "पटल क्रिया",
@ -227,7 +228,7 @@
"shapes": "आकार"
},
"hints": {
"canvasPanning": "पटल हलवण्यासाठी, ड्रग करताना माउस वील ला पकड़ा किव्हा स्पेसबार दाबून ठेवा",
"canvasPanning": "कॅनव्हास सरकवण्या साठी, ड्रग करताना माउस व्हील धरा किवा स्पेसबार दाबून ठेवा अथवा हात वालं उपकरण वापरा",
"linearElement": "अनेक बिंदु साठी क्लिक करा, रेघे साठी ड्रैग करा",
"freeDraw": "क्लिक आणि ड्रैग करा, झालं तेव्हा सोडा",
"text": "टीप: तुम्हीं निवड यंत्रानी कोठेही दुहेरी क्लिक करून टेक्स्ट जोडू शकता",
@ -245,7 +246,8 @@
"publishLibrary": "आपला खाजगी संग्रह प्रकाशित करा",
"bindTextToElement": "मजकूर जोडण्यासाठी एंटर की दाबा",
"deepBoxSelect": "खोल निवड ह्या साठी कंट्रोल किव्हा कमांड दाबून ठेवा, आणि बाहेर खेचणे वाचवण्या साठी पण",
"eraserRevert": "खोडण्या साठी घेतलेल्या वस्तु ना घेण्या साठी Alt दाबून ठेवावे"
"eraserRevert": "खोडण्या साठी घेतलेल्या वस्तु ना घेण्या साठी Alt दाबून ठेवावे",
"firefox_clipboard_write": "हे वैशिष्ट्य \"dom.events.asyncClipboard.clipboardItem\" फ्लॅग \"सत्य\" वर सेट करून शक्यतो सक्षम केले जाऊ शकते. Firefox मध्ये ब्राउझर फ्लॅग बदलण्यासाठी, \"about:config\" पृष्ठावर जा."
},
"canvasError": {
"cannotShowPreview": "पूर्वावलोकन दाखवू शकत नाही",
@ -448,15 +450,15 @@
},
"welcomeScreen": {
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
"center_heading": "तुमचा सर्व डेटा तुमच्या ब्राउझरमध्ये स्थानिक पातळीवर जतन केला जातो.",
"center_heading_plus": "त्याऐवजी तुम्हाला Excalidraw+ वर जायचे आहे का?",
"menuHint": "निर्यात, आवड़ी-निवडी, भाषा, ..."
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
"menuHint": "निर्यात, आवड़ी निवडी आणि आणकिही...",
"center_heading": "आकृत्या. काढणे. सोपे.",
"toolbarHint": "एक साधन निवडा आणि चित्रीकरण सुरु करा!",
"helpHint": "शॉर्टकट आणि सहाय्य"
}
}
}

View File

@ -219,7 +219,8 @@
"lock": "ရွေးချယ်ထားသောကိရိယာကိုသာဆက်သုံး",
"penMode": "",
"link": "",
"eraser": ""
"eraser": "",
"hand": ""
},
"headings": {
"canvasActions": "ကားချပ်လုပ်ဆောင်ချက်",
@ -245,7 +246,8 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "နမူနာမပြသနိုင်ပါ",

View File

@ -219,7 +219,8 @@
"lock": "Behold merket verktøy som aktivt",
"penMode": "Pennemodus - forhindre berøring",
"link": "Legg til / oppdater link for en valgt figur",
"eraser": "Viskelær"
"eraser": "Viskelær",
"hand": "Hånd (panoreringsverktøy)"
},
"headings": {
"canvasActions": "Handlinger: lerret",
@ -227,7 +228,7 @@
"shapes": "Former"
},
"hints": {
"canvasPanning": "For å flytte lerretet, hold musehjulet eller mellomromstasten mens du drar",
"canvasPanning": "For å flytte lerretet, hold musehjulet eller mellomromstasten mens du drar, eller bruk hånd-verktøyet",
"linearElement": "Klikk for å starte linje med flere punkter, eller dra for en enkel linje",
"freeDraw": "Klikk og dra, slipp når du er ferdig",
"text": "Tips: du kan også legge til tekst ved å dobbeltklikke hvor som helst med utvalgsverktøyet",
@ -245,7 +246,8 @@
"publishLibrary": "Publiser ditt eget bibliotek",
"bindTextToElement": "Trykk Enter for å legge til tekst",
"deepBoxSelect": "Hold CTRL/CMD for å markere dypt og forhindre flytting",
"eraserRevert": "Hold Alt for å reversere elementene merket for sletting"
"eraserRevert": "Hold Alt for å reversere elementene merket for sletting",
"firefox_clipboard_write": "Denne funksjonen kan sannsynligvis aktiveres ved å sette \"dom.events.asyncClipboard.clipboardItem\" flagget til \"true\". For å endre nettleserens flagg i Firefox, besøk \"about:config\"-siden."
},
"canvasError": {
"cannotShowPreview": "Kan ikke vise forhåndsvisning",

View File

@ -219,7 +219,8 @@
"lock": "Geselecteerde tool actief houden na tekenen",
"penMode": "Pen modus - Blokkeer aanraken",
"link": "",
"eraser": "Gum"
"eraser": "Gum",
"hand": ""
},
"headings": {
"canvasActions": "Canvasacties",
@ -227,7 +228,7 @@
"shapes": "Vormen"
},
"hints": {
"canvasPanning": "Om canvas te verplaatsen, houd muiswiel of spatiebalk ingedrukt tijdens slepen",
"canvasPanning": "",
"linearElement": "Klik om meerdere punten te starten, sleep voor één lijn",
"freeDraw": "Klik en sleep, laat los als je klaar bent",
"text": "Tip: je kunt tekst toevoegen door ergens dubbel te klikken met de selectietool",
@ -245,7 +246,8 @@
"publishLibrary": "Publiceer je eigen bibliotheek",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Kan voorbeeld niet tonen",

View File

@ -219,7 +219,8 @@
"lock": "Hald fram med valt verktøy",
"penMode": "",
"link": "Legg til/ oppdater lenke til valt figur",
"eraser": "Viskelêr"
"eraser": "Viskelêr",
"hand": ""
},
"headings": {
"canvasActions": "Handlingar: lerret",
@ -227,7 +228,7 @@
"shapes": "Formar"
},
"hints": {
"canvasPanning": "For å flytte lerretet, hald inne musehjulet eller mellomromstasten medan du dreg",
"canvasPanning": "",
"linearElement": "Klikk for å starte linje med fleire punkt, eller drag for ei enkel linje",
"freeDraw": "Klikk og drag, slepp når du er ferdig",
"text": "Tips: du kan òg leggje til tekst ved å dobbeltklikke kor som helst med utvalgsverktyet",
@ -245,7 +246,8 @@
"publishLibrary": "Publiser ditt eige bibliotek",
"bindTextToElement": "Trykk på enter for å legge til tekst",
"deepBoxSelect": "Hald inne Ctrl / Cmd for å velje djupt, og forhindre flytting",
"eraserRevert": "Hald inne Alt for å reversere markering av element for sletting"
"eraserRevert": "Hald inne Alt for å reversere markering av element for sletting",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Kan ikkje vise førehandsvising",

View File

@ -38,8 +38,8 @@
"arrowhead_bar": "Barra",
"arrowhead_dot": "Ponch",
"arrowhead_triangle": "Triangle",
"fontSize": "Talha poliça",
"fontFamily": "Familha de poliça",
"fontSize": "Talha polissa",
"fontFamily": "Familha de polissa",
"onlySelected": "Seleccion sonque",
"withBackground": "Rèireplan",
"exportEmbedScene": "Scèna embarcada",
@ -106,8 +106,8 @@
"toggleTheme": "Alternar tèma",
"personalLib": "Bibliotèca personala",
"excalidrawLib": "Bibliotèca Excalidraw",
"decreaseFontSize": "Reduire talha poliça",
"increaseFontSize": "Aumentar talha poliça",
"decreaseFontSize": "Reduire talha polissa",
"increaseFontSize": "Aumentar talha polissa",
"unbindText": "Dessociar lo tèxte",
"bindText": "Ligar lo tèxt al contenidor",
"link": {
@ -219,7 +219,8 @@
"lock": "Mantenir activa laisina aprèp dessenhar",
"penMode": "Mòde estilo - empachar lo contact",
"link": "Apondre/Actualizar lo ligam per una fòrma seleccionada",
"eraser": "Goma"
"eraser": "Goma",
"hand": ""
},
"headings": {
"canvasActions": "Accions del canabàs",
@ -227,7 +228,7 @@
"shapes": "Formas"
},
"hints": {
"canvasPanning": "Per desplaçar los canabasses, tenètz la rodeta de la mirga o la barra despaci pendent lo desplaçament",
"canvasPanning": "",
"linearElement": "Clicatz per començar mantun punt, lisatz per una sola linha",
"freeDraw": "Clicatz e lisatz, relargatz un còp acabat",
"text": "Astúcia: podètz tanben apondre de tèxt en doble clicant ont que siá amb laisina de seleccion",
@ -245,7 +246,8 @@
"publishLibrary": "Publicar vòstra pròpria bibliotèca",
"bindTextToElement": "Quichatz Entrada per apondre de tèxte",
"deepBoxSelect": "Gardar CtrlOCmd per una seleccion gropada e empachar lo desplaçament",
"eraserRevert": "Tenètz quichat Alt per anullar los elements marcats per supression"
"eraserRevert": "Tenètz quichat Alt per anullar los elements marcats per supression",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Afichatge impossible de lapercebut",

View File

@ -219,7 +219,8 @@
"lock": "ਡਰਾਇੰਗ ਤੋਂ ਬਾਅਦ ਵੀ ਚੁਣੇ ਹੋਏ ਸੰਦ ਨੂੰ ਸਰਗਰਮ ਰੱਖੋ ",
"penMode": "",
"link": "",
"eraser": "ਰਬੜ"
"eraser": "ਰਬੜ",
"hand": ""
},
"headings": {
"canvasActions": "ਕੈਨਵਸ ਦੀਆਂ ਕਾਰਵਾਈਆਂ",
@ -245,7 +246,8 @@
"publishLibrary": "ਆਪਣੀ ਲਾਇਬ੍ਰੇਰੀ ਪ੍ਰਕਾਸ਼ਿਤ ਕਰੋ",
"bindTextToElement": "ਪਾਠ ਜੋੜਨ ਲਈ ਐੰਟਰ ਦਬਾਓ",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "ਝਲਕ ਨਹੀਂ ਦਿਖਾ ਸਕਦੇ",

View File

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

View File

@ -219,7 +219,8 @@
"lock": "Zablokuj wybrane narzędzie",
"penMode": "",
"link": "",
"eraser": "Gumka"
"eraser": "Gumka",
"hand": ""
},
"headings": {
"canvasActions": "Narzędzia",
@ -227,7 +228,7 @@
"shapes": "Kształty"
},
"hints": {
"canvasPanning": "Aby przesunąć płótno, przytrzymaj kółko myszy lub spację podczas przeciągania",
"canvasPanning": "",
"linearElement": "Naciśnij, aby zrobić punkt, przeciągnij, aby narysować linię",
"freeDraw": "Naciśnij i przeciągnij by rysować, puść kiedy skończysz",
"text": "Wskazówka: możesz również dodać tekst klikając dwukrotnie gdziekolwiek za pomocą narzędzia zaznaczania",
@ -245,7 +246,8 @@
"publishLibrary": "Opublikuj własną bibliotekę",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Nie można wyświetlić podglądu",

View File

@ -219,7 +219,8 @@
"lock": "Manter ativa a ferramenta selecionada após desenhar",
"penMode": "Modo caneta — impede o toque",
"link": "Adicionar/Atualizar link para uma forma selecionada",
"eraser": "Borracha"
"eraser": "Borracha",
"hand": ""
},
"headings": {
"canvasActions": "Ações da tela",
@ -227,7 +228,7 @@
"shapes": "Formas"
},
"hints": {
"canvasPanning": "Para mover a tela, segure a roda do mouse ou a barra de espaço enquanto arrasta",
"canvasPanning": "",
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
"freeDraw": "Toque e arraste, solte quando terminar",
"text": "Dica: você também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
@ -245,7 +246,8 @@
"publishLibrary": "Publicar sua própria biblioteca",
"bindTextToElement": "Pressione Enter para adicionar o texto",
"deepBoxSelect": "Segure Ctrl/Cmd para seleção profunda e para evitar arrastar",
"eraserRevert": "Segure a tecla Alt para inverter os elementos marcados para exclusão"
"eraserRevert": "Segure a tecla Alt para inverter os elementos marcados para exclusão",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Não é possível mostrar pré-visualização",

View File

@ -219,7 +219,8 @@
"lock": "Manter a ferramenta selecionada ativa após desenhar",
"penMode": "Modo caneta - impedir toque",
"link": "Acrescentar/ Adicionar ligação para uma forma seleccionada",
"eraser": "Borracha"
"eraser": "Borracha",
"hand": ""
},
"headings": {
"canvasActions": "Ações da área de desenho",
@ -227,7 +228,7 @@
"shapes": "Formas"
},
"hints": {
"canvasPanning": "Para mover a tela, carregue na roda do rato ou na barra de espaço enquanto arrasta",
"canvasPanning": "",
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
"freeDraw": "Clique e arraste, large quando terminar",
"text": "Dica: também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
@ -245,7 +246,8 @@
"publishLibrary": "Publique a sua própria biblioteca",
"bindTextToElement": "Carregue Enter para acrescentar texto",
"deepBoxSelect": "Mantenha a tecla CtrlOrCmd carregada para selecção profunda, impedindo o arrastamento",
"eraserRevert": "Carregue também em Alt para reverter os elementos marcados para serem apagados"
"eraserRevert": "Carregue também em Alt para reverter os elementos marcados para serem apagados",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Não é possível mostrar uma pré-visualização",

View File

@ -219,7 +219,8 @@
"lock": "Menține activ instrumentul selectat după desenare",
"penMode": "Mod stilou împiedică atingerea",
"link": "Adăugare/actualizare URL pentru forma selectată",
"eraser": "Radieră"
"eraser": "Radieră",
"hand": ""
},
"headings": {
"canvasActions": "Acțiuni pentru pânză",
@ -227,7 +228,7 @@
"shapes": "Forme"
},
"hints": {
"canvasPanning": "Pentru a muta pânză, ține apăsată rotița mausului sau bara de spațiu în timpul glisării",
"canvasPanning": "",
"linearElement": "Dă clic pentru a crea mai multe puncte, glisează pentru a forma o singură linie",
"freeDraw": "Dă clic pe pânză și glisează cursorul, apoi eliberează-l când ai terminat",
"text": "Sfat: poți adăuga text și dând dublu clic oriunde cu instrumentul de selecție",
@ -245,7 +246,8 @@
"publishLibrary": "Publică propria bibliotecă",
"bindTextToElement": "Apasă tasta Enter pentru a adăuga text",
"deepBoxSelect": "Ține apăsată tasta Ctrl sau Cmd pentru a efectua selectarea de adâncime și pentru a preveni glisarea",
"eraserRevert": "Ține apăsată tasta Alt pentru a anula elementele marcate pentru ștergere"
"eraserRevert": "Ține apăsată tasta Alt pentru a anula elementele marcate pentru ștergere",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Nu se poate afișa previzualizarea",

View File

@ -219,7 +219,8 @@
"lock": "Сохранять выбранный инструмент активным после рисования",
"penMode": "Режим пера - предотвращение касания",
"link": "Добавить/обновить ссылку для выбранной фигуры",
"eraser": "Ластик"
"eraser": "Ластик",
"hand": "Рука (перемещение холста)"
},
"headings": {
"canvasActions": "Операции холста",
@ -227,7 +228,7 @@
"shapes": "Фигуры"
},
"hints": {
"canvasPanning": "Чтобы перемещать холст, удерживайте колесо мыши или пробел во время перетаскивания",
"canvasPanning": "Чтобы двигать холст, удерживайте колесо мыши или пробел во время перетаскивания, или используйте инструмент \"Рука\"",
"linearElement": "Нажмите, чтобы начать несколько точек, перетащите для одной линии",
"freeDraw": "Нажмите и перетаскивайте, отпустите по завершении",
"text": "Совет: при выбранном инструменте выделения дважды щёлкните в любом месте, чтобы добавить текст",
@ -245,7 +246,8 @@
"publishLibrary": "Опубликовать свою собственную библиотеку",
"bindTextToElement": "Нажмите Enter для добавления текста",
"deepBoxSelect": "Удерживайте Ctrl или Cmd для глубокого выделения, чтобы предотвратить перетаскивание",
"eraserRevert": "Удерживайте Alt, чтобы вернуть элементы, отмеченные для удаления"
"eraserRevert": "Удерживайте Alt, чтобы вернуть элементы, отмеченные для удаления",
"firefox_clipboard_write": "Эта функция может быть включена при изменении значения флага \"dom.events.asyncClipboard.clipboardItem\" на \"true\". Чтобы изменить флаги браузера в Firefox, посетите страницу \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Не удается отобразить предпросмотр",
@ -448,15 +450,15 @@
},
"welcomeScreen": {
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
"center_heading": "Все ваши данные сохраняются локально в вашем браузере.",
"center_heading_plus": "Хотите перейти на Excalidraw+?",
"menuHint": "Экспорт, настройки, языки, ..."
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
"menuHint": "Экспорт, настройки и другое...",
"center_heading": "Диаграммы. Просто.",
"toolbarHint": "Выберите инструмент и начните рисовать!",
"helpHint": "Сочетания клавиш и помощь"
}
}
}

View File

@ -219,7 +219,8 @@
"lock": "",
"penMode": "",
"link": "",
"eraser": ""
"eraser": "",
"hand": ""
},
"headings": {
"canvasActions": "",
@ -245,7 +246,8 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "",

View File

@ -219,7 +219,8 @@
"lock": "Nechať zvolený nástroj aktívny po skončení kreslenia",
"penMode": "Režim pera zabrániť dotyku",
"link": "Pridať/ Upraviť odkaz pre vybraný tvar",
"eraser": "Guma"
"eraser": "Guma",
"hand": "Ruka (nástroj pre pohyb plátna)"
},
"headings": {
"canvasActions": "Akcie plátna",
@ -227,7 +228,7 @@
"shapes": "Tvary"
},
"hints": {
"canvasPanning": "Pre pohyb plátna podržte koliesko myši alebo medzerník počas ťahania",
"canvasPanning": "Pre pohyb plátna podržte koliesko myši alebo medzerník počas ťahania, alebo použite nástroj ruka",
"linearElement": "Kliknite na vloženie viacerých bodov, potiahnite na vytvorenie jednej priamky",
"freeDraw": "Kliknite a ťahajte, pustite na ukončenie",
"text": "Tip: text môžete pridať aj dvojklikom kdekoľvek, ak je zvolený nástroj výber",
@ -245,7 +246,8 @@
"publishLibrary": "Uverejniť vašu knižnicu",
"bindTextToElement": "Stlačte enter na pridanie textu",
"deepBoxSelect": "Podržte CtrlOrCmd na výber v skupine alebo zamedzeniu poťiahnutia",
"eraserRevert": "Podržte Alt pre prehodenie položiek určených na vymazanie"
"eraserRevert": "Podržte Alt pre prehodenie položiek určených na vymazanie",
"firefox_clipboard_write": "Táto sa funkcionalita sa dá zapnúť nastavením \"dom.events.asyncClipboard.clipboardItem\" na \"true\". Pre zmenu nastavení vo Firefox-e otvorte stránku \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Nie je možné zobraziť náhľad plátna",

View File

@ -219,7 +219,8 @@
"lock": "Ohrani izbrano orodje aktivno po risanju",
"penMode": "Način peresa - prepreči dotik",
"link": "Dodaj/posodobi povezavo za izbrano obliko",
"eraser": "Radirka"
"eraser": "Radirka",
"hand": "Roka (orodje za premikanje)"
},
"headings": {
"canvasActions": "Dejanja za platno",
@ -227,7 +228,7 @@
"shapes": "Oblike"
},
"hints": {
"canvasPanning": "Za premik platna med vlečenjem držite kolesce miške ali preslednico",
"canvasPanning": "Za premikanje platna med vlečenjem držite kolesce miške ali preslednico ali uporabite orodje roka",
"linearElement": "Kliknite za začetek več točk, povlecite za posamezno črto",
"freeDraw": "Kliknite in povlecite, spustite, ko končate",
"text": "Namig: besedilo lahko dodate tudi z dvoklikom kjer koli z orodjem za izbiro",
@ -245,7 +246,8 @@
"publishLibrary": "Objavi svojo knjižnico",
"bindTextToElement": "Pritisnite tipko Enter za dodajanje besedila",
"deepBoxSelect": "Držite tipko CtrlOrCmd za globoko izbiro in preprečitev vlečenja",
"eraserRevert": "Pridržite tipko Alt, da razveljavite elemente, označene za brisanje"
"eraserRevert": "Pridržite tipko Alt, da razveljavite elemente, označene za brisanje",
"firefox_clipboard_write": "To funkcijo lahko verjetno omogočite z nastavitvijo zastavice \"dom.events.asyncClipboard.clipboardItem\" na \"true\". Če želite spremeniti zastavice brskalnika v Firefoxu, obiščite stran \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Predogleda ni bilo mogoče prikazati",

View File

@ -1,7 +1,7 @@
{
"labels": {
"paste": "Klistra in",
"pasteAsPlaintext": "",
"pasteAsPlaintext": "Klistra som oformaterad text",
"pasteCharts": "Klistra in diagram",
"selectAll": "Markera alla",
"multiSelect": "Lägg till element till markering",
@ -202,8 +202,8 @@
"invalidSVGString": "Ogiltig SVG.",
"cannotResolveCollabServer": "Det gick inte att ansluta till samarbets-servern. Ladda om sidan och försök igen.",
"importLibraryError": "Kunde inte ladda bibliotek",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
"collabSaveFailed": "Det gick inte att spara i backend-databasen. Om problemen kvarstår bör du spara filen lokalt för att se till att du inte förlorar ditt arbete.",
"collabSaveFailed_sizeExceeded": "Det gick inte att spara till backend-databasen, whiteboarden verkar vara för stor. Du bör spara filen lokalt för att du inte ska förlora ditt arbete."
},
"toolBar": {
"selection": "Markering",
@ -219,7 +219,8 @@
"lock": "Håll valt verktyg aktivt efter ritande",
"penMode": "Pennläge - förhindra touch",
"link": "Lägg till / Uppdatera länk för en vald form",
"eraser": "Radergummi"
"eraser": "Radergummi",
"hand": "Hand (panoreringsverktyg)"
},
"headings": {
"canvasActions": "Canvas-åtgärder",
@ -227,7 +228,7 @@
"shapes": "Former"
},
"hints": {
"canvasPanning": "För att flytta canvas, håll mushjulet eller mellanslagstangenten medan du drar",
"canvasPanning": "För att flytta whiteboarden, håll mushjulet eller mellanslagstangenten medan du drar eller använd handverktyget",
"linearElement": "Klicka för att starta flera punkter, dra för en linje",
"freeDraw": "Klicka och dra, släpp när du är klar",
"text": "Tips: du kan också lägga till text genom att dubbelklicka var som helst med markeringsverktyget",
@ -238,14 +239,15 @@
"resize": "Du kan behålla proportioner genom att hålla SHIFT medan du ändrar storlek,\nhåller du ALT ändras storlek relativt mitten",
"resizeImage": "Du kan ändra storlek fritt genom att hålla SHIFT,\nhåll ALT för att ändra storlek från mitten",
"rotate": "Du kan begränsa vinklar genom att hålla SHIFT medan du roterar",
"lineEditor_info": "",
"lineEditor_info": "Håll Ctrl/Cmd och dubbelklicka eller tryck på Ctrl/Cmd + Enter för att redigera punkter",
"lineEditor_pointSelected": "Tryck på Ta bort för att ta bort punkt(er), Ctrl + D eller Cmd + D för att duplicera, eller dra för att flytta",
"lineEditor_nothingSelected": "Välj en punkt att redigera (håll SHIFT för att välja flera),\neller håll ned Alt och klicka för att lägga till nya punkter",
"placeImage": "Klicka för att placera bilden, eller klicka och dra för att ställa in dess storlek manuellt",
"publishLibrary": "Publicera ditt eget bibliotek",
"bindTextToElement": "Tryck på Enter för att lägga till text",
"deepBoxSelect": "Håll Ctrl eller Cmd för att djupvälja, och för att förhindra att dra",
"eraserRevert": "Håll Alt för att återställa de element som är markerade för borttagning"
"eraserRevert": "Håll Alt för att återställa de element som är markerade för borttagning",
"firefox_clipboard_write": "Denna funktion kan sannolikt aktiveras genom att ställa in \"dom.events.asyncClipboard.clipboardItem\" flaggan till \"true\". För att ändra webbläsarens flaggor i Firefox, besök \"about:config\" sidan."
},
"canvasError": {
"cannotShowPreview": "Kan inte visa förhandsgranskning",
@ -314,8 +316,8 @@
"zoomToFit": "Zooma för att rymma alla element",
"zoomToSelection": "Zooma till markering",
"toggleElementLock": "Lås/Lås upp valda",
"movePageUpDown": "",
"movePageLeftRight": ""
"movePageUpDown": "Flytta sida upp/ner",
"movePageLeftRight": "Flytta sida vänster/höger"
},
"clearCanvasDialog": {
"title": "Rensa canvas"
@ -397,7 +399,7 @@
"fileSavedToFilename": "Sparad till {filename}",
"canvas": "canvas",
"selection": "markering",
"pasteAsSingleElement": ""
"pasteAsSingleElement": "Använd {{shortcut}} för att klistra in som ett enda element,\neller klistra in i en befintlig textredigerare"
},
"colors": {
"ffffff": "Vit",
@ -448,15 +450,15 @@
},
"welcomeScreen": {
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
"center_heading": "All data sparas lokalt i din webbläsare.",
"center_heading_plus": "Ville du gå till Excalidraw+ istället?",
"menuHint": "Exportera, inställningar, språk, ..."
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
"menuHint": "Exportera, inställningar och mer...",
"center_heading": "Förenklade. Diagram.",
"toolbarHint": "Välj ett verktyg & börja rita!",
"helpHint": "Genvägar & hjälp"
}
}
}

View File

@ -219,7 +219,8 @@
"lock": "தேர்ந்த கருவியை வரைந்த பின்பும் வைத்திரு",
"penMode": "",
"link": "தேர்தெடுத்த வடிவத்திற்குத் தொடுப்பைச் சேர்/ புதுப்பி",
"eraser": "அழிப்பி"
"eraser": "அழிப்பி",
"hand": ""
},
"headings": {
"canvasActions": "கித்தான் செயல்கள்",
@ -227,7 +228,7 @@
"shapes": "வடிவங்கள்"
},
"hints": {
"canvasPanning": "கித்தானை நகர்த்த, பிடித்திழுக்கையில் சுட்டிச்சக்கரத்தை அ இடைவெளிப்பட்டையை அழுத்திப்பிடி",
"canvasPanning": "",
"linearElement": "பல புள்ளிகளைத் துவக்க சொடுக்கு, ஒற்றை வரிக்கு பிடித்திழு",
"freeDraw": "சொடுக்கி பிடித்திழு, முடித்ததும் விடுவி",
"text": "துணுக்குதவி: தெரிவு கருவி கொண்டு எங்காவது இரு-சொடுக்கி உரையைச் சேர்க்கலாம்",
@ -245,7 +246,8 @@
"publishLibrary": "உம் சொந்த நூலகத்தைப் பிரசுரி",
"bindTextToElement": "உரையைச் சேர்க்க enterஐ அழுத்து",
"deepBoxSelect": "ஆழ்ந்துத் தேரவும் பிடித்திழுத்தலைத் தவிர்க்கவும் CtrlOrCmdஐ அழுத்திப்பிடி",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "முன்னோட்டம் காட்ட இயலவில்லை",

View File

@ -219,7 +219,8 @@
"lock": "Seçilen aracı çizimden sonra aktif tut",
"penMode": "Kalem modu - dokunmayı engelle",
"link": "Seçilen şekil için bağlantı Ekle/Güncelle",
"eraser": "Silgi"
"eraser": "Silgi",
"hand": ""
},
"headings": {
"canvasActions": "Tuval eylemleri",
@ -227,7 +228,7 @@
"shapes": "Şekiller"
},
"hints": {
"canvasPanning": "Tuvali taşımak için, tuvali sürüklerken aynı zamanda fare tekerleğine veya boşluk tuşuna basılı tutun",
"canvasPanning": "",
"linearElement": "Birden fazla nokta için tıklayın, tek çizgi için sürükleyin",
"freeDraw": "Tıkla ve sürükle, bitirdiğinde serbest bırak",
"text": "İpucu: seçme aracıyla herhangi bir yere çift tıklayarak da yazı ekleyebilirsin",
@ -245,7 +246,8 @@
"publishLibrary": "Kendi kitaplığınızı yayınlayın",
"bindTextToElement": "Enter tuşuna basarak metin ekleyin",
"deepBoxSelect": "Ctrl/Cmd tuşuna basılı tutarak derin seçim yapın ya da sürüklemeyi engelleyin",
"eraserRevert": "Alt tuşuna basılı tutarak silinme için işaretlenmiş ögeleri tersine çevirin"
"eraserRevert": "Alt tuşuna basılı tutarak silinme için işaretlenmiş ögeleri tersine çevirin",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Önizleme gösterilemiyor",

View File

@ -219,7 +219,8 @@
"lock": "Залишити обраний інструмент після креслення",
"penMode": "Режим пера - запобігання дотику",
"link": "Додати/Оновити посилання для вибраної форми",
"eraser": "Очищувач"
"eraser": "Очищувач",
"hand": ""
},
"headings": {
"canvasActions": "Дії з полотном",
@ -227,7 +228,7 @@
"shapes": "Фігури"
},
"hints": {
"canvasPanning": "Щоб перемістити полотно, утримуйте коліщатко миші або пробіл під час перетягування",
"canvasPanning": "",
"linearElement": "Натисніть щоб додати кілька точок. Перетягніть щоб намалювати одну лінію",
"freeDraw": "Натисніть і потягніть, відпустіть коли завершите",
"text": "Порада: можна також додати текст, двічі клацнувши по будь-якому місці інструментом вибору",
@ -245,7 +246,8 @@
"publishLibrary": "Опублікувати свою власну бібліотеку",
"bindTextToElement": "Натисніть Enter, щоб додати текст",
"deepBoxSelect": "Втримуйте Ctrl/Cmd для глибокого виділення та щоб попередити перетягування",
"eraserRevert": "Втримуйте клавішу Alt, щоб повернути елементи позначені для видалення"
"eraserRevert": "Втримуйте клавішу Alt, щоб повернути елементи позначені для видалення",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Не вдається показати попередній перегляд",

View File

@ -66,9 +66,9 @@
"cartoonist": "Hoạt hình",
"fileTitle": "",
"colorPicker": "Chọn màu",
"canvasColors": "",
"canvasBackground": "",
"drawingCanvas": "",
"canvasColors": "Đã dùng trên canvas",
"canvasBackground": "Nền canvas",
"drawingCanvas": "Canvas vẽ",
"layers": "Lớp",
"actions": "Chức năng",
"language": "Ngôn ngữ",
@ -219,7 +219,8 @@
"lock": "",
"penMode": "",
"link": "",
"eraser": ""
"eraser": "",
"hand": ""
},
"headings": {
"canvasActions": "",
@ -245,7 +246,8 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "",

View File

@ -219,7 +219,8 @@
"lock": "绘制后保持所选的工具栏状态",
"penMode": "笔模式 避免误触",
"link": "为选中的形状添加/更新链接",
"eraser": "橡皮"
"eraser": "橡皮",
"hand": "抓手(平移工具)"
},
"headings": {
"canvasActions": "画布动作",
@ -227,7 +228,7 @@
"shapes": "形状"
},
"hints": {
"canvasPanning": "要移动画布,请按住鼠标滚轮或空格键,再拖拽鼠标",
"canvasPanning": "要移动画布,请按住鼠标滚轮或空格键同时拖拽鼠标,或使用抓手工具。",
"linearElement": "点击创建多个点 拖动创建直线",
"freeDraw": "点击并拖动,完成时松开",
"text": "提示:您也可以使用选择工具双击任意位置来添加文字",
@ -245,7 +246,8 @@
"publishLibrary": "发布您自己的素材库",
"bindTextToElement": "按下 Enter 以添加文本",
"deepBoxSelect": "按住 CtrlOrCmd 以深度选择,并避免拖拽",
"eraserRevert": "按住 Alt 以反选被标记删除的元素"
"eraserRevert": "按住 Alt 以反选被标记删除的元素",
"firefox_clipboard_write": "将高级配置首选项“dom.events.asyncClipboard.lipboarditem”设置为“true”可以启用此功能。要更改 Firefox 的高级配置首选项请前往“about:config”页面。"
},
"canvasError": {
"cannotShowPreview": "无法显示预览",

View File

@ -219,7 +219,8 @@
"lock": "",
"penMode": "",
"link": "",
"eraser": ""
"eraser": "",
"hand": ""
},
"headings": {
"canvasActions": "畫布動作",
@ -245,7 +246,8 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": ""
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "無法顯示預覽",

View File

@ -219,7 +219,8 @@
"lock": "可連續使用選取的工具",
"penMode": "筆模式 - 避免觸摸",
"link": "為所選的形狀增加\b/更新連結",
"eraser": "橡皮擦"
"eraser": "橡皮擦",
"hand": "手形(平移工具)"
},
"headings": {
"canvasActions": "canvas 動作",
@ -227,7 +228,7 @@
"shapes": "形狀"
},
"hints": {
"canvasPanning": "若要移動畫布,請在拖曳時按住滑鼠滾輪或空白鍵",
"canvasPanning": "若要移動畫布,請在拖曳時按住滑鼠滾輪或空白鍵,或使用手形工具",
"linearElement": "點擊以繪製多點曲線;或拖曳以繪製直線",
"freeDraw": "點擊並拖曳來繪圖,放開即結束",
"text": "提示:亦可使用選取工具在任何地方雙擊來加入文字",
@ -245,7 +246,8 @@
"publishLibrary": "發布個人資料庫",
"bindTextToElement": "按下 Enter 以加入文字。",
"deepBoxSelect": "按住 Ctrl 或 Cmd 以深度選取並避免拖曳",
"eraserRevert": "按住 Alt 以反選取已標記待刪除的元素"
"eraserRevert": "按住 Alt 以反選取已標記待刪除的元素",
"firefox_clipboard_write": "此功能有機會透過將 \"dom.events.asyncClipboard.clipboardItem\" 設定為 \"true\" 來開啟。\n若要變更 Firefox 瀏覽器的此設定值,請至 \"about:config\" 頁面。"
},
"canvasError": {
"cannotShowPreview": "無法顯示預覽",

View File

@ -459,3 +459,15 @@ export const mapIntervalToBezierT = (
export const arePointsEqual = (p1: Point, p2: Point) => {
return p1[0] === p2[0] && p1[1] === p2[1];
};
export const isRightAngle = (angle: number) => {
// if our angles were mathematically accurate, we could just check
//
// angle % (Math.PI / 2) === 0
//
// but since we're in floating point land, we need to round.
//
// Below, after dividing by Math.PI, a multiple of 0.5 indicates a right
// angle, which we can check with modulo after rounding.
return Math.round((angle / Math.PI) * 10000) % 5000 === 0;
};

View File

@ -11,6 +11,24 @@ 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.
-->
## Unreleased
### Features
- [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) API now takes an optional parameter `opts` which currently supports the below attributes
```js
{ refreshDimensions?: boolean, repair?: boolean }
```
The same `opts` param has been added to [`restore`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restore) API as well.
For more details refer to the [docs](https://docs.excalidraw.com)
#### BREAKING CHANGE
- The optional parameter `refreshDimensions` in [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) has been removed and can be enabled via `opts`
## 0.14.2 (2023-02-01)
### Features

View File

@ -80,7 +80,13 @@ const COMMENT_ICON_DIMENSION = 32;
const COMMENT_INPUT_HEIGHT = 50;
const COMMENT_INPUT_WIDTH = 150;
export default function App() {
export interface AppProps {
appTitle: string;
useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
customArgs?: any[];
}
export default function App({ appTitle, useCustom, customArgs }: AppProps) {
const appRef = useRef<any>(null);
const [viewModeEnabled, setViewModeEnabled] = useState(false);
const [zenModeEnabled, setZenModeEnabled] = useState(false);
@ -107,6 +113,8 @@ export default function App() {
const [excalidrawAPI, setExcalidrawAPI] =
useState<ExcalidrawImperativeAPI | null>(null);
useCustom(excalidrawAPI, customArgs);
useHandleLibrary({ excalidrawAPI });
useEffect(() => {
@ -114,7 +122,7 @@ export default function App() {
return;
}
const fetchData = async () => {
const res = await fetch("/rocket.jpeg");
const res = await fetch("/images/rocket.jpeg");
const imageData = await res.blob();
const reader = new FileReader();
reader.readAsDataURL(imageData);
@ -150,7 +158,7 @@ export default function App() {
/>
)}
<button
onClick={() => alert("This is dummy top right UI")}
onClick={() => alert("This is an empty top right UI")}
style={{ height: "2.5rem" }}
>
{" "}
@ -397,7 +405,7 @@ export default function App() {
}}
>
<div className="comment-avatar">
<img src="doremon.png" alt="doremon" />
<img src="images/doremon.png" alt="doremon" />
</div>
</div>
);
@ -525,7 +533,7 @@ export default function App() {
};
return (
<div className="App" ref={appRef}>
<h1> Excalidraw Example</h1>
<h1>{appTitle}</h1>
<ExampleSidebar>
<div className="button-wrapper">
<button onClick={loadSceneOrLibrary}>Load Scene or Library</button>
@ -611,15 +619,15 @@ export default function App() {
const collaborators = new Map();
collaborators.set("id1", {
username: "Doremon",
avatarUrl: "doremon.png",
avatarUrl: "images/doremon.png",
});
collaborators.set("id2", {
username: "Excalibot",
avatarUrl: "excalibot.png",
avatarUrl: "images/excalibot.png",
});
collaborators.set("id3", {
username: "Pika",
avatarUrl: "pika.jpeg",
avatarUrl: "images/pika.jpeg",
});
collaborators.set("id4", {
username: "fallback",

View File

@ -8,6 +8,9 @@ const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
<App
appTitle={"Excalidraw Example"}
useCustom={(api: any, args?: any[]) => {}}
/>
</StrictMode>,
);

View File

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -10,8 +10,8 @@ export default function Sidebar({ children }: { children: React.ReactNode }) {
x
</button>
<div className="sidebar-links">
<button>Dummy Home</button>
<button>Dummy About</button>{" "}
<button>Empty Home</button>
<button>Empty About</button>{" "}
</div>
</div>
<div className={`${open ? "sidebar-open" : ""}`}>

View File

@ -27,7 +27,7 @@ import { RoughGenerator } from "roughjs/bin/generator";
import { RenderConfig } from "../scene/types";
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
import { getCornerRadius, isPathALoop } from "../math";
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
import rough from "roughjs/bin/rough";
import { AppState, BinaryFiles, Zoom } from "../types";
import { getDefaultAppState } from "../appState";
@ -989,7 +989,33 @@ export const renderElement = (
element,
renderConfig,
);
const currentImageSmoothingStatus = context.imageSmoothingEnabled;
if (
// do not disable smoothing during zoom as blurry shapes look better
// on low resolution (while still zooming in) than sharp ones
!renderConfig?.shouldCacheIgnoreZoom &&
// angle is 0 -> always disable smoothing
(!element.angle ||
// or check if angle is a right angle in which case we can still
// disable smoothing without adversely affecting the result
isRightAngle(element.angle))
) {
// Disabling smoothing makes output much sharper, especially for
// text. Unless for non-right angles, where the aliasing is really
// terrible on Chromium.
//
// Note that `context.imageSmoothingQuality="high"` has almost
// zero effect.
//
context.imageSmoothingEnabled = false;
}
drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig);
// reset
context.imageSmoothingEnabled = currentImageSmoothingStatus;
}
break;
}

View File

@ -37,7 +37,10 @@ import {
getSelectedGroupIds,
getElementsInGroup,
} from "../groups";
import { maxBindingGap } from "../element/collision";
import {
isHittingElementNotConsideringBoundingBox,
maxBindingGap,
} from "../element/collision";
import {
SuggestedBinding,
SuggestedPointBinding,
@ -60,6 +63,8 @@ import {
getLinkHandleFromCoords,
} from "../element/Hyperlink";
import { isLinearElement } from "../element/typeChecks";
import { rotatePoint } from "../math";
import { isHittingContainerStroke } from "../element/textElement";
const hasEmojiSupport = supportsEmoji();
export const DEFAULT_SPACING = 2;
@ -407,9 +412,44 @@ export const _renderScene = ({
let editingLinearElement: NonDeleted<ExcalidrawLinearElement> | undefined =
undefined;
let idx = -1;
visibleElements.forEach((element) => {
idx++;
try {
const useProdAlgo = idx % 2 === 0;
context.fillStyle = useProdAlgo ? "lime" : "red";
const padding = 40 / renderConfig.zoom.value;
const bounds = getCommonBounds([element]);
const box = [
bounds[0] + renderConfig.scrollX,
bounds[1] + renderConfig.scrollY,
bounds[2] + renderConfig.scrollX,
bounds[3] + renderConfig.scrollY,
];
for (let x = box[0] - padding; x < box[2] + padding; x++) {
for (let y = box[1] - padding; y < box[3] + padding; y++) {
const sceneX = x - renderConfig.scrollX;
const sceneY = y - renderConfig.scrollY;
if (
useProdAlgo
? isHittingElementNotConsideringBoundingBox(element, appState, [
sceneX,
sceneY,
])
: isHittingContainerStroke(
sceneX,
sceneY,
// @ts-ignore
element,
renderConfig.zoom.value,
)
) {
context.fillRect(x, y, 1, 1);
}
}
}
renderElement(element, rc, context, renderConfig, appState);
// Getting the element using LinearElementEditor during collab mismatches version - being one head of visible elements due to
// ShapeCache returns empty hence making sure that we get the
// correct element from visible elements

View File

@ -0,0 +1,45 @@
import ExcalidrawApp from "../excalidraw-app";
import {
mockBoundingClientRect,
render,
restoreOriginalGetBoundingClientRect,
} from "./test-utils";
import { UI } from "./helpers/ui";
describe("Test MobileMenu", () => {
const { h } = window;
const dimensions = { height: 400, width: 800 };
beforeEach(async () => {
await render(<ExcalidrawApp />);
//@ts-ignore
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
});
beforeAll(() => {
mockBoundingClientRect(dimensions);
});
afterAll(() => {
restoreOriginalGetBoundingClientRect();
});
it("should set device correctly", () => {
expect(h.app.device).toMatchInlineSnapshot(`
Object {
"canDeviceFitSidebar": false,
"isMobile": true,
"isSmScreen": false,
"isTouchScreen": false,
}
`);
});
it("should initialize with welcome screen and hide once user interacts", async () => {
expect(document.querySelector(".welcome-screen-center")).toMatchSnapshot();
UI.clickTool("rectangle");
expect(document.querySelector(".welcome-screen-center")).toBeNull();
});
});

View File

@ -0,0 +1,240 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Test MobileMenu should initialize with welcome screen and hide once user interacts 1`] = `
<div
class="welcome-screen-center"
>
<div
class="welcome-screen-center__logo virgil welcome-screen-decor"
>
<svg
aria-hidden="true"
class=""
fill="none"
focusable="false"
role="img"
viewBox="0 0 26 62"
>
<g
fill="currentColor"
>
<path
d="M24.296 12.214c0 .112-.134.224-.291.224-.135 0-.516.629-.808 1.392-.897 2.335-9.867 20.096-9.89 19.534 0-.292-.134-.494-.359-.494-.313 0-.358.18-.224 1.055.135 1.01.045 1.236-3.14 7.432-1.793 3.525-3.722 7.208-4.282 8.196-.584 1.032-1.032 2.155-1.077 2.626-.067.809.022.92 1.973 2.605 1.122.988 2.557 2.223 3.185 2.784 2.826 2.582 4.149 3.615 4.508 3.547.538-.09 8.858-8.823 8.88-9.317 0-.225-.403-3.638-.897-7.59-.852-6.735-1.66-14.616-1.57-15.38.068-.47-.269-2.85-.516-3.884-.201-.808-.112-1.145 1.503-4.827.942-2.178 2.176-4.85 2.714-5.928.515-1.077.964-2.02.964-2.088 0-.067-.157-.112-.336-.112-.18 0-.337.09-.337.225Zm-5.158 16.772c.247 1.572.74 5.344 1.099 8.375.695 5.568 1.503 11.742 1.727 13.314.135.786.045.943-1.413 2.56-2.534 2.851-5.225 5.658-6.145 6.376l-.852.674-4.373-4.086c-4.037-3.728-4.373-4.11-4.127-4.558a5154.2 5154.2 0 0 1 2.535-4.626 727.864 727.864 0 0 0 3.678-6.78c.784-1.46 1.502-2.717 1.637-2.785.156-.09.201 2.178.156 7.006-.09 7.207-.067 7.23.651 7.072.09 0 .157-3.637.157-8.06V35.43l2.355-4.715c1.3-2.605 2.377-4.693 2.422-4.67.045.022.27 1.347.493 2.94ZM9.562 1.818C7.903 3.143 5.346 5.388 3.328 7.32L1.735 8.823l.292 1.976c.157 1.078.449 3.188.628 4.67.202 1.482.404 2.874.47 3.077.09.269 0 .404-.246.404-.426 0-.449-.113.718 3.592.286.952.577 1.903.875 2.851.044.158.224.225.425.158.202-.09.314-.27.247-.427-.067-.18.045-.36.224-.427.247-.09.225-.269-.157-.92-.605-1.01-2.152-9.633-2.242-12.416-.067-1.976-.067-1.999.762-3.121.808-1.1 2.67-2.762 5.54-4.873.807-.605 1.614-1.28 1.839-1.504.336-.404.493-.292 3.319 2.717 1.637 1.729 3.453 3.502 4.037 3.952l1.076.808-.83 1.75c-.448.944-2.265 4.581-4.059 8.04-3.745 7.274-2.983 6.578-7.333 6.645l-2.826.023-.942 1.077c-.987 1.146-1.121 1.572-.65 2.29.18.248.313.652.313.898 0 .405.157.472 1.055.517.56.023 1.076.09 1.144.157.067.068.156 1.46.224 3.098l.09 2.965-1.503 3.232C1.735 45.422.749 47.891.749 48.7c0 .427.09.786.18.786.224 0 .224-.022 9.35-19.085a4398.495 4398.495 0 0 1 8.927-18.546c.672-1.369 1.278-2.626 1.323-2.806.045-.202-1.503-1.751-3.97-3.93-2.22-1.975-4.171-3.772-4.35-3.974-.516-.628-1.279-.426-2.647.674ZM8.441 31.231c-.18.472-.65 1.46-1.031 2.2-.629 1.258-.696 1.303-.853.786-.09-.314-.157-1.235-.18-2.066-.022-1.639-.067-1.616 1.817-1.728L8.8 30.4l-.358.831Zm1.884-3.592c-1.032 1.998-1.077 2.02-3.903 2.155-2.489.135-2.533.112-2.533-.36 0-.269-.09-.628-.203-.808-.134-.202-.044-.56.27-1.055l.493-.763H6.69c1.234-.023 2.647-.113 3.14-.202.494-.09.92-.135.965-.113.045.023-.18.54-.471 1.146Zm-.09-20.477c-.404.292-.516.584-.516 1.325 0 .875.067 1.01.673 1.257.605.247.763.224 1.458-.247.92-.629.941-.786.269-1.796-.583-.876-1.166-1.033-1.884-.54Z"
/>
<path
clip-rule="evenodd"
d="M23.703 11.793c.166-.291.501-.514.93-.514.38 0 .698.161.82.283.161.162.225.35.225.54a.822.822 0 0 1-.056.289c-.08.218-.5 1.106-.983 2.116-.535 1.071-1.76 3.727-2.699 5.895-.79 1.802-1.209 2.784-1.404 3.416-.142.461-.132.665-.058.961.264 1.103.6 3.647.53 4.132-.088.756.727 8.547 1.57 15.21.5 3.997.903 7.45.903 7.676l-.001.033c-.004.087-.041.288-.211.54-.24.354-.914 1.143-1.8 2.119-2.004 2.21-5.107 5.423-6.463 6.653-.322.292-.566.485-.696.56a.884.884 0 0 1-.289.111c-.194.037-.579-.007-1.11-.349-.707-.453-1.981-1.522-4-3.366-.627-.561-2.061-1.794-3.176-2.776-.81-.699-1.308-1.138-1.612-1.466-.32-.343-.47-.61-.549-.87-.078-.257-.085-.515-.055-.874.05-.52.521-1.769 1.166-2.91.559-.985 2.48-4.654 4.269-8.17 1.579-3.071 2.392-4.663 2.792-5.612.32-.759.329-1 .277-1.387-.085-.553-.092-.891-.052-1.092a.942.942 0 0 1 .274-.52c.164-.157.384-.261.704-.261.094 0 .184.011.27.033 1.924-3.44 8.554-16.632 9.316-18.616.276-.724.64-1.336.848-1.556a.965.965 0 0 1 .32-.228Zm-5.399 16.402c-.49.942-.971 1.888-1.446 2.837l-2.28 4.565v7.871c0 4.023-.06 7.404-.136 8.04-.067.552-.474.691-.654.722l.075-.008c-.317.07-.574.063-.778-.023-.234-.098-.5-.297-.63-.857-.156-.681-.158-2.462-.103-6.893.019-2.022.022-3.592.008-4.725-.156.276-.315.562-.467.843a737.624 737.624 0 0 1-3.682 6.79 3618.972 3618.972 0 0 0-2.462 4.493c.062.088.169.231.289.364.55.61 1.631 1.623 3.624 3.462l3.931 3.674.377-.298c.907-.709 3.554-3.479 6.055-6.293.425-.47.73-.814.946-1.084.175-.22.28-.36.319-.501.031-.117.002-.227-.024-.379l-.004-.02c-.224-1.572-1.032-7.753-1.728-13.33-.358-3.022-.85-6.782-1.096-8.349l-.002-.01c-.042-.301-.087-.603-.132-.891ZM9.118 1.264C9.91.628 10.537.27 11.028.144c.727-.186 1.27.003 1.713.53.186.209 2.107 1.972 4.287 3.912 2.02 1.783 3.434 3.16 3.897 3.743.326.41.322.756.296.873a1.046 1.046 0 0 1-.005.018c-.047.188-.669 1.512-1.374 2.947a4348.55 4348.55 0 0 0-8.923 18.54c-7.335 15.32-8.808 18.396-9.217 19.015-.235.355-.419.404-.525.437a.815.815 0 0 1-.249.036.745.745 0 0 1-.647-.363C.176 49.67.04 49.222.04 48.7c0-.286.09-.754.316-1.434.452-1.356 1.466-3.722 3.225-7.53l1.432-3.083-.084-2.787a72.902 72.902 0 0 0-.156-2.53 7.307 7.307 0 0 0-.539-.046c-.463-.024-.764-.062-.96-.124-.304-.096-.48-.252-.598-.438-.105-.165-.17-.374-.17-.663 0-.134-.081-.348-.178-.481l-.019-.028c-.293-.448-.406-.831-.373-1.234.04-.484.34-1.052 1.08-1.91l.759-.869c-.103-.325-.471-1.513-.854-2.787-.737-2.339-1.004-3.238-1.018-3.578-.016-.393.134-.59.27-.715a.721.721 0 0 1 .192-.125 89.87 89.87 0 0 1-.414-2.782 231.651 231.651 0 0 0-.625-4.652l-.292-1.976a.71.71 0 0 1 .215-.62l1.589-1.501C4.87 4.86 7.446 2.599 9.118 1.264Zm-1.833 33.75a.819.819 0 0 1-.406.208.73.73 0 0 1-.491-.063l.048 1.618v.009l.849-1.773Zm5.874.697c-.035.087-.07.175-.107.261a20.92 20.92 0 0 1-.36.798.688.688 0 0 1 .457.007l.01.004v-1.07Zm.72-1.892-.015.018a.745.745 0 0 1-.407.236c.02.195.027.378 0 .592l.422-.846ZM7.7 31.175c-.268.027-.489.055-.6.07-.006.056-.013.13-.016.194-.005.19 0 .42.004.694.003.111.006.225.011.338.232-.471.459-.956.6-1.296Zm2.12-1.456a2.04 2.04 0 0 1-.415.31c.064.104.099.222.104.341l.132-.277.18-.374Zm-.14-2.374c-.654.079-1.882.153-2.974.173h-1.87l-.281.435c-.09.141-.17.331-.203.414.102.21.189.508.226.788h.007c.364.006.928-.023 1.805-.07 1.243-.06 1.88-.052 2.315-.291.154-.086.266-.215.387-.393.176-.261.354-.605.587-1.056Zm2.136-1.784c-.157.16-.331.3-.52.422a.631.631 0 0 1 .182.281l.337-.703Zm7.205-1.478c-.222.442-.445.883-.667 1.32a.787.787 0 0 1 .61.007c.036.018.145.07.243.2-.032-.165-.067-.33-.105-.493-.088-.351-.137-.633-.08-1.034h-.001ZM11.415 2.546c-.358.319-1.039.879-1.725 1.394C6.903 5.989 5.087 7.59 4.301 8.662c-.28.38-.458.605-.556.852-.15.38-.103.798-.068 1.824.063 1.923.833 6.669 1.493 9.686.262 1.199.483 2.11.654 2.394.25.426.364.71.398.894a.923.923 0 0 1-.184.764l1.27-.01c.863-.014 1.523.003 2.056-.019.424-.017.75-.052 1.034-.187.336-.159.596-.458.921-.955.62-.948 1.373-2.515 2.705-5.103 1.789-3.448 3.6-7.076 4.047-8.015l.582-1.227-.62-.466c-.595-.458-2.45-2.263-4.12-4.027a59.654 59.654 0 0 0-2.498-2.52ZM5.81 24.876v-.001l-.013-.03.013.031Zm-.71-.835.027-.011a.55.55 0 0 0-.028.011Zm19.904-11.777v.01-.01Zm.002-.016v-.034.034ZM9.82 6.587c-.587.424-.81.823-.81 1.9 0 .787.12 1.157.344 1.42.158.186.388.339.77.494.352.144.603.207.838.209.347.002.688-.12 1.285-.525.707-.483.98-.864 1.036-1.238.052-.352-.09-.812-.574-1.54-.412-.619-.853-.95-1.29-1.072-.489-.139-1.016-.05-1.586.342l-.013.01Zm2.015 2.028a6.288 6.288 0 0 0-.306-.52c-.19-.284-.326-.488-.531-.5-.113-.007-.224.058-.352.146-.218.159-.218.34-.218.745 0 .198.02.419.028.504.047.025.133.068.204.097.133.054.222.102.312.103.04 0 .071-.027.12-.054a4.29 4.29 0 0 0 .358-.225c.147-.1.299-.223.385-.296ZM9.12 1.263l-.002.002.002-.002Z"
fill-rule="evenodd"
/>
</g>
</svg>
Excalidraw
</div>
<div
class="welcome-screen-center__heading welcome-screen-decor virgil"
>
All your data is saved locally in your browser.
</div>
<div
class="welcome-screen-menu"
>
<button
class="welcome-screen-menu-item "
type="button"
>
<div
class="welcome-screen-menu-item__icon"
>
<svg
aria-hidden="true"
class=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
viewBox="0 0 20 20"
>
<path
d="m9.257 6.351.183.183H15.819c.34 0 .727.182 1.051.506.323.323.505.708.505 1.05v5.819c0 .316-.183.7-.52 1.035-.337.338-.723.522-1.037.522H4.182c-.352 0-.74-.181-1.058-.5-.318-.318-.499-.705-.499-1.057V5.182c0-.351.181-.736.5-1.054.32-.321.71-.503 1.057-.503H6.53l2.726 2.726Z"
stroke-width="1.25"
/>
</svg>
</div>
<div
class="welcome-screen-menu-item__text"
>
Open
</div>
<div
class="welcome-screen-menu-item__shortcut"
>
Ctrl+O
</div>
</button>
<button
class="welcome-screen-menu-item "
type="button"
>
<div
class="welcome-screen-menu-item__icon"
>
<svg
aria-hidden="true"
class=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
>
<g
stroke-width="1.5"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<circle
cx="12"
cy="12"
r="9"
/>
<line
x1="12"
x2="12"
y1="17"
y2="17.01"
/>
<path
d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4"
/>
</g>
</svg>
</div>
<div
class="welcome-screen-menu-item__text"
>
Help
</div>
<div
class="welcome-screen-menu-item__shortcut"
>
?
</div>
</button>
<button
class="welcome-screen-menu-item "
type="button"
>
<div
class="welcome-screen-menu-item__icon"
>
<svg
aria-hidden="true"
class=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
>
<g
stroke-width="1.5"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<circle
cx="9"
cy="7"
r="4"
/>
<path
d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"
/>
<path
d="M16 3.13a4 4 0 0 1 0 7.75"
/>
<path
d="M21 21v-2a4 4 0 0 0 -3 -3.85"
/>
</g>
</svg>
</div>
<div
class="welcome-screen-menu-item__text"
>
Live collaboration...
</div>
</button>
<a
class="welcome-screen-menu-item "
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
rel="noreferrer"
target="_blank"
>
<div
class="welcome-screen-menu-item__icon"
>
<svg
aria-hidden="true"
class=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
>
<g
stroke-width="1.5"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<rect
height="4"
rx="1"
width="18"
x="3"
y="8"
/>
<line
x1="12"
x2="12"
y1="8"
y2="21"
/>
<path
d="M19 12v7a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-7"
/>
<path
d="M7.5 8a2.5 2.5 0 0 1 0 -5a4.8 8 0 0 1 4.5 5a4.8 8 0 0 1 4.5 -5a2.5 2.5 0 0 1 0 5"
/>
</g>
</svg>
</div>
<div
class="welcome-screen-menu-item__text"
>
Try Excalidraw Plus!
</div>
</a>
</div>
</div>
`;

View File

@ -36,4 +36,9 @@ describe("getClientInitials", () => {
result = getClientInitials(null);
expect(result).toBe("?");
});
it('returns "?" when value is blank', () => {
const result = getClientInitials(" ");
expect(result).toBe("?");
});
});

View File

@ -534,7 +534,7 @@ describe("restore", () => {
});
describe("repairing bindings", () => {
it("should repair container boundElements", () => {
it("should repair container boundElements when repair is true", () => {
const container = API.createElement({
type: "rectangle",
boundElements: [],
@ -546,11 +546,28 @@ describe("repairing bindings", () => {
expect(container.boundElements).toEqual([]);
const restoredElements = restore.restoreElements(
let restoredElements = restore.restoreElements(
[container, boundElement],
null,
);
expect(restoredElements).toEqual([
expect.objectContaining({
id: container.id,
boundElements: [],
}),
expect.objectContaining({
id: boundElement.id,
containerId: container.id,
}),
]);
restoredElements = restore.restoreElements(
[container, boundElement],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([
expect.objectContaining({
id: container.id,
@ -563,7 +580,7 @@ describe("repairing bindings", () => {
]);
});
it("should repair containerId of boundElements", () => {
it("should repair containerId of boundElements when repair is true", () => {
const boundElement = API.createElement({
type: "text",
containerId: null,
@ -573,11 +590,28 @@ describe("repairing bindings", () => {
boundElements: [{ type: boundElement.type, id: boundElement.id }],
});
const restoredElements = restore.restoreElements(
let restoredElements = restore.restoreElements(
[container, boundElement],
null,
);
expect(restoredElements).toEqual([
expect.objectContaining({
id: container.id,
boundElements: [{ type: boundElement.type, id: boundElement.id }],
}),
expect.objectContaining({
id: boundElement.id,
containerId: null,
}),
]);
restoredElements = restore.restoreElements(
[container, boundElement],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([
expect.objectContaining({
id: container.id,
@ -620,7 +654,7 @@ describe("repairing bindings", () => {
]);
});
it("should remove bindings of deleted elements from boundElements", () => {
it("should remove bindings of deleted elements from boundElements when repair is true", () => {
const container = API.createElement({
type: "rectangle",
boundElements: [],
@ -642,6 +676,8 @@ describe("repairing bindings", () => {
type: invisibleBoundElement.type,
id: invisibleBoundElement.id,
};
expect(container.boundElements).toEqual([]);
const nonExistentBinding = { type: "text", id: "non-existent" };
// @ts-ignore
container.boundElements = [
@ -650,17 +686,28 @@ describe("repairing bindings", () => {
nonExistentBinding,
];
expect(container.boundElements).toEqual([
obsoleteBinding,
invisibleBinding,
nonExistentBinding,
]);
const restoredElements = restore.restoreElements(
let restoredElements = restore.restoreElements(
[container, invisibleBoundElement, boundElement],
null,
);
expect(restoredElements).toEqual([
expect.objectContaining({
id: container.id,
boundElements: [obsoleteBinding, invisibleBinding, nonExistentBinding],
}),
expect.objectContaining({
id: boundElement.id,
containerId: container.id,
}),
]);
restoredElements = restore.restoreElements(
[container, invisibleBoundElement, boundElement],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([
expect.objectContaining({
id: container.id,
@ -673,7 +720,7 @@ describe("repairing bindings", () => {
]);
});
it("should remove containerId if container not exists", () => {
it("should remove containerId if container not exists when repair is true", () => {
const boundElement = API.createElement({
type: "text",
containerId: "non-existent",
@ -684,11 +731,28 @@ describe("repairing bindings", () => {
isDeleted: true,
});
const restoredElements = restore.restoreElements(
let restoredElements = restore.restoreElements(
[boundElement, boundElementDeleted],
null,
);
expect(restoredElements).toEqual([
expect.objectContaining({
id: boundElement.id,
containerId: "non-existent",
}),
expect.objectContaining({
id: boundElementDeleted.id,
containerId: "non-existent",
}),
]);
restoredElements = restore.restoreElements(
[boundElement, boundElementDeleted],
null,
{ repairBindings: true },
);
expect(restoredElements).toEqual([
expect.objectContaining({
id: boundElement.id,

View File

@ -11,6 +11,7 @@ import {
} from "../actions";
import { AppState } from "../types";
import { API } from "./helpers/api";
import { selectGroupsForSelectedElements } from "../groups";
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@ -34,6 +35,7 @@ const populateElements = (
height?: number;
containerId?: string;
}[],
appState?: Partial<AppState>,
) => {
const selectedElementIds: any = {};
@ -84,6 +86,11 @@ const populateElements = (
});
h.setState({
...selectGroupsForSelectedElements(
{ ...h.state, ...appState, selectedElementIds },
h.elements,
),
...appState,
selectedElementIds,
});
@ -111,11 +118,7 @@ const assertZindex = ({
appState?: Partial<AppState>;
operations: [Actions, string[]][];
}) => {
const selectedElementIds = populateElements(elements);
h.setState({
editingGroupId: appState?.editingGroupId || null,
});
const selectedElementIds = populateElements(elements, appState);
operations.forEach(([action, expected]) => {
h.app.actionManager.executeAction(action);
@ -884,9 +887,6 @@ describe("z-index manipulation", () => {
{ id: "A", groupIds: ["g1"], isSelected: true },
{ id: "B", groupIds: ["g1"], isSelected: true },
]);
h.setState({
selectedGroupIds: { g1: true },
});
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements).toMatchObject([
{ id: "A" },
@ -908,9 +908,6 @@ describe("z-index manipulation", () => {
{ id: "B", groupIds: ["g1"], isSelected: true },
{ id: "C" },
]);
h.setState({
selectedGroupIds: { g1: true },
});
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements).toMatchObject([
{ id: "A" },
@ -933,9 +930,6 @@ describe("z-index manipulation", () => {
{ id: "B", groupIds: ["g1"], isSelected: true },
{ id: "C", isSelected: true },
]);
h.setState({
selectedGroupIds: { g1: true },
});
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([
"A",
@ -952,9 +946,6 @@ describe("z-index manipulation", () => {
{ id: "C", groupIds: ["g2"], isSelected: true },
{ id: "D", groupIds: ["g2"], isSelected: true },
]);
h.setState({
selectedGroupIds: { g1: true, g2: true },
});
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([
"A",
@ -967,14 +958,16 @@ describe("z-index manipulation", () => {
"D_copy",
]);
populateElements([
{ id: "A", groupIds: ["g1", "g2"], isSelected: true },
{ id: "B", groupIds: ["g1", "g2"], isSelected: true },
{ id: "C", groupIds: ["g2"], isSelected: true },
]);
h.setState({
selectedGroupIds: { g1: true },
});
populateElements(
[
{ id: "A", groupIds: ["g1", "g2"], isSelected: true },
{ id: "B", groupIds: ["g1", "g2"], isSelected: true },
{ id: "C", groupIds: ["g2"], isSelected: true },
],
{
selectedGroupIds: { g1: true },
},
);
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([
"A",
@ -985,14 +978,16 @@ describe("z-index manipulation", () => {
"C_copy",
]);
populateElements([
{ id: "A", groupIds: ["g1", "g2"], isSelected: true },
{ id: "B", groupIds: ["g1", "g2"], isSelected: true },
{ id: "C", groupIds: ["g2"], isSelected: true },
]);
h.setState({
selectedGroupIds: { g2: true },
});
populateElements(
[
{ id: "A", groupIds: ["g1", "g2"], isSelected: true },
{ id: "B", groupIds: ["g1", "g2"], isSelected: true },
{ id: "C", groupIds: ["g2"], isSelected: true },
],
{
selectedGroupIds: { g2: true },
},
);
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([
"A",
@ -1003,17 +998,19 @@ describe("z-index manipulation", () => {
"C_copy",
]);
populateElements([
{ id: "A", groupIds: ["g1", "g2"], isSelected: true },
{ id: "B", groupIds: ["g1", "g2"], isSelected: true },
{ id: "C", groupIds: ["g2"], isSelected: true },
{ id: "D", groupIds: ["g3", "g4"], isSelected: true },
{ id: "E", groupIds: ["g3", "g4"], isSelected: true },
{ id: "F", groupIds: ["g4"], isSelected: true },
]);
h.setState({
selectedGroupIds: { g2: true, g4: true },
});
populateElements(
[
{ id: "A", groupIds: ["g1", "g2"], isSelected: true },
{ id: "B", groupIds: ["g1", "g2"], isSelected: true },
{ id: "C", groupIds: ["g2"], isSelected: true },
{ id: "D", groupIds: ["g3", "g4"], isSelected: true },
{ id: "E", groupIds: ["g3", "g4"], isSelected: true },
{ id: "F", groupIds: ["g4"], isSelected: true },
],
{
selectedGroupIds: { g2: true, g4: true },
},
);
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([
"A",
@ -1030,11 +1027,14 @@ describe("z-index manipulation", () => {
"F_copy",
]);
populateElements([
{ id: "A", groupIds: ["g1", "g2"], isSelected: true },
{ id: "B", groupIds: ["g1", "g2"] },
{ id: "C", groupIds: ["g2"] },
]);
populateElements(
[
{ id: "A", groupIds: ["g1", "g2"], isSelected: true },
{ id: "B", groupIds: ["g1", "g2"] },
{ id: "C", groupIds: ["g2"] },
],
{ editingGroupId: "g1" },
);
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([
"A",
@ -1043,11 +1043,14 @@ describe("z-index manipulation", () => {
"C",
]);
populateElements([
{ id: "A", groupIds: ["g1", "g2"] },
{ id: "B", groupIds: ["g1", "g2"], isSelected: true },
{ id: "C", groupIds: ["g2"] },
]);
populateElements(
[
{ id: "A", groupIds: ["g1", "g2"] },
{ id: "B", groupIds: ["g1", "g2"], isSelected: true },
{ id: "C", groupIds: ["g2"] },
],
{ editingGroupId: "g1" },
);
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([
"A",
@ -1056,11 +1059,14 @@ describe("z-index manipulation", () => {
"C",
]);
populateElements([
{ id: "A", groupIds: ["g1", "g2"], isSelected: true },
{ id: "B", groupIds: ["g1", "g2"], isSelected: true },
{ id: "C", groupIds: ["g2"], isSelected: true },
]);
populateElements(
[
{ id: "A", groupIds: ["g1", "g2"], isSelected: true },
{ id: "B", groupIds: ["g1", "g2"], isSelected: true },
{ id: "C", groupIds: ["g2"] },
],
{ editingGroupId: "g1" },
);
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([
"A",
@ -1068,7 +1074,42 @@ describe("z-index manipulation", () => {
"B",
"B_copy",
"C",
]);
});
it("duplicating incorrectly interleaved elements (group elements should be together) should still produce reasonable result", () => {
populateElements([
{ id: "A", groupIds: ["g1"], isSelected: true },
{ id: "B" },
{ id: "C", groupIds: ["g1"], isSelected: true },
]);
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([
"A",
"C",
"A_copy",
"C_copy",
"B",
]);
});
it("group-selected duplication should includes deleted elements that weren't selected on account of being deleted", () => {
populateElements([
{ id: "A", groupIds: ["g1"], isDeleted: true },
{ id: "B", groupIds: ["g1"], isSelected: true },
{ id: "C", groupIds: ["g1"], isSelected: true },
{ id: "D" },
]);
expect(h.state.selectedGroupIds).toEqual({ g1: true });
h.app.actionManager.executeAction(actionDuplicateSelection);
expect(h.elements.map((element) => element.id)).toEqual([
"A",
"B",
"C",
"A_copy",
"B_copy",
"C_copy",
"D",
]);
});

View File

@ -607,6 +607,14 @@ export const arrayToMap = <T extends { id: string } | string>(
}, new Map());
};
export const arrayToMapWithIndex = <T extends { id: string }>(
elements: readonly T[],
) =>
elements.reduce((acc, element: T, idx) => {
acc.set(element.id, [element, idx]);
return acc;
}, new Map<string, [element: T, index: number]>());
export const isTestEnv = () =>
typeof process !== "undefined" && process.env?.NODE_ENV === "test";