Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09ae07ed7f | ||
|
|
6d45430344 | ||
|
|
3aa0c5ebc0 | ||
|
|
e940993e0e | ||
|
|
8f90aeb8d5 | ||
|
|
e92d133973 | ||
|
|
b682d88167 | ||
|
|
7daf1a7944 | ||
|
|
5c0eff50a0 | ||
|
|
19056d635b | ||
|
|
4d5f00ff08 | ||
|
|
20de06ef50 | ||
|
|
1849ff6ee2 | ||
|
|
6765fc16be | ||
|
|
5ca4f5bbf4 | ||
|
|
9392ec276d | ||
|
|
b26e4fcf99 | ||
|
|
45f3410da8 | ||
|
|
94b387ef7b | ||
|
|
6d0716eb6b | ||
|
|
8e26d5b500 | ||
|
|
c5a7723185 | ||
|
|
49172ac2d3 | ||
|
|
618a846451 | ||
|
|
d9f49ffd67 | ||
|
|
46e43baad1 | ||
|
|
bd35b682fa | ||
|
|
b6f9a8005e | ||
|
|
1acfaf6b6e | ||
|
|
5cf7087754 | ||
|
|
b2d49155ef | ||
|
|
9745461db7 | ||
|
|
21e9fcb2f5 | ||
|
|
e203203993 | ||
|
|
f224e4d596 | ||
|
|
e0ca689759 | ||
|
|
f792eb5ae7 | ||
|
|
4604c8d823 | ||
|
|
0896892f8a | ||
|
|
7fe225ee99 | ||
|
|
d2fd7be457 | ||
|
|
5c61613a2e | ||
|
|
b2767924de | ||
|
|
59d0a77862 | ||
|
|
987526d1e5 |
+5
-1
@@ -4,5 +4,9 @@ REACT_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
|||||||
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||||
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||||
|
|
||||||
REACT_APP_SOCKET_SERVER_URL=http://localhost:3002
|
REACT_APP_PORTAL_URL=http://localhost:3002
|
||||||
|
# Fill to set socket server URL used for collaboration.
|
||||||
|
# Meant for forks only: excalidraw.com uses custom REACT_APP_PORTAL_URL flow
|
||||||
|
REACT_APP_WS_SERVER_URL=
|
||||||
|
|
||||||
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
|
||||||
|
|||||||
+5
-1
@@ -4,7 +4,11 @@ REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
|||||||
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||||
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||||
|
|
||||||
REACT_APP_SOCKET_SERVER_URL=https://oss-collab-us1.excalidraw.com
|
REACT_APP_PORTAL_URL=https://portal.excalidraw.com
|
||||||
|
# Fill to set socket server URL used for collaboration.
|
||||||
|
# Meant for forks only: excalidraw.com uses custom REACT_APP_PORTAL_URL flow
|
||||||
|
REACT_APP_WS_SERVER_URL=
|
||||||
|
|
||||||
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
||||||
|
|
||||||
# production-only vars
|
# production-only vars
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
name: Build packages
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
packages:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Setup Node.js 14.x
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 14.x
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
yarn --frozen-lockfile
|
|
||||||
yarn --cwd src/packages/excalidraw
|
|
||||||
yarn --cwd src/packages/utils
|
|
||||||
- name: Build @excalidraw/excalidraw
|
|
||||||
run: |
|
|
||||||
yarn --cwd src/packages/excalidraw run pack
|
|
||||||
- name: Build @excalidraw/utils
|
|
||||||
run: |
|
|
||||||
yarn --cwd src/packages/utils run pack
|
|
||||||
@@ -32,6 +32,10 @@ Last but not least, we're thankful to these companies for offering their service
|
|||||||
|
|
||||||
[](https://vercel.com) [](https://sentry.io) [](https://crowdin.com)
|
[](https://vercel.com) [](https://sentry.io) [](https://crowdin.com)
|
||||||
|
|
||||||
|
## Who's integrating Excalidraw
|
||||||
|
|
||||||
|
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) •
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
### Shortcuts
|
### Shortcuts
|
||||||
|
|||||||
+2
-3
@@ -26,10 +26,10 @@
|
|||||||
"@tldraw/vec": "1.4.3",
|
"@tldraw/vec": "1.4.3",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/pica": "5.1.3",
|
"@types/pica": "5.1.3",
|
||||||
"@types/react": "17.0.38",
|
"@types/react": "17.0.39",
|
||||||
"@types/react-dom": "17.0.11",
|
"@types/react-dom": "17.0.11",
|
||||||
"@types/socket.io-client": "1.4.36",
|
"@types/socket.io-client": "1.4.36",
|
||||||
"browser-fs-access": "0.23.0",
|
"browser-fs-access": "0.24.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"fake-indexeddb": "3.1.7",
|
"fake-indexeddb": "3.1.7",
|
||||||
"firebase": "8.3.3",
|
"firebase": "8.3.3",
|
||||||
@@ -65,7 +65,6 @@
|
|||||||
"dotenv": "10.0.0",
|
"dotenv": "10.0.0",
|
||||||
"eslint-config-prettier": "8.3.0",
|
"eslint-config-prettier": "8.3.0",
|
||||||
"eslint-plugin-prettier": "3.3.1",
|
"eslint-plugin-prettier": "3.3.1",
|
||||||
"firebase-tools": "9.23.0",
|
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
"jest-canvas-mock": "2.3.1",
|
"jest-canvas-mock": "2.3.1",
|
||||||
"lint-staged": "12.3.3",
|
"lint-staged": "12.3.3",
|
||||||
|
|||||||
@@ -72,12 +72,6 @@
|
|||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link
|
|
||||||
href="%REACT_APP_SOCKET_SERVER_URL%/socket.io"
|
|
||||||
rel="preconnect"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<link
|
<link
|
||||||
rel="manifest"
|
rel="manifest"
|
||||||
href="manifest.json"
|
href="manifest.json"
|
||||||
|
|||||||
+18
-11
@@ -20,7 +20,7 @@ const headerForType = {
|
|||||||
perf: "Performance",
|
perf: "Performance",
|
||||||
build: "Build",
|
build: "Build",
|
||||||
};
|
};
|
||||||
|
const badCommits = [];
|
||||||
const getCommitHashForLastVersion = async () => {
|
const getCommitHashForLastVersion = async () => {
|
||||||
try {
|
try {
|
||||||
const commitMessage = `"release @excalidraw/excalidraw@${lastVersion}"`;
|
const commitMessage = `"release @excalidraw/excalidraw@${lastVersion}"`;
|
||||||
@@ -53,19 +53,26 @@ const getLibraryCommitsSinceLastRelease = async () => {
|
|||||||
const messageWithoutType = commit.slice(indexOfColon + 1).trim();
|
const messageWithoutType = commit.slice(indexOfColon + 1).trim();
|
||||||
const messageWithCapitalizeFirst =
|
const messageWithCapitalizeFirst =
|
||||||
messageWithoutType.charAt(0).toUpperCase() + messageWithoutType.slice(1);
|
messageWithoutType.charAt(0).toUpperCase() + messageWithoutType.slice(1);
|
||||||
const prNumber = commit.match(/\(#([0-9]*)\)/)[1];
|
const prMatch = commit.match(/\(#([0-9]*)\)/);
|
||||||
|
if (prMatch) {
|
||||||
|
const prNumber = prMatch[1];
|
||||||
|
|
||||||
// return if the changelog already contains the pr number which would happen for package updates
|
// return if the changelog already contains the pr number which would happen for package updates
|
||||||
if (existingChangeLog.includes(prNumber)) {
|
if (existingChangeLog.includes(prNumber)) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
const prMarkdown = `[#${prNumber}](https://github.com/excalidraw/excalidraw/pull/${prNumber})`;
|
||||||
|
const messageWithPRLink = messageWithCapitalizeFirst.replace(
|
||||||
|
/\(#[0-9]*\)/,
|
||||||
|
prMarkdown,
|
||||||
|
);
|
||||||
|
commitList[type].push(messageWithPRLink);
|
||||||
|
} else {
|
||||||
|
badCommits.push(commit);
|
||||||
|
commitList[type].push(messageWithCapitalizeFirst);
|
||||||
}
|
}
|
||||||
const prMarkdown = `[#${prNumber}](https://github.com/excalidraw/excalidraw/pull/${prNumber})`;
|
|
||||||
const messageWithPRLink = messageWithCapitalizeFirst.replace(
|
|
||||||
/\(#[0-9]*\)/,
|
|
||||||
prMarkdown,
|
|
||||||
);
|
|
||||||
commitList[type].push(messageWithPRLink);
|
|
||||||
});
|
});
|
||||||
|
console.info("Bad commits:", badCommits);
|
||||||
return commitList;
|
return commitList;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ColorPicker } from "../components/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker";
|
||||||
import { zoomIn, zoomOut } from "../components/icons";
|
import { eraser, zoomIn, zoomOut } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { DarkModeToggle } from "../components/DarkModeToggle";
|
import { DarkModeToggle } from "../components/DarkModeToggle";
|
||||||
import { THEME, ZOOM_STEP } from "../constants";
|
import { THEME, ZOOM_STEP } from "../constants";
|
||||||
@@ -15,8 +15,9 @@ import { getShortcutKey } from "../utils";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState, isEraserActive } from "../appState";
|
||||||
import ClearCanvas from "../components/ClearCanvas";
|
import ClearCanvas from "../components/ClearCanvas";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
@@ -26,7 +27,7 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
commitToHistory: !!value.viewBackgroundColor,
|
commitToHistory: !!value.viewBackgroundColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
@@ -39,6 +40,8 @@ export const actionChangeViewBackgroundColor = register({
|
|||||||
updateData({ openPopup: active ? "canvasColorPicker" : null })
|
updateData({ openPopup: active ? "canvasColorPicker" : null })
|
||||||
}
|
}
|
||||||
data-testid="canvas-background-picker"
|
data-testid="canvas-background-picker"
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -287,3 +290,31 @@ export const actionToggleTheme = register({
|
|||||||
),
|
),
|
||||||
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
|
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionErase = register({
|
||||||
|
name: "eraser",
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
return {
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
elementType: isEraserActive(appState) ? "selection" : "eraser",
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData, data }) => (
|
||||||
|
<ToolButton
|
||||||
|
type="button"
|
||||||
|
icon={eraser}
|
||||||
|
className={clsx("eraser", { active: isEraserActive(appState) })}
|
||||||
|
title={t("toolBar.eraser")}
|
||||||
|
aria-label={t("toolBar.eraser")}
|
||||||
|
onClick={() => {
|
||||||
|
updateData(null);
|
||||||
|
}}
|
||||||
|
size={data?.size || "medium"}
|
||||||
|
></ToolButton>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { distributeElements, Distribution } from "../disitrubte";
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CODES } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
@@ -49,7 +49,8 @@ export const distributeHorizontally = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) => event.altKey && event.code === CODES.H,
|
keyTest: (event) =>
|
||||||
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
@@ -77,7 +78,8 @@ export const distributeVertically = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) => event.altKey && event.code === CODES.V,
|
keyTest: (event) =>
|
||||||
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!enableActionGroup(elements, appState)}
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ const flipElement = (
|
|||||||
// calculate new x-coord for transformation
|
// calculate new x-coord for transformation
|
||||||
newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
|
newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
|
||||||
resizeSingleElement(
|
resizeSingleElement(
|
||||||
element,
|
new Map().set(element.id, element),
|
||||||
true,
|
true,
|
||||||
element,
|
element,
|
||||||
usingNWHandle ? "nw" : "ne",
|
usingNWHandle ? "nw" : "ne",
|
||||||
|
|||||||
@@ -30,11 +30,15 @@ import {
|
|||||||
TextAlignCenterIcon,
|
TextAlignCenterIcon,
|
||||||
TextAlignLeftIcon,
|
TextAlignLeftIcon,
|
||||||
TextAlignRightIcon,
|
TextAlignRightIcon,
|
||||||
|
TextAlignTopIcon,
|
||||||
|
TextAlignBottomIcon,
|
||||||
|
TextAlignMiddleIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import {
|
import {
|
||||||
getNonDeletedElements,
|
getNonDeletedElements,
|
||||||
@@ -47,6 +51,7 @@ import {
|
|||||||
getContainerElement,
|
getContainerElement,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
|
hasBoundTextElement,
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isLinearElementType,
|
isLinearElementType,
|
||||||
@@ -58,6 +63,7 @@ import {
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
TextAlign,
|
TextAlign,
|
||||||
|
VerticalAlign,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getLanguage, t } from "../i18n";
|
import { getLanguage, t } from "../i18n";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
@@ -101,6 +107,7 @@ const getFormValue = function <T>(
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
getAttribute: (element: ExcalidrawElement) => T,
|
getAttribute: (element: ExcalidrawElement) => T,
|
||||||
defaultValue?: T,
|
defaultValue?: T,
|
||||||
|
onlyBoundTextElements: boolean = false,
|
||||||
): T | null {
|
): T | null {
|
||||||
const editingElement = appState.editingElement;
|
const editingElement = appState.editingElement;
|
||||||
const nonDeletedElements = getNonDeletedElements(elements);
|
const nonDeletedElements = getNonDeletedElements(elements);
|
||||||
@@ -111,6 +118,7 @@ const getFormValue = function <T>(
|
|||||||
nonDeletedElements,
|
nonDeletedElements,
|
||||||
appState,
|
appState,
|
||||||
getAttribute,
|
getAttribute,
|
||||||
|
onlyBoundTextElements,
|
||||||
)
|
)
|
||||||
: defaultValue) ??
|
: defaultValue) ??
|
||||||
null
|
null
|
||||||
@@ -191,8 +199,8 @@ const changeFontSize = (
|
|||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
export const actionChangeStrokeColor = register({
|
export const actionChangeFontColor = register({
|
||||||
name: "changeStrokeColor",
|
name: "changeFontColor",
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return {
|
return {
|
||||||
...(value.currentItemStrokeColor && {
|
...(value.currentItemStrokeColor && {
|
||||||
@@ -200,7 +208,7 @@ export const actionChangeStrokeColor = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(el) => {
|
(el) => {
|
||||||
return hasStrokeColor(el.type)
|
return isTextElement(el)
|
||||||
? newElementWith(el, {
|
? newElementWith(el, {
|
||||||
strokeColor: value.currentItemStrokeColor,
|
strokeColor: value.currentItemStrokeColor,
|
||||||
})
|
})
|
||||||
@@ -216,26 +224,107 @@ export const actionChangeStrokeColor = register({
|
|||||||
commitToHistory: !!value.currentItemStrokeColor,
|
commitToHistory: !!value.currentItemStrokeColor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
<>
|
return (
|
||||||
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
|
<>
|
||||||
<ColorPicker
|
<h3 aria-hidden="true">{t("labels.fontColor")}</h3>
|
||||||
type="elementStroke"
|
<ColorPicker
|
||||||
label={t("labels.stroke")}
|
type="elementFontColor"
|
||||||
color={getFormValue(
|
label={t("labels.fontColor")}
|
||||||
|
color={getFormValue(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
(element) => element.strokeColor,
|
||||||
|
appState.currentItemStrokeColor,
|
||||||
|
true,
|
||||||
|
)}
|
||||||
|
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||||
|
isActive={appState.openPopup === "fontColorPicker"}
|
||||||
|
setActive={(active) =>
|
||||||
|
updateData({ openPopup: active ? "fontColorPicker" : null })
|
||||||
|
}
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionChangeStrokeColor = register({
|
||||||
|
name: "changeStrokeColor",
|
||||||
|
perform: (elements, appState, value) => {
|
||||||
|
const targetElements = getTargetElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasOnlyContainersWithBoundText =
|
||||||
|
targetElements.length > 1 &&
|
||||||
|
targetElements.every(
|
||||||
|
(element) =>
|
||||||
|
hasBoundTextElement(element) || isBoundToContainer(element),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(value.currentItemStrokeColor && {
|
||||||
|
elements: changeProperty(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeColor,
|
(el) => {
|
||||||
appState.currentItemStrokeColor,
|
return (hasStrokeColor(el.type) &&
|
||||||
)}
|
!hasOnlyContainersWithBoundText) ||
|
||||||
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
!isBoundToContainer(el)
|
||||||
isActive={appState.openPopup === "strokeColorPicker"}
|
? newElementWith(el, {
|
||||||
setActive={(active) =>
|
strokeColor: value.currentItemStrokeColor,
|
||||||
updateData({ openPopup: active ? "strokeColorPicker" : null })
|
})
|
||||||
}
|
: el;
|
||||||
/>
|
},
|
||||||
</>
|
true,
|
||||||
),
|
),
|
||||||
|
}),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
...value,
|
||||||
|
},
|
||||||
|
commitToHistory: !!value.currentItemStrokeColor,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
|
const targetElements = getTargetElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasOnlyContainersWithBoundText = targetElements.every(
|
||||||
|
(element) => hasBoundTextElement(element) || isBoundToContainer(element),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
|
||||||
|
<ColorPicker
|
||||||
|
type="elementStroke"
|
||||||
|
label={t("labels.stroke")}
|
||||||
|
color={getFormValue(
|
||||||
|
hasOnlyContainersWithBoundText
|
||||||
|
? elements.filter((element) => !isTextElement(element))
|
||||||
|
: elements,
|
||||||
|
appState,
|
||||||
|
(element) => element.strokeColor,
|
||||||
|
appState.currentItemStrokeColor,
|
||||||
|
)}
|
||||||
|
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||||
|
isActive={appState.openPopup === "strokeColorPicker"}
|
||||||
|
setActive={(active) =>
|
||||||
|
updateData({ openPopup: active ? "strokeColorPicker" : null })
|
||||||
|
}
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeBackgroundColor = register({
|
export const actionChangeBackgroundColor = register({
|
||||||
@@ -273,6 +362,8 @@ export const actionChangeBackgroundColor = register({
|
|||||||
setActive={(active) =>
|
setActive={(active) =>
|
||||||
updateData({ openPopup: active ? "backgroundColorPicker" : null })
|
updateData({ openPopup: active ? "backgroundColorPicker" : null })
|
||||||
}
|
}
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
@@ -709,9 +800,7 @@ export const actionChangeTextAlign = register({
|
|||||||
if (isTextElement(oldElement)) {
|
if (isTextElement(oldElement)) {
|
||||||
const newElement: ExcalidrawTextElement = newElementWith(
|
const newElement: ExcalidrawTextElement = newElementWith(
|
||||||
oldElement,
|
oldElement,
|
||||||
{
|
{ textAlign: value },
|
||||||
textAlign: value,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
newElement,
|
newElement,
|
||||||
@@ -732,47 +821,119 @@ export const actionChangeTextAlign = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
<fieldset>
|
return (
|
||||||
<legend>{t("labels.textAlign")}</legend>
|
<fieldset>
|
||||||
<ButtonIconSelect<TextAlign | false>
|
<legend>{t("labels.textAlign")}</legend>
|
||||||
group="text-align"
|
<ButtonIconSelect<TextAlign | false>
|
||||||
options={[
|
group="text-align"
|
||||||
{
|
options={[
|
||||||
value: "left",
|
{
|
||||||
text: t("labels.left"),
|
value: "left",
|
||||||
icon: <TextAlignLeftIcon theme={appState.theme} />,
|
text: t("labels.left"),
|
||||||
},
|
icon: <TextAlignLeftIcon theme={appState.theme} />,
|
||||||
{
|
},
|
||||||
value: "center",
|
{
|
||||||
text: t("labels.center"),
|
value: "center",
|
||||||
icon: <TextAlignCenterIcon theme={appState.theme} />,
|
text: t("labels.center"),
|
||||||
},
|
icon: <TextAlignCenterIcon theme={appState.theme} />,
|
||||||
{
|
},
|
||||||
value: "right",
|
{
|
||||||
text: t("labels.right"),
|
value: "right",
|
||||||
icon: <TextAlignRightIcon theme={appState.theme} />,
|
text: t("labels.right"),
|
||||||
},
|
icon: <TextAlignRightIcon theme={appState.theme} />,
|
||||||
]}
|
},
|
||||||
value={getFormValue(
|
]}
|
||||||
elements,
|
value={getFormValue(
|
||||||
appState,
|
elements,
|
||||||
(element) => {
|
appState,
|
||||||
if (isTextElement(element)) {
|
(element) => {
|
||||||
return element.textAlign;
|
if (isTextElement(element)) {
|
||||||
|
return element.textAlign;
|
||||||
|
}
|
||||||
|
const boundTextElement = getBoundTextElement(element);
|
||||||
|
if (boundTextElement) {
|
||||||
|
return boundTextElement.textAlign;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
appState.currentItemTextAlign,
|
||||||
|
)}
|
||||||
|
onChange={(value) => updateData(value)}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const actionChangeVerticalAlign = register({
|
||||||
|
name: "changeVerticalAlign",
|
||||||
|
perform: (elements, appState, value) => {
|
||||||
|
return {
|
||||||
|
elements: changeProperty(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
(oldElement) => {
|
||||||
|
if (isTextElement(oldElement)) {
|
||||||
|
const newElement: ExcalidrawTextElement = newElementWith(
|
||||||
|
oldElement,
|
||||||
|
{ verticalAlign: value },
|
||||||
|
);
|
||||||
|
|
||||||
|
redrawTextBoundingBox(
|
||||||
|
newElement,
|
||||||
|
getContainerElement(oldElement),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
return newElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldElement;
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
|
return (
|
||||||
|
<fieldset>
|
||||||
|
<ButtonIconSelect<VerticalAlign | false>
|
||||||
|
group="text-align"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: VERTICAL_ALIGN.TOP,
|
||||||
|
text: t("labels.alignTop"),
|
||||||
|
icon: <TextAlignTopIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: VERTICAL_ALIGN.MIDDLE,
|
||||||
|
text: t("labels.centerVertically"),
|
||||||
|
icon: <TextAlignMiddleIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: VERTICAL_ALIGN.BOTTOM,
|
||||||
|
text: t("labels.alignBottom"),
|
||||||
|
icon: <TextAlignBottomIcon theme={appState.theme} />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={getFormValue(elements, appState, (element) => {
|
||||||
|
if (isTextElement(element) && element.containerId) {
|
||||||
|
return element.verticalAlign;
|
||||||
}
|
}
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
return boundTextElement.textAlign;
|
return boundTextElement.verticalAlign;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
})}
|
||||||
appState.currentItemTextAlign,
|
onChange={(value) => updateData(value)}
|
||||||
)}
|
/>
|
||||||
onChange={(value) => updateData(value)}
|
</fieldset>
|
||||||
/>
|
);
|
||||||
</fieldset>
|
},
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeSharpness = register({
|
export const actionChangeSharpness = register({
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { CODES } from "../keys";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
||||||
|
import { fireEvent, render, screen } from "../tests/test-utils";
|
||||||
|
import { copiedStyles } from "./actionStyles";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
|
describe("actionStyles", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
});
|
||||||
|
it("should copy & paste styles via keyboard", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
mouse.down(10, 10);
|
||||||
|
mouse.up(20, 20);
|
||||||
|
|
||||||
|
// Change some styles of second rectangle
|
||||||
|
UI.clickLabeledElement("Stroke");
|
||||||
|
UI.clickLabeledElement(t("colors.c92a2a"));
|
||||||
|
UI.clickLabeledElement("Background");
|
||||||
|
UI.clickLabeledElement(t("colors.e64980"));
|
||||||
|
// Fill style
|
||||||
|
fireEvent.click(screen.getByTitle("Cross-hatch"));
|
||||||
|
// Stroke width
|
||||||
|
fireEvent.click(screen.getByTitle("Bold"));
|
||||||
|
// Stroke style
|
||||||
|
fireEvent.click(screen.getByTitle("Dotted"));
|
||||||
|
// Roughness
|
||||||
|
fireEvent.click(screen.getByTitle("Cartoonist"));
|
||||||
|
// Opacity
|
||||||
|
fireEvent.change(screen.getByLabelText("Opacity"), {
|
||||||
|
target: { value: "60" },
|
||||||
|
});
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
|
||||||
|
API.setSelectedElements([h.elements[1]]);
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true, alt: true }, () => {
|
||||||
|
Keyboard.codeDown(CODES.C);
|
||||||
|
});
|
||||||
|
const secondRect = JSON.parse(copiedStyles);
|
||||||
|
expect(secondRect.id).toBe(h.elements[1].id);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
// Paste styles to first rectangle
|
||||||
|
API.setSelectedElements([h.elements[0]]);
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true, alt: true }, () => {
|
||||||
|
Keyboard.codeDown(CODES.V);
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstRect = API.getSelectedElement();
|
||||||
|
expect(firstRect.id).toBe(h.elements[0].id);
|
||||||
|
expect(firstRect.strokeColor).toBe("#c92a2a");
|
||||||
|
expect(firstRect.backgroundColor).toBe("#e64980");
|
||||||
|
expect(firstRect.fillStyle).toBe("cross-hatch");
|
||||||
|
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
|
||||||
|
expect(firstRect.strokeStyle).toBe("dotted");
|
||||||
|
expect(firstRect.roughness).toBe(2); // Cartoonist: 2
|
||||||
|
expect(firstRect.opacity).toBe(60);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -64,8 +64,8 @@ export const actionPasteStyles = register({
|
|||||||
});
|
});
|
||||||
|
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
element,
|
newElement,
|
||||||
getContainerElement(element),
|
getContainerElement(newElement),
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export {
|
|||||||
export { actionSelectAll } from "./actionSelectAll";
|
export { actionSelectAll } from "./actionSelectAll";
|
||||||
export { actionDuplicateSelection } from "./actionDuplicateSelection";
|
export { actionDuplicateSelection } from "./actionDuplicateSelection";
|
||||||
export {
|
export {
|
||||||
|
actionChangeFontColor,
|
||||||
actionChangeStrokeColor,
|
actionChangeStrokeColor,
|
||||||
actionChangeBackgroundColor,
|
actionChangeBackgroundColor,
|
||||||
actionChangeStrokeWidth,
|
actionChangeStrokeWidth,
|
||||||
@@ -17,6 +18,7 @@ export {
|
|||||||
actionChangeFontSize,
|
actionChangeFontSize,
|
||||||
actionChangeFontFamily,
|
actionChangeFontFamily,
|
||||||
actionChangeTextAlign,
|
actionChangeTextAlign,
|
||||||
|
actionChangeVerticalAlign,
|
||||||
} from "./actionProperties";
|
} from "./actionProperties";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
+37
-1
@@ -10,6 +10,31 @@ import {
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { MODES } from "../constants";
|
import { MODES } from "../constants";
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
|
||||||
|
const trackAction = (
|
||||||
|
action: Action,
|
||||||
|
source: "ui" | "keyboard" | "api",
|
||||||
|
value: any,
|
||||||
|
) => {
|
||||||
|
if (action.trackEvent !== false) {
|
||||||
|
try {
|
||||||
|
if (action.trackEvent === true) {
|
||||||
|
trackEvent(
|
||||||
|
action.name,
|
||||||
|
source,
|
||||||
|
typeof value === "number" || typeof value === "string"
|
||||||
|
? String(value)
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
action.trackEvent?.(action, source, value);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("error while logging action:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export class ActionManager implements ActionsManagerInterface {
|
export class ActionManager implements ActionsManagerInterface {
|
||||||
actions = {} as ActionsManagerInterface["actions"];
|
actions = {} as ActionsManagerInterface["actions"];
|
||||||
@@ -65,9 +90,15 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length !== 1) {
|
||||||
|
if (data.length > 1) {
|
||||||
|
console.warn("Canceling as multiple actions match this shortcut", data);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const action = data[0];
|
||||||
|
|
||||||
const { viewModeEnabled } = this.getAppState();
|
const { viewModeEnabled } = this.getAppState();
|
||||||
if (viewModeEnabled) {
|
if (viewModeEnabled) {
|
||||||
if (!Object.values(MODES).includes(data[0].name)) {
|
if (!Object.values(MODES).includes(data[0].name)) {
|
||||||
@@ -75,6 +106,8 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackAction(action, "keyboard", null);
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.updater(
|
this.updater(
|
||||||
data[0].perform(
|
data[0].perform(
|
||||||
@@ -96,6 +129,7 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
this.app,
|
this.app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
trackAction(action, "api", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,6 +156,8 @@ export class ActionManager implements ActionsManagerInterface {
|
|||||||
this.app,
|
this.app,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
trackAction(action, "ui", formState);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isDarwin } from "../keys";
|
import { isDarwin } from "../keys";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
|
import { ActionName } from "./types";
|
||||||
|
|
||||||
export type ShortcutName =
|
export type ShortcutName = SubtypeOf<
|
||||||
|
ActionName,
|
||||||
| "cut"
|
| "cut"
|
||||||
| "copy"
|
| "copy"
|
||||||
| "paste"
|
| "paste"
|
||||||
@@ -26,7 +28,8 @@ export type ShortcutName =
|
|||||||
| "viewMode"
|
| "viewMode"
|
||||||
| "flipHorizontal"
|
| "flipHorizontal"
|
||||||
| "flipVertical"
|
| "flipVertical"
|
||||||
| "link";
|
| "hyperlink"
|
||||||
|
>;
|
||||||
|
|
||||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||||
@@ -63,11 +66,11 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
flipHorizontal: [getShortcutKey("Shift+H")],
|
flipHorizontal: [getShortcutKey("Shift+H")],
|
||||||
flipVertical: [getShortcutKey("Shift+V")],
|
flipVertical: [getShortcutKey("Shift+V")],
|
||||||
viewMode: [getShortcutKey("Alt+R")],
|
viewMode: [getShortcutKey("Alt+R")],
|
||||||
link: [getShortcutKey("CtrlOrCmd+K")],
|
hyperlink: [getShortcutKey("CtrlOrCmd+K")],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||||
const shortcuts = shortcutMap[name];
|
const shortcuts = shortcutMap[name];
|
||||||
// if multiple shortcuts availiable, take the first one
|
// if multiple shortcuts available, take the first one
|
||||||
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
|
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type ActionName =
|
|||||||
| "gridMode"
|
| "gridMode"
|
||||||
| "zenMode"
|
| "zenMode"
|
||||||
| "stats"
|
| "stats"
|
||||||
|
| "changeFontColor"
|
||||||
| "changeStrokeColor"
|
| "changeStrokeColor"
|
||||||
| "changeBackgroundColor"
|
| "changeBackgroundColor"
|
||||||
| "changeFillStyle"
|
| "changeFillStyle"
|
||||||
@@ -82,6 +83,7 @@ export type ActionName =
|
|||||||
| "zoomToSelection"
|
| "zoomToSelection"
|
||||||
| "changeFontFamily"
|
| "changeFontFamily"
|
||||||
| "changeTextAlign"
|
| "changeTextAlign"
|
||||||
|
| "changeVerticalAlign"
|
||||||
| "toggleFullScreen"
|
| "toggleFullScreen"
|
||||||
| "toggleShortcuts"
|
| "toggleShortcuts"
|
||||||
| "group"
|
| "group"
|
||||||
@@ -105,7 +107,8 @@ export type ActionName =
|
|||||||
| "increaseFontSize"
|
| "increaseFontSize"
|
||||||
| "decreaseFontSize"
|
| "decreaseFontSize"
|
||||||
| "unbindText"
|
| "unbindText"
|
||||||
| "link";
|
| "hyperlink"
|
||||||
|
| "eraser";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
@@ -136,6 +139,9 @@ export interface Action {
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
checked?: (appState: Readonly<AppState>) => boolean;
|
checked?: (appState: Readonly<AppState>) => boolean;
|
||||||
|
trackEvent?:
|
||||||
|
| boolean
|
||||||
|
| ((action: Action, type: "ui" | "keyboard" | "api", value: any) => void);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionsManagerInterface {
|
export interface ActionsManagerInterface {
|
||||||
|
|||||||
+5
-5
@@ -3,16 +3,16 @@ export const trackEvent =
|
|||||||
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
|
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
|
||||||
typeof window !== "undefined" &&
|
typeof window !== "undefined" &&
|
||||||
window.gtag
|
window.gtag
|
||||||
? (category: string, name: string, label?: string, value?: number) => {
|
? (category: string, action: string, label?: string, value?: number) => {
|
||||||
window.gtag("event", name, {
|
window.gtag("event", action, {
|
||||||
event_category: category,
|
event_category: category,
|
||||||
event_label: label,
|
event_label: label,
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
|
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
|
||||||
? (category: string, name: string, label?: string, value?: number) => {}
|
? (category: string, action: string, label?: string, value?: number) => {}
|
||||||
: (category: string, name: string, label?: string, value?: number) => {
|
: (category: string, action: string, label?: string, value?: number) => {
|
||||||
// Uncomment the next line to track locally
|
// Uncomment the next line to track locally
|
||||||
// console.info("Track Event", category, name, label, value);
|
// console.info("Track Event", category, action, label, value);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -213,3 +213,9 @@ export const cleanAppStateForExport = (appState: Partial<AppState>) => {
|
|||||||
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {
|
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {
|
||||||
return _clearAppStateForStorage(appState, "server");
|
return _clearAppStateForStorage(appState, "server");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isEraserActive = ({
|
||||||
|
elementType,
|
||||||
|
}: {
|
||||||
|
elementType: AppState["elementType"];
|
||||||
|
}) => elementType === "eraser";
|
||||||
|
|||||||
+8
-3
@@ -1,5 +1,10 @@
|
|||||||
import colors from "./colors";
|
import colors from "./colors";
|
||||||
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, ENV } from "./constants";
|
import {
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
ENV,
|
||||||
|
VERTICAL_ALIGN,
|
||||||
|
} from "./constants";
|
||||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||||
import { NonDeletedExcalidrawElement } from "./element/types";
|
import { NonDeletedExcalidrawElement } from "./element/types";
|
||||||
import { randomId } from "./random";
|
import { randomId } from "./random";
|
||||||
@@ -103,7 +108,7 @@ const transposeCells = (cells: string[][]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
|
export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
|
||||||
// Copy/paste from excel, spreadhseets, tsv, csv.
|
// Copy/paste from excel, spreadsheets, tsv, csv.
|
||||||
// For now we only accept 2 columns with an optional header
|
// For now we only accept 2 columns with an optional header
|
||||||
|
|
||||||
// Check for tab separated values
|
// Check for tab separated values
|
||||||
@@ -161,7 +166,7 @@ const commonProps = {
|
|||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
verticalAlign: "middle",
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const getChartDimentions = (spreadsheet: Spreadsheet) => {
|
const getChartDimentions = (spreadsheet: Spreadsheet) => {
|
||||||
|
|||||||
+1
-1
@@ -124,7 +124,7 @@ const getSystemClipboard = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attemps to parse clipboard. Prefers system clipboard.
|
* Attempts to parse clipboard. Prefers system clipboard.
|
||||||
*/
|
*/
|
||||||
export const parseClipboard = async (
|
export const parseClipboard = async (
|
||||||
event: ClipboardEvent | null,
|
event: ClipboardEvent | null,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
|
|||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
|
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
@@ -29,12 +30,21 @@ export const SelectedShapeActions = ({
|
|||||||
appState: AppState;
|
appState: AppState;
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
elementType: ExcalidrawElement["type"];
|
elementType: AppState["elementType"];
|
||||||
}) => {
|
}) => {
|
||||||
const targetElements = getTargetElements(
|
const targetElements = getTargetElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let isSingleElementBoundContainer = false;
|
||||||
|
if (
|
||||||
|
targetElements.length === 2 &&
|
||||||
|
(hasBoundTextElement(targetElements[0]) ||
|
||||||
|
hasBoundTextElement(targetElements[1]))
|
||||||
|
) {
|
||||||
|
isSingleElementBoundContainer = true;
|
||||||
|
}
|
||||||
const isEditing = Boolean(appState.editingElement);
|
const isEditing = Boolean(appState.editingElement);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
@@ -58,8 +68,15 @@ export const SelectedShapeActions = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasOnlyContainersWithBoundText =
|
||||||
|
targetElements.length > 1 &&
|
||||||
|
targetElements.every(
|
||||||
|
(element) => hasBoundTextElement(element) || isBoundToContainer(element),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
|
{hasOnlyContainersWithBoundText && renderAction("changeFontColor")}
|
||||||
{((hasStrokeColor(elementType) &&
|
{((hasStrokeColor(elementType) &&
|
||||||
elementType !== "image" &&
|
elementType !== "image" &&
|
||||||
commonSelectedType !== "image") ||
|
commonSelectedType !== "image") ||
|
||||||
@@ -100,6 +117,10 @@ export const SelectedShapeActions = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{targetElements.some(
|
||||||
|
(element) =>
|
||||||
|
hasBoundTextElement(element) || isBoundToContainer(element),
|
||||||
|
) && renderAction("changeVerticalAlign")}
|
||||||
{(canHaveArrowheads(elementType) ||
|
{(canHaveArrowheads(elementType) ||
|
||||||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
||||||
<>{renderAction("changeArrowhead")}</>
|
<>{renderAction("changeArrowhead")}</>
|
||||||
@@ -117,7 +138,7 @@ export const SelectedShapeActions = ({
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{targetElements.length > 1 && (
|
{targetElements.length > 1 && !isSingleElementBoundContainer && (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.align")}</legend>
|
<legend>{t("labels.align")}</legend>
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
@@ -150,15 +171,15 @@ export const SelectedShapeActions = ({
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
)}
|
)}
|
||||||
{!isMobile && !isEditing && targetElements.length > 0 && (
|
{!isEditing && targetElements.length > 0 && (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.actions")}</legend>
|
<legend>{t("labels.actions")}</legend>
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
{renderAction("duplicateSelection")}
|
{!isMobile && renderAction("duplicateSelection")}
|
||||||
{renderAction("deleteSelectedElements")}
|
{!isMobile && renderAction("deleteSelectedElements")}
|
||||||
{renderAction("group")}
|
{renderAction("group")}
|
||||||
{renderAction("ungroup")}
|
{renderAction("ungroup")}
|
||||||
{targetElements.length === 1 && renderAction("link")}
|
{targetElements.length === 1 && renderAction("hyperlink")}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
)}
|
)}
|
||||||
@@ -173,7 +194,7 @@ export const ShapesSwitcher = ({
|
|||||||
onImageAction,
|
onImageAction,
|
||||||
}: {
|
}: {
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
elementType: ExcalidrawElement["type"];
|
elementType: AppState["elementType"];
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
||||||
}) => (
|
}) => (
|
||||||
|
|||||||
+200
-53
@@ -35,7 +35,7 @@ import { ActionManager } from "../actions/manager";
|
|||||||
import { actions } from "../actions/register";
|
import { actions } from "../actions/register";
|
||||||
import { ActionResult } from "../actions/types";
|
import { ActionResult } from "../actions/types";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState, isEraserActive } from "../appState";
|
||||||
import {
|
import {
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
parseClipboard,
|
parseClipboard,
|
||||||
@@ -69,6 +69,7 @@ import {
|
|||||||
TOUCH_CTX_MENU_TIMEOUT,
|
TOUCH_CTX_MENU_TIMEOUT,
|
||||||
URL_HASH_KEYS,
|
URL_HASH_KEYS,
|
||||||
URL_QUERY_KEYS,
|
URL_QUERY_KEYS,
|
||||||
|
VERTICAL_ALIGN,
|
||||||
ZOOM_STEP,
|
ZOOM_STEP,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { loadFromBlob } from "../data";
|
import { loadFromBlob } from "../data";
|
||||||
@@ -115,11 +116,7 @@ import {
|
|||||||
updateBoundElements,
|
updateBoundElements,
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import {
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
bumpVersion,
|
|
||||||
mutateElement,
|
|
||||||
newElementWith,
|
|
||||||
} from "../element/mutateElement";
|
|
||||||
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
|
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
@@ -130,6 +127,7 @@ import {
|
|||||||
isInitializedImageElement,
|
isInitializedImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isLinearElementType,
|
isLinearElementType,
|
||||||
|
isTextBindableContainer,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
@@ -143,6 +141,7 @@ import {
|
|||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
FileId,
|
FileId,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
ExcalidrawTextContainer,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getCenter, getDistance } from "../gesture";
|
import { getCenter, getDistance } from "../gesture";
|
||||||
import {
|
import {
|
||||||
@@ -170,7 +169,7 @@ import { renderScene } from "../renderer";
|
|||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
import {
|
import {
|
||||||
calculateScrollCenter,
|
calculateScrollCenter,
|
||||||
getElementContainingPosition,
|
getTextBindableContainerAtPosition,
|
||||||
getElementsAtPosition,
|
getElementsAtPosition,
|
||||||
getElementsWithinSelection,
|
getElementsWithinSelection,
|
||||||
getNormalizedZoom,
|
getNormalizedZoom,
|
||||||
@@ -241,7 +240,7 @@ import {
|
|||||||
bindTextToShapeAfterDuplication,
|
bindTextToShapeAfterDuplication,
|
||||||
getApproxMinLineHeight,
|
getApproxMinLineHeight,
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElementId,
|
getBoundTextElement,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
||||||
import {
|
import {
|
||||||
@@ -315,6 +314,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
|
lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
|
||||||
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
||||||
contextMenuOpen: boolean = false;
|
contextMenuOpen: boolean = false;
|
||||||
|
lastScenePointer: { x: number; y: number } | null = null;
|
||||||
|
|
||||||
constructor(props: AppProps) {
|
constructor(props: AppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -1045,6 +1045,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
||||||
|
if (
|
||||||
|
Object.keys(this.state.selectedElementIds).length &&
|
||||||
|
isEraserActive(this.state)
|
||||||
|
) {
|
||||||
|
this.setState({ elementType: "selection" });
|
||||||
|
}
|
||||||
// Hide hyperlink popup if shown when element type is not selection
|
// Hide hyperlink popup if shown when element type is not selection
|
||||||
if (
|
if (
|
||||||
prevState.elementType === "selection" &&
|
prevState.elementType === "selection" &&
|
||||||
@@ -1620,9 +1626,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
this.files = { ...this.files, ...Object.fromEntries(filesMap) };
|
this.files = { ...this.files, ...Object.fromEntries(filesMap) };
|
||||||
|
|
||||||
// bump versions for elements that reference added files so that
|
|
||||||
// we/host apps can detect the change, and invalidate the image & shape
|
|
||||||
// cache
|
|
||||||
this.scene.getElements().forEach((element) => {
|
this.scene.getElements().forEach((element) => {
|
||||||
if (
|
if (
|
||||||
isInitializedImageElement(element) &&
|
isInitializedImageElement(element) &&
|
||||||
@@ -1630,7 +1633,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
) {
|
) {
|
||||||
this.imageCache.delete(element.fileId);
|
this.imageCache.delete(element.fileId);
|
||||||
invalidateShapeForElement(element);
|
invalidateShapeForElement(element);
|
||||||
bumpVersion(element);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.scene.informMutation();
|
this.scene.informMutation();
|
||||||
@@ -1838,7 +1840,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === KEYS.G || event.key === KEYS.S) {
|
if (
|
||||||
|
event.key === KEYS.G ||
|
||||||
|
event.key === KEYS.S ||
|
||||||
|
event.key === KEYS.C
|
||||||
|
) {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
this.scene.getElements(),
|
this.scene.getElements(),
|
||||||
this.state,
|
this.state,
|
||||||
@@ -1860,6 +1866,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (event.key === KEYS.S) {
|
if (event.key === KEYS.S) {
|
||||||
this.setState({ openPopup: "strokeColorPicker" });
|
this.setState({ openPopup: "strokeColorPicker" });
|
||||||
}
|
}
|
||||||
|
if (event.key === KEYS.C) {
|
||||||
|
this.setState({ openPopup: "fontColorPicker" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -2164,28 +2173,40 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
window.devicePixelRatio,
|
window.devicePixelRatio,
|
||||||
);
|
);
|
||||||
|
|
||||||
// bind to container when shouldBind is true or
|
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
|
||||||
// clicked on center of container
|
let container: ExcalidrawTextContainer | null = null;
|
||||||
const container =
|
|
||||||
shouldBind || parentCenterPosition
|
|
||||||
? getElementContainingPosition(
|
|
||||||
this.scene.getElements().filter((ele) => !isTextElement(ele)),
|
|
||||||
sceneX,
|
|
||||||
sceneY,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
let existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
const selectedElements = getSelectedElements(
|
||||||
|
this.scene.getElements(),
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
|
||||||
// consider bounded text element if container present
|
if (selectedElements.length === 1) {
|
||||||
if (container) {
|
if (isTextElement(selectedElements[0])) {
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
existingTextElement = selectedElements[0];
|
||||||
if (boundTextElementId) {
|
} else if (isTextBindableContainer(selectedElements[0])) {
|
||||||
existingTextElement = this.scene.getElement(
|
container = selectedElements[0];
|
||||||
boundTextElementId,
|
existingTextElement = getBoundTextElement(container);
|
||||||
) as ExcalidrawTextElement;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
existingTextElement =
|
||||||
|
existingTextElement ?? this.getTextElementAtPosition(sceneX, sceneY);
|
||||||
|
|
||||||
|
// bind to container when shouldBind is true or
|
||||||
|
// clicked on center of container
|
||||||
|
if (
|
||||||
|
!container &&
|
||||||
|
!existingTextElement &&
|
||||||
|
(shouldBind || parentCenterPosition)
|
||||||
|
) {
|
||||||
|
container = getTextBindableContainerAtPosition(
|
||||||
|
this.scene.getElements().filter((ele) => !isTextElement(ele)),
|
||||||
|
sceneX,
|
||||||
|
sceneY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!existingTextElement && container) {
|
if (!existingTextElement && container) {
|
||||||
const fontString = {
|
const fontString = {
|
||||||
fontSize: this.state.currentItemFontSize,
|
fontSize: this.state.currentItemFontSize,
|
||||||
@@ -2233,7 +2254,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
? "center"
|
? "center"
|
||||||
: this.state.currentItemTextAlign,
|
: this.state.currentItemTextAlign,
|
||||||
verticalAlign: parentCenterPosition
|
verticalAlign: parentCenterPosition
|
||||||
? "middle"
|
? VERTICAL_ALIGN.MIDDLE
|
||||||
: DEFAULT_VERTICAL_ALIGN,
|
: DEFAULT_VERTICAL_ALIGN,
|
||||||
containerId: container?.id ?? undefined,
|
containerId: container?.id ?? undefined,
|
||||||
groupIds: container?.groupIds ?? [],
|
groupIds: container?.groupIds ?? [],
|
||||||
@@ -2241,19 +2262,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
this.setState({ editingElement: element });
|
this.setState({ editingElement: element });
|
||||||
|
|
||||||
if (existingTextElement) {
|
if (!existingTextElement) {
|
||||||
// if text element is no longer centered to a container, reset
|
|
||||||
// verticalAlign to default because it's currently internal-only
|
|
||||||
if (!parentCenterPosition || element.textAlign !== "center") {
|
|
||||||
mutateElement(element, { verticalAlign: DEFAULT_VERTICAL_ALIGN });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.scene.replaceAllElements([
|
this.scene.replaceAllElements([
|
||||||
...this.scene.getElementsIncludingDeleted(),
|
...this.scene.getElementsIncludingDeleted(),
|
||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// case: creating new text not centered to parent elemenent → offset Y
|
// case: creating new text not centered to parent element → offset Y
|
||||||
// so that the text is centered to cursor position
|
// so that the text is centered to cursor position
|
||||||
if (!parentCenterPosition) {
|
if (!parentCenterPosition) {
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
@@ -2449,7 +2464,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
this.savePointer(event.clientX, event.clientY, this.state.cursorButton);
|
this.savePointer(event.clientX, event.clientY, this.state.cursorButton);
|
||||||
|
|
||||||
if (gesture.pointers.has(event.pointerId)) {
|
if (gesture.pointers.has(event.pointerId)) {
|
||||||
gesture.pointers.set(event.pointerId, {
|
gesture.pointers.set(event.pointerId, {
|
||||||
x: event.clientX,
|
x: event.clientX,
|
||||||
@@ -2623,7 +2637,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (
|
if (
|
||||||
hasDeselectedButton ||
|
hasDeselectedButton ||
|
||||||
(this.state.elementType !== "selection" &&
|
(this.state.elementType !== "selection" &&
|
||||||
this.state.elementType !== "text")
|
this.state.elementType !== "text" &&
|
||||||
|
this.state.elementType !== "eraser")
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2698,8 +2713,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!this.state.showHyperlinkPopup
|
!this.state.showHyperlinkPopup
|
||||||
) {
|
) {
|
||||||
this.setState({ showHyperlinkPopup: "info" });
|
this.setState({ showHyperlinkPopup: "info" });
|
||||||
}
|
} else if (isEraserActive(this.state)) {
|
||||||
if (this.state.elementType === "text") {
|
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||||
|
} else if (this.state.elementType === "text") {
|
||||||
setCursor(
|
setCursor(
|
||||||
this.canvas,
|
this.canvas,
|
||||||
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
|
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
|
||||||
@@ -2740,6 +2756,80 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleEraser = (
|
||||||
|
event: PointerEvent,
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
scenePointer: { x: number; y: number },
|
||||||
|
) => {
|
||||||
|
const updateElementIds = (elements: ExcalidrawElement[]) => {
|
||||||
|
elements.forEach((element) => {
|
||||||
|
idsToUpdate.push(element.id);
|
||||||
|
if (event.altKey) {
|
||||||
|
if (pointerDownState.elementIdsToErase[element.id]) {
|
||||||
|
pointerDownState.elementIdsToErase[element.id] = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pointerDownState.elementIdsToErase[element.id] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const idsToUpdate: Array<string> = [];
|
||||||
|
|
||||||
|
const distance = distance2d(
|
||||||
|
pointerDownState.lastCoords.x,
|
||||||
|
pointerDownState.lastCoords.y,
|
||||||
|
scenePointer.x,
|
||||||
|
scenePointer.y,
|
||||||
|
);
|
||||||
|
const threshold = 10 / this.state.zoom.value;
|
||||||
|
const point = { ...pointerDownState.lastCoords };
|
||||||
|
let samplingInterval = 0;
|
||||||
|
while (samplingInterval <= distance) {
|
||||||
|
const hitElements = this.getElementsAtPosition(point.x, point.y);
|
||||||
|
updateElementIds(hitElements);
|
||||||
|
|
||||||
|
// Exit since we reached current point
|
||||||
|
if (samplingInterval === distance) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate next point in the line at a distance of sampling interval
|
||||||
|
samplingInterval = Math.min(samplingInterval + threshold, distance);
|
||||||
|
|
||||||
|
const distanceRatio = samplingInterval / distance;
|
||||||
|
const nextX =
|
||||||
|
(1 - distanceRatio) * point.x + distanceRatio * scenePointer.x;
|
||||||
|
const nextY =
|
||||||
|
(1 - distanceRatio) * point.y + distanceRatio * scenePointer.y;
|
||||||
|
point.x = nextX;
|
||||||
|
point.y = nextY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = this.scene.getElements().map((ele) => {
|
||||||
|
const id =
|
||||||
|
isBoundToContainer(ele) && idsToUpdate.includes(ele.containerId)
|
||||||
|
? ele.containerId
|
||||||
|
: ele.id;
|
||||||
|
if (idsToUpdate.includes(id)) {
|
||||||
|
if (event.altKey) {
|
||||||
|
if (pointerDownState.elementIdsToErase[id] === false) {
|
||||||
|
return newElementWith(ele, {
|
||||||
|
opacity: this.state.currentItemOpacity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return newElementWith(ele, { opacity: 20 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ele;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scene.replaceAllElements(elements);
|
||||||
|
|
||||||
|
pointerDownState.lastCoords.x = scenePointer.x;
|
||||||
|
pointerDownState.lastCoords.y = scenePointer.y;
|
||||||
|
};
|
||||||
// set touch moving for mobile context menu
|
// set touch moving for mobile context menu
|
||||||
private handleTouchMove = (event: React.TouchEvent<HTMLCanvasElement>) => {
|
private handleTouchMove = (event: React.TouchEvent<HTMLCanvasElement>) => {
|
||||||
invalidateContextMenu = true;
|
invalidateContextMenu = true;
|
||||||
@@ -2772,6 +2862,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (isPanning) {
|
if (isPanning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastPointerDown = event;
|
this.lastPointerDown = event;
|
||||||
this.setState({
|
this.setState({
|
||||||
lastPointerDownWith: event.pointerType,
|
lastPointerDownWith: event.pointerType,
|
||||||
@@ -2864,7 +2955,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.elementType,
|
this.state.elementType,
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
);
|
);
|
||||||
} else {
|
} else if (this.state.elementType !== "eraser") {
|
||||||
this.createGenericElementOnPointerDown(
|
this.createGenericElementOnPointerDown(
|
||||||
this.state.elementType,
|
this.state.elementType,
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
@@ -2899,6 +2990,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
) => {
|
) => {
|
||||||
this.lastPointerUp = event;
|
this.lastPointerUp = event;
|
||||||
const isTouchScreen = ["pen", "touch"].includes(event.pointerType);
|
const isTouchScreen = ["pen", "touch"].includes(event.pointerType);
|
||||||
|
|
||||||
if (isTouchScreen) {
|
if (isTouchScreen) {
|
||||||
const scenePointer = viewportCoordsToSceneCoords(
|
const scenePointer = viewportCoordsToSceneCoords(
|
||||||
{ clientX: event.clientX, clientY: event.clientY },
|
{ clientX: event.clientX, clientY: event.clientY },
|
||||||
@@ -2913,6 +3005,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
hitElement,
|
hitElement,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isEraserActive(this.state)) {
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this.hitLinkElement &&
|
this.hitLinkElement &&
|
||||||
!this.state.selectedElementIds[this.hitLinkElement.id]
|
!this.state.selectedElementIds[this.hitLinkElement.id]
|
||||||
@@ -3014,7 +3108,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
/*
|
/*
|
||||||
* Reenable next paste in case of disabled middle click paste for
|
* Reenable next paste in case of disabled middle click paste for
|
||||||
* any reason:
|
* any reason:
|
||||||
* - rigth click paste
|
* - right click paste
|
||||||
* - empty clipboard
|
* - empty clipboard
|
||||||
*/
|
*/
|
||||||
const enableNextPaste = () => {
|
const enableNextPaste = () => {
|
||||||
@@ -3138,6 +3232,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
boxSelection: {
|
boxSelection: {
|
||||||
hasOccurred: false,
|
hasOccurred: false,
|
||||||
},
|
},
|
||||||
|
elementIdsToErase: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3726,7 +3821,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
@@ -3737,6 +3831,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
|
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
|
||||||
|
|
||||||
|
if (isEraserActive(this.state)) {
|
||||||
|
this.handleEraser(event, pointerDownState, pointerCoords);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
pointerCoords.x,
|
pointerCoords.x,
|
||||||
pointerCoords.y,
|
pointerCoords.y,
|
||||||
@@ -4089,7 +4189,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isResizing,
|
isResizing,
|
||||||
isRotating,
|
isRotating,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
isRotating: false,
|
isRotating: false,
|
||||||
@@ -4310,6 +4409,33 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// Code below handles selection when element(s) weren't
|
// Code below handles selection when element(s) weren't
|
||||||
// drag or added to selection on pointer down phase.
|
// drag or added to selection on pointer down phase.
|
||||||
const hitElement = pointerDownState.hit.element;
|
const hitElement = pointerDownState.hit.element;
|
||||||
|
if (isEraserActive(this.state)) {
|
||||||
|
const draggedDistance = distance2d(
|
||||||
|
this.lastPointerDown!.clientX,
|
||||||
|
this.lastPointerDown!.clientY,
|
||||||
|
this.lastPointerUp!.clientX,
|
||||||
|
this.lastPointerUp!.clientY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (draggedDistance === 0) {
|
||||||
|
const scenePointer = viewportCoordsToSceneCoords(
|
||||||
|
{
|
||||||
|
clientX: this.lastPointerUp!.clientX,
|
||||||
|
clientY: this.lastPointerUp!.clientY,
|
||||||
|
},
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
const hitElement = this.getElementAtPosition(
|
||||||
|
scenePointer.x,
|
||||||
|
scenePointer.y,
|
||||||
|
);
|
||||||
|
|
||||||
|
pointerDownState.hit.element = hitElement;
|
||||||
|
}
|
||||||
|
this.eraseElements(pointerDownState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hitElement &&
|
hitElement &&
|
||||||
!pointerDownState.drag.hasOccurred &&
|
!pointerDownState.drag.hasOccurred &&
|
||||||
@@ -4449,6 +4575,27 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private eraseElements = (pointerDownState: PointerDownState) => {
|
||||||
|
const hitElement = pointerDownState.hit.element;
|
||||||
|
const elements = this.scene.getElements().map((ele) => {
|
||||||
|
if (pointerDownState.elementIdsToErase[ele.id]) {
|
||||||
|
return newElementWith(ele, { isDeleted: true });
|
||||||
|
} else if (hitElement && ele.id === hitElement.id) {
|
||||||
|
return newElementWith(ele, { isDeleted: true });
|
||||||
|
} else if (
|
||||||
|
isBoundToContainer(ele) &&
|
||||||
|
(pointerDownState.elementIdsToErase[ele.containerId] ||
|
||||||
|
(hitElement && ele.containerId === hitElement.id))
|
||||||
|
) {
|
||||||
|
return newElementWith(ele, { isDeleted: true });
|
||||||
|
}
|
||||||
|
return ele;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.history.resumeRecording();
|
||||||
|
this.scene.replaceAllElements(elements);
|
||||||
|
};
|
||||||
|
|
||||||
private initializeImage = async ({
|
private initializeImage = async ({
|
||||||
imageFile,
|
imageFile,
|
||||||
imageElement: _imageElement,
|
imageElement: _imageElement,
|
||||||
@@ -4750,7 +4897,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const width = height * (image.naturalWidth / image.naturalHeight);
|
const width = height * (image.naturalWidth / image.naturalHeight);
|
||||||
|
|
||||||
// add current imageElement width/height to account for previous centering
|
// add current imageElement width/height to account for previous centering
|
||||||
// of the placholder image
|
// of the placeholder image
|
||||||
const x = imageElement.x + imageElement.width / 2 - width / 2;
|
const x = imageElement.x + imageElement.width / 2 - width / 2;
|
||||||
const y = imageElement.y + imageElement.height / 2 - height / 2;
|
const y = imageElement.y + imageElement.height / 2 - height / 2;
|
||||||
|
|
||||||
@@ -4931,7 +5078,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private handleAppOnDrop = async (event: React.DragEvent<HTMLDivElement>) => {
|
private handleAppOnDrop = async (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
try {
|
try {
|
||||||
const file = event.dataTransfer.files[0];
|
const file = event.dataTransfer.files.item(0);
|
||||||
|
|
||||||
if (isSupportedImageFile(file)) {
|
if (isSupportedImageFile(file)) {
|
||||||
// first attempt to decode scene from the image if it's embedded
|
// first attempt to decode scene from the image if it's embedded
|
||||||
@@ -5007,7 +5154,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = event.dataTransfer?.files[0];
|
const file = event.dataTransfer?.files.item(0);
|
||||||
if (
|
if (
|
||||||
file?.type === MIME_TYPES.excalidrawlib ||
|
file?.type === MIME_TYPES.excalidrawlib ||
|
||||||
file?.name?.endsWith(".excalidrawlib")
|
file?.name?.endsWith(".excalidrawlib")
|
||||||
@@ -5023,7 +5170,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ isLoading: false, errorMessage: error.message }),
|
this.setState({ isLoading: false, errorMessage: error.message }),
|
||||||
);
|
);
|
||||||
// default: assume an Excalidraw file regardless of extension/MimeType
|
// default: assume an Excalidraw file regardless of extension/MimeType
|
||||||
} else {
|
} else if (file) {
|
||||||
this.setState({ isLoading: true });
|
this.setState({ isLoading: true });
|
||||||
if (nativeFileSystemSupported) {
|
if (nativeFileSystemSupported) {
|
||||||
try {
|
try {
|
||||||
@@ -5445,7 +5592,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
canvas: HTMLCanvasElement | null,
|
canvas: HTMLCanvasElement | null,
|
||||||
scale: number,
|
scale: number,
|
||||||
) {
|
) {
|
||||||
const elementClickedInside = getElementContainingPosition(
|
const elementClickedInside = getTextBindableContainerAtPosition(
|
||||||
this.scene
|
this.scene
|
||||||
.getElementsIncludingDeleted()
|
.getElementsIncludingDeleted()
|
||||||
.filter((element) => !isTextElement(element)),
|
.filter((element) => !isTextElement(element)),
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
top: -11px;
|
top: -11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker-content {
|
.color-picker-content--default {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, auto);
|
grid-template-columns: repeat(5, auto);
|
||||||
@@ -59,6 +59,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color-picker-content--canvas {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.25rem;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
color: $oc-gray-6;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-colors {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
|
.color-picker-swatch {
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.color-picker-content .color-input-container {
|
.color-picker-content .color-input-container {
|
||||||
grid-column: 1 / span 5;
|
grid-column: 1 / span 5;
|
||||||
}
|
}
|
||||||
@@ -235,7 +255,8 @@
|
|||||||
color: #aaa;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker-type-elementStroke .color-picker-keybinding {
|
.color-picker-type-elementStroke .color-picker-keybinding,
|
||||||
|
.color-picker-type-elementFontColor .color-picker-keybinding {
|
||||||
color: #d4d4d4;
|
color: #d4d4d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+167
-46
@@ -7,6 +7,53 @@ import { isArrowKey, KEYS } from "../keys";
|
|||||||
import { t, getLanguage } from "../i18n";
|
import { t, getLanguage } from "../i18n";
|
||||||
import { isWritableElement } from "../utils";
|
import { isWritableElement } from "../utils";
|
||||||
import colors from "../colors";
|
import colors from "../colors";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
|
||||||
|
const MAX_CUSTOM_COLORS = 5;
|
||||||
|
const MAX_DEFAULT_COLORS = 15;
|
||||||
|
|
||||||
|
export const getCustomColors = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
type: "elementBackground" | "elementStroke",
|
||||||
|
) => {
|
||||||
|
const customColors: string[] = [];
|
||||||
|
const updatedElements = elements
|
||||||
|
.filter((element) => !element.isDeleted)
|
||||||
|
.sort((ele1, ele2) => ele2.updated - ele1.updated);
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
const elementColorTypeMap = {
|
||||||
|
elementBackground: "backgroundColor",
|
||||||
|
elementStroke: "strokeColor",
|
||||||
|
};
|
||||||
|
const colorType = elementColorTypeMap[type] as
|
||||||
|
| "backgroundColor"
|
||||||
|
| "strokeColor";
|
||||||
|
while (
|
||||||
|
index < updatedElements.length &&
|
||||||
|
customColors.length < MAX_CUSTOM_COLORS
|
||||||
|
) {
|
||||||
|
const element = updatedElements[index];
|
||||||
|
|
||||||
|
if (
|
||||||
|
customColors.length < MAX_CUSTOM_COLORS &&
|
||||||
|
isCustomColor(element[colorType], type) &&
|
||||||
|
!customColors.includes(element[colorType])
|
||||||
|
) {
|
||||||
|
customColors.push(element[colorType]);
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return customColors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCustomColor = (
|
||||||
|
color: string,
|
||||||
|
type: "elementBackground" | "elementStroke",
|
||||||
|
) => {
|
||||||
|
return !colors[type].includes(color);
|
||||||
|
};
|
||||||
|
|
||||||
const isValidColor = (color: string) => {
|
const isValidColor = (color: string) => {
|
||||||
const style = new Option().style;
|
const style = new Option().style;
|
||||||
@@ -35,6 +82,7 @@ const keyBindings = [
|
|||||||
["1", "2", "3", "4", "5"],
|
["1", "2", "3", "4", "5"],
|
||||||
["q", "w", "e", "r", "t"],
|
["q", "w", "e", "r", "t"],
|
||||||
["a", "s", "d", "f", "g"],
|
["a", "s", "d", "f", "g"],
|
||||||
|
["z", "x", "c", "v", "b"],
|
||||||
].flat();
|
].flat();
|
||||||
|
|
||||||
const Picker = ({
|
const Picker = ({
|
||||||
@@ -45,6 +93,7 @@ const Picker = ({
|
|||||||
label,
|
label,
|
||||||
showInput = true,
|
showInput = true,
|
||||||
type,
|
type,
|
||||||
|
elements,
|
||||||
}: {
|
}: {
|
||||||
colors: string[];
|
colors: string[];
|
||||||
color: string | null;
|
color: string | null;
|
||||||
@@ -52,12 +101,25 @@ const Picker = ({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
label: string;
|
label: string;
|
||||||
showInput: boolean;
|
showInput: boolean;
|
||||||
type: "canvasBackground" | "elementBackground" | "elementStroke";
|
type:
|
||||||
|
| "canvasBackground"
|
||||||
|
| "elementBackground"
|
||||||
|
| "elementStroke"
|
||||||
|
| "elementFontColor";
|
||||||
|
elements: readonly ExcalidrawElement[];
|
||||||
}) => {
|
}) => {
|
||||||
const firstItem = React.useRef<HTMLButtonElement>();
|
const firstItem = React.useRef<HTMLButtonElement>();
|
||||||
const activeItem = React.useRef<HTMLButtonElement>();
|
const activeItem = React.useRef<HTMLButtonElement>();
|
||||||
const gallery = React.useRef<HTMLDivElement>();
|
const gallery = React.useRef<HTMLDivElement>();
|
||||||
const colorInput = React.useRef<HTMLInputElement>();
|
const colorInput = React.useRef<HTMLInputElement>();
|
||||||
|
const colorType = type === "elementFontColor" ? "elementStroke" : type;
|
||||||
|
|
||||||
|
const [customColors] = React.useState(() => {
|
||||||
|
if (colorType === "canvasBackground") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getCustomColors(elements, colorType);
|
||||||
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// After the component is first mounted focus on first input
|
// After the component is first mounted focus on first input
|
||||||
@@ -85,23 +147,42 @@ const Picker = ({
|
|||||||
} else if (isArrowKey(event.key)) {
|
} else if (isArrowKey(event.key)) {
|
||||||
const { activeElement } = document;
|
const { activeElement } = document;
|
||||||
const isRTL = getLanguage().rtl;
|
const isRTL = getLanguage().rtl;
|
||||||
const index = Array.prototype.indexOf.call(
|
let isCustom = false;
|
||||||
gallery!.current!.children,
|
let index = Array.prototype.indexOf.call(
|
||||||
|
gallery!.current!.querySelector(".color-picker-content--default")!
|
||||||
|
.children,
|
||||||
activeElement,
|
activeElement,
|
||||||
);
|
);
|
||||||
|
if (index === -1) {
|
||||||
|
index = Array.prototype.indexOf.call(
|
||||||
|
gallery!.current!.querySelector(
|
||||||
|
".color-picker-content--canvas-colors",
|
||||||
|
)!.children,
|
||||||
|
activeElement,
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
isCustom = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parentSelector = isCustom
|
||||||
|
? gallery!.current!.querySelector(
|
||||||
|
".color-picker-content--canvas-colors",
|
||||||
|
)!
|
||||||
|
: gallery!.current!.querySelector(".color-picker-content--default")!;
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const length = gallery!.current!.children.length - (showInput ? 1 : 0);
|
const length = parentSelector!.children.length - (showInput ? 1 : 0);
|
||||||
const nextIndex =
|
const nextIndex =
|
||||||
event.key === (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT)
|
event.key === (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT)
|
||||||
? (index + 1) % length
|
? (index + 1) % length
|
||||||
: event.key === (isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT)
|
: event.key === (isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT)
|
||||||
? (length + index - 1) % length
|
? (length + index - 1) % length
|
||||||
: event.key === KEYS.ARROW_DOWN
|
: !isCustom && event.key === KEYS.ARROW_DOWN
|
||||||
? (index + 5) % length
|
? (index + 5) % length
|
||||||
: event.key === KEYS.ARROW_UP
|
: !isCustom && event.key === KEYS.ARROW_UP
|
||||||
? (length + index - 5) % length
|
? (length + index - 5) % length
|
||||||
: index;
|
: index;
|
||||||
(gallery!.current!.children![nextIndex] as any).focus();
|
(parentSelector!.children![nextIndex] as HTMLElement)?.focus();
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (
|
} else if (
|
||||||
@@ -109,7 +190,15 @@ const Picker = ({
|
|||||||
!isWritableElement(event.target)
|
!isWritableElement(event.target)
|
||||||
) {
|
) {
|
||||||
const index = keyBindings.indexOf(event.key.toLowerCase());
|
const index = keyBindings.indexOf(event.key.toLowerCase());
|
||||||
(gallery!.current!.children![index] as any).focus();
|
const isCustom = index >= MAX_DEFAULT_COLORS;
|
||||||
|
const parentSelector = isCustom
|
||||||
|
? gallery!.current!.querySelector(
|
||||||
|
".color-picker-content--canvas-colors",
|
||||||
|
)!
|
||||||
|
: gallery!.current!.querySelector(".color-picker-content--default")!;
|
||||||
|
const actualIndex = isCustom ? index - MAX_DEFAULT_COLORS : index;
|
||||||
|
(parentSelector!.children![actualIndex] as HTMLElement)?.focus();
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
|
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -119,6 +208,50 @@ const Picker = ({
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderColors = (colors: Array<string>, custom: boolean = false) => {
|
||||||
|
return colors.map((_color, i) => {
|
||||||
|
const _colorWithoutHash = _color.replace("#", "");
|
||||||
|
const keyBinding = custom
|
||||||
|
? keyBindings[i + MAX_DEFAULT_COLORS]
|
||||||
|
: keyBindings[i];
|
||||||
|
const label = custom
|
||||||
|
? _colorWithoutHash
|
||||||
|
: t(`colors.${_colorWithoutHash}`);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="color-picker-swatch"
|
||||||
|
onClick={(event) => {
|
||||||
|
(event.currentTarget as HTMLButtonElement).focus();
|
||||||
|
onChange(_color);
|
||||||
|
}}
|
||||||
|
title={`${label}${
|
||||||
|
!isTransparent(_color) ? ` (${_color})` : ""
|
||||||
|
} — ${keyBinding.toUpperCase()}`}
|
||||||
|
aria-label={label}
|
||||||
|
aria-keyshortcuts={keyBindings[i]}
|
||||||
|
style={{ color: _color }}
|
||||||
|
key={_color}
|
||||||
|
ref={(el) => {
|
||||||
|
if (!custom && el && i === 0) {
|
||||||
|
firstItem.current = el;
|
||||||
|
}
|
||||||
|
if (el && _color === color) {
|
||||||
|
activeItem.current = el;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
onChange(_color);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isTransparent(_color) ? (
|
||||||
|
<div className="color-picker-transparent"></div>
|
||||||
|
) : undefined}
|
||||||
|
<span className="color-picker-keybinding">{keyBinding}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`color-picker color-picker-type-${type}`}
|
className={`color-picker color-picker-type-${type}`}
|
||||||
@@ -138,41 +271,20 @@ const Picker = ({
|
|||||||
}}
|
}}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
{colors.map((_color, i) => {
|
<div className="color-picker-content--default">
|
||||||
const _colorWithoutHash = _color.replace("#", "");
|
{renderColors(colors)}
|
||||||
return (
|
</div>
|
||||||
<button
|
{!!customColors.length && (
|
||||||
className="color-picker-swatch"
|
<div className="color-picker-content--canvas">
|
||||||
onClick={(event) => {
|
<span className="color-picker-content--canvas-title">
|
||||||
(event.currentTarget as HTMLButtonElement).focus();
|
{t("labels.canvasColors")}
|
||||||
onChange(_color);
|
</span>
|
||||||
}}
|
<div className="color-picker-content--canvas-colors">
|
||||||
title={`${t(`colors.${_colorWithoutHash}`)}${
|
{renderColors(customColors, true)}
|
||||||
!isTransparent(_color) ? ` (${_color})` : ""
|
</div>
|
||||||
} — ${keyBindings[i].toUpperCase()}`}
|
</div>
|
||||||
aria-label={t(`colors.${_colorWithoutHash}`)}
|
)}
|
||||||
aria-keyshortcuts={keyBindings[i]}
|
|
||||||
style={{ color: _color }}
|
|
||||||
key={_color}
|
|
||||||
ref={(el) => {
|
|
||||||
if (el && i === 0) {
|
|
||||||
firstItem.current = el;
|
|
||||||
}
|
|
||||||
if (el && _color === color) {
|
|
||||||
activeItem.current = el;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFocus={() => {
|
|
||||||
onChange(_color);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isTransparent(_color) ? (
|
|
||||||
<div className="color-picker-transparent"></div>
|
|
||||||
) : undefined}
|
|
||||||
<span className="color-picker-keybinding">{keyBindings[i]}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<ColorInput
|
<ColorInput
|
||||||
color={color}
|
color={color}
|
||||||
@@ -246,16 +358,24 @@ export const ColorPicker = ({
|
|||||||
label,
|
label,
|
||||||
isActive,
|
isActive,
|
||||||
setActive,
|
setActive,
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
}: {
|
}: {
|
||||||
type: "canvasBackground" | "elementBackground" | "elementStroke";
|
type:
|
||||||
|
| "canvasBackground"
|
||||||
|
| "elementBackground"
|
||||||
|
| "elementStroke"
|
||||||
|
| "elementFontColor";
|
||||||
color: string | null;
|
color: string | null;
|
||||||
onChange: (color: string) => void;
|
onChange: (color: string) => void;
|
||||||
label: string;
|
label: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
setActive: (active: boolean) => void;
|
setActive: (active: boolean) => void;
|
||||||
|
elements: readonly ExcalidrawElement[];
|
||||||
|
appState: AppState;
|
||||||
}) => {
|
}) => {
|
||||||
const pickerButton = React.useRef<HTMLButtonElement>(null);
|
const pickerButton = React.useRef<HTMLButtonElement>(null);
|
||||||
|
const colorType = type === "elementFontColor" ? "elementStroke" : type;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="color-picker-control-container">
|
<div className="color-picker-control-container">
|
||||||
@@ -282,7 +402,7 @@ export const ColorPicker = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Picker
|
<Picker
|
||||||
colors={colors[type]}
|
colors={colors[colorType]}
|
||||||
color={color || null}
|
color={color || null}
|
||||||
onChange={(changedColor) => {
|
onChange={(changedColor) => {
|
||||||
onChange(changedColor);
|
onChange(changedColor);
|
||||||
@@ -294,6 +414,7 @@ export const ColorPicker = ({
|
|||||||
label={label}
|
label={label}
|
||||||
showInput={false}
|
showInput={false}
|
||||||
type={type}
|
type={type}
|
||||||
|
elements={elements}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
|
import { isEraserActive } from "../appState";
|
||||||
|
|
||||||
interface HintViewerProps {
|
interface HintViewerProps {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@@ -22,6 +23,9 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
|
|||||||
const { elementType, isResizing, isRotating, lastPointerDownWith } = appState;
|
const { elementType, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||||
const multiMode = appState.multiElement !== null;
|
const multiMode = appState.multiElement !== null;
|
||||||
|
|
||||||
|
if (isEraserActive(appState)) {
|
||||||
|
return t("hints.eraserRevert");
|
||||||
|
}
|
||||||
if (elementType === "arrow" || elementType === "line") {
|
if (elementType === "arrow" || elementType === "line") {
|
||||||
if (!multiMode) {
|
if (!multiMode) {
|
||||||
return t("hints.linearElement");
|
return t("hints.linearElement");
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ const LayerUI = ({
|
|||||||
className={CLASSES.SHAPE_ACTIONS_MENU}
|
className={CLASSES.SHAPE_ACTIONS_MENU}
|
||||||
padding={2}
|
padding={2}
|
||||||
style={{
|
style={{
|
||||||
// we want to make sure this doesn't overflow so substracting 200
|
// we want to make sure this doesn't overflow so subtracting 200
|
||||||
// which is approximately height of zoom footer and top left menu items with some buffer
|
// which is approximately height of zoom footer and top left menu items with some buffer
|
||||||
// if active file name is displayed, subtracting 248 to account for its height
|
// if active file name is displayed, subtracting 248 to account for its height
|
||||||
maxHeight: `${appState.height - (appState.fileHandle ? 248 : 200)}px`,
|
maxHeight: `${appState.height - (appState.fileHandle ? 248 : 200)}px`,
|
||||||
@@ -428,6 +428,14 @@ const LayerUI = ({
|
|||||||
{actionManager.renderAction("redo", { size: "small" })}
|
{actionManager.renderAction("redo", { size: "small" })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div
|
||||||
|
className={clsx("eraser-buttons zen-mode-transition", {
|
||||||
|
"layer-ui__wrapper__footer-left--transition-left":
|
||||||
|
zenModeEnabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{actionManager.renderAction("eraser", { size: "small" })}
|
||||||
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { NonDeletedExcalidrawElement } from "../element/types";
|
|||||||
import { FixedSideContainer } from "./FixedSideContainer";
|
import { FixedSideContainer } from "./FixedSideContainer";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { HintViewer } from "./HintViewer";
|
import { HintViewer } from "./HintViewer";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
import CollabButton from "./CollabButton";
|
import CollabButton from "./CollabButton";
|
||||||
@@ -113,6 +113,12 @@ export const MobileMenu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderAppToolbar = () => {
|
const renderAppToolbar = () => {
|
||||||
|
// Render eraser conditionally in mobile
|
||||||
|
const showEraser =
|
||||||
|
!appState.viewModeEnabled &&
|
||||||
|
!appState.editingElement &&
|
||||||
|
getSelectedElements(elements, appState).length === 0;
|
||||||
|
|
||||||
if (viewModeEnabled) {
|
if (viewModeEnabled) {
|
||||||
return (
|
return (
|
||||||
<div className="App-toolbar-content">
|
<div className="App-toolbar-content">
|
||||||
@@ -120,12 +126,16 @@ export const MobileMenu = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App-toolbar-content">
|
<div className="App-toolbar-content">
|
||||||
{actionManager.renderAction("toggleCanvasMenu")}
|
{actionManager.renderAction("toggleCanvasMenu")}
|
||||||
{actionManager.renderAction("toggleEditMenu")}
|
{actionManager.renderAction("toggleEditMenu")}
|
||||||
|
|
||||||
{actionManager.renderAction("undo")}
|
{actionManager.renderAction("undo")}
|
||||||
{actionManager.renderAction("redo")}
|
{actionManager.renderAction("redo")}
|
||||||
|
{showEraser && actionManager.renderAction("eraser")}
|
||||||
|
|
||||||
{actionManager.renderAction(
|
{actionManager.renderAction(
|
||||||
appState.multiElement ? "finalize" : "duplicateSelection",
|
appState.multiElement ? "finalize" : "duplicateSelection",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -87,8 +87,14 @@
|
|||||||
|
|
||||||
.ToolIcon {
|
.ToolIcon {
|
||||||
&:hover {
|
&:hover {
|
||||||
--icon-fill-color: var(--color-primary-chubb);
|
--icon-fill-color: var(
|
||||||
--keybinding-color: var(--color-primary-chubb);
|
--color-primary-contrast-offset,
|
||||||
|
var(--color-primary)
|
||||||
|
);
|
||||||
|
--keybinding-color: var(
|
||||||
|
--color-primary-contrast-offset,
|
||||||
|
var(--color-primary)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
&:active {
|
&:active {
|
||||||
--icon-fill-color: #{$oc-gray-9};
|
--icon-fill-color: #{$oc-gray-9};
|
||||||
|
|||||||
@@ -885,6 +885,40 @@ export const TextAlignRightIcon = React.memo(({ theme }: { theme: Theme }) =>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const TextAlignTopIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
d="m16,132l416,0c8.837,0 16,-7.163 16,-16l0,-40c0,-8.837 -7.163,-16 -16,-16l-416,0c-8.837,0 -16,7.163 -16,16l0,40c0,8.837 7.163,16 16,16zm0,160l416,0c8.837,0 16,-7.163 16,-16l0,-40c0,-8.837 -7.163,-16 -16,-16l-416,0c-8.837,0 -16,7.163 -16,16l0,40c0,8.837 7.163,16 16,16z"
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>,
|
||||||
|
{ width: 448, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TextAlignBottomIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
d="M16,292L432,292C440.837,292 448,284.837 448,276L448,236C448,227.163 440.837,220 432,220L16,220C7.163,220 0,227.163 0,236L0,276C0,284.837 7.163,292 16,292ZM16,452L432,452C440.837,452 448,444.837 448,436L448,396C448,387.163 440.837,380 432,380L16,380C7.163,380 0,387.163 0,396L0,436C0,444.837 7.163,452 16,452Z"
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>,
|
||||||
|
{ width: 448, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TextAlignMiddleIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||||
|
createIcon(
|
||||||
|
<path
|
||||||
|
transform="matrix(1,0,0,1,0,80)"
|
||||||
|
d="M16,132L432,132C440.837,132 448,124.837 448,116L448,76C448,67.163 440.837,60 432,60L16,60C7.163,60 0,67.163 0,76L0,116C0,124.837 7.163,132 16,132ZM16,292L432,292C440.837,292 448,284.837 448,276L448,236C448,227.163 440.837,220 432,220L16,220C7.163,220 0,227.163 0,236L0,276C0,284.837 7.163,292 16,292Z"
|
||||||
|
fill={iconFillColor(theme)}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>,
|
||||||
|
{ width: 448, height: 512 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const publishIcon = createIcon(
|
export const publishIcon = createIcon(
|
||||||
<path
|
<path
|
||||||
d="M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4zM393.4 288H328v112c0 8.8-7.2 16-16 16h-48c-8.8 0-16-7.2-16-16V288h-65.4c-14.3 0-21.4-17.2-11.3-27.3l105.4-105.4c6.2-6.2 16.4-6.2 22.6 0l105.4 105.4c10.1 10.1 2.9 27.3-11.3 27.3z"
|
d="M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4zM393.4 288H328v112c0 8.8-7.2 16-16 16h-48c-8.8 0-16-7.2-16-16V288h-65.4c-14.3 0-21.4-17.2-11.3-27.3l105.4-105.4c6.2-6.2 16.4-6.2 22.6 0l105.4 105.4c10.1 10.1 2.9 27.3-11.3 27.3z"
|
||||||
@@ -900,3 +934,7 @@ export const editIcon = createIcon(
|
|||||||
></path>,
|
></path>,
|
||||||
{ width: 640, height: 512 },
|
{ width: 640, height: 512 },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const eraser = createIcon(
|
||||||
|
<path d="M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z" />,
|
||||||
|
);
|
||||||
|
|||||||
@@ -182,3 +182,9 @@ export const VERSIONS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const BOUND_TEXT_PADDING = 5;
|
export const BOUND_TEXT_PADDING = 5;
|
||||||
|
|
||||||
|
export const VERTICAL_ALIGN = {
|
||||||
|
TOP: "top",
|
||||||
|
MIDDLE: "middle",
|
||||||
|
BOTTOM: "bottom",
|
||||||
|
};
|
||||||
|
|||||||
+12
-1
@@ -290,6 +290,16 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.eraser {
|
||||||
|
&.ToolIcon:hover {
|
||||||
|
--icon-fill-color: #fff;
|
||||||
|
--keybinding-color: #fff;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-toolbar-content {
|
.App-toolbar-content {
|
||||||
@@ -467,7 +477,8 @@
|
|||||||
font-family: var(--ui-font);
|
font-family: var(--ui-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
.undo-redo-buttons {
|
.undo-redo-buttons,
|
||||||
|
.eraser-buttons {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
gap: 0.4em;
|
gap: 0.4em;
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
--text-primary-color: #{$oc-gray-8};
|
--text-primary-color: #{$oc-gray-8};
|
||||||
|
|
||||||
--color-primary: #6965db;
|
--color-primary: #6965db;
|
||||||
--color-primary-chubb: #625ee0; // to offset Chubb illusion
|
|
||||||
--color-primary-darker: #5b57d1;
|
--color-primary-darker: #5b57d1;
|
||||||
--color-primary-darkest: #4a47b1;
|
--color-primary-darkest: #4a47b1;
|
||||||
--color-primary-light: #e2e1fc;
|
--color-primary-light: #e2e1fc;
|
||||||
@@ -85,7 +84,6 @@
|
|||||||
--text-primary-color: #{$oc-gray-4};
|
--text-primary-color: #{$oc-gray-4};
|
||||||
|
|
||||||
--color-primary: #5650f0;
|
--color-primary: #5650f0;
|
||||||
--color-primary-chubb: #726dff; // to offset Chubb illusion
|
|
||||||
--color-primary-darker: #4b46d8;
|
--color-primary-darker: #4b46d8;
|
||||||
--color-primary-darkest: #3e39be;
|
--color-primary-darkest: #3e39be;
|
||||||
--color-primary-light: #3f3d64;
|
--color-primary-light: #3f3d64;
|
||||||
|
|||||||
+3
-2
@@ -31,8 +31,8 @@ type RestoredAppState = Omit<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export const AllowedExcalidrawElementTypes: Record<
|
export const AllowedExcalidrawElementTypes: Record<
|
||||||
ExcalidrawElement["type"],
|
AppState["elementType"],
|
||||||
true
|
boolean
|
||||||
> = {
|
> = {
|
||||||
selection: true,
|
selection: true,
|
||||||
text: true,
|
text: true,
|
||||||
@@ -43,6 +43,7 @@ export const AllowedExcalidrawElementTypes: Record<
|
|||||||
image: true,
|
image: true,
|
||||||
arrow: true,
|
arrow: true,
|
||||||
freedraw: true,
|
freedraw: true,
|
||||||
|
eraser: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RestoredDataState = {
|
export type RestoredDataState = {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { isPointHittingElementBoundingBox } from "./collision";
|
|||||||
import { getElementAbsoluteCoords } from "./";
|
import { getElementAbsoluteCoords } from "./";
|
||||||
|
|
||||||
import "./Hyperlink.scss";
|
import "./Hyperlink.scss";
|
||||||
|
import { trackEvent } from "../analytics";
|
||||||
|
|
||||||
const CONTAINER_WIDTH = 320;
|
const CONTAINER_WIDTH = 320;
|
||||||
const SPACE_BOTTOM = 85;
|
const SPACE_BOTTOM = 85;
|
||||||
@@ -69,6 +70,10 @@ export const Hyperlink = ({
|
|||||||
|
|
||||||
const link = normalizeLink(inputRef.current.value);
|
const link = normalizeLink(inputRef.current.value);
|
||||||
|
|
||||||
|
if (!element.link && link) {
|
||||||
|
trackEvent("hyperlink", "create");
|
||||||
|
}
|
||||||
|
|
||||||
mutateElement(element, { link });
|
mutateElement(element, { link });
|
||||||
setAppState({ showHyperlinkPopup: "info" });
|
setAppState({ showHyperlinkPopup: "info" });
|
||||||
}, [element, setAppState]);
|
}, [element, setAppState]);
|
||||||
@@ -108,6 +113,7 @@ export const Hyperlink = ({
|
|||||||
}, [appState, element, isEditing, setAppState]);
|
}, [appState, element, isEditing, setAppState]);
|
||||||
|
|
||||||
const handleRemove = useCallback(() => {
|
const handleRemove = useCallback(() => {
|
||||||
|
trackEvent("hyperlink", "delete");
|
||||||
mutateElement(element, { link: null });
|
mutateElement(element, { link: null });
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
inputRef.current!.value = "";
|
inputRef.current!.value = "";
|
||||||
@@ -116,13 +122,15 @@ export const Hyperlink = ({
|
|||||||
}, [setAppState, element, isEditing]);
|
}, [setAppState, element, isEditing]);
|
||||||
|
|
||||||
const onEdit = () => {
|
const onEdit = () => {
|
||||||
|
trackEvent("hyperlink", "edit", "popup-ui");
|
||||||
setAppState({ showHyperlinkPopup: "editor" });
|
setAppState({ showHyperlinkPopup: "editor" });
|
||||||
};
|
};
|
||||||
const { x, y } = getCoordsForPopover(element, appState);
|
const { x, y } = getCoordsForPopover(element, appState);
|
||||||
if (
|
if (
|
||||||
appState.draggingElement ||
|
appState.draggingElement ||
|
||||||
appState.resizingElement ||
|
appState.resizingElement ||
|
||||||
appState.isRotating
|
appState.isRotating ||
|
||||||
|
appState.openMenu
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -238,20 +246,25 @@ export const isLocalLink = (link: string | null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actionLink = register({
|
export const actionLink = register({
|
||||||
name: "link",
|
name: "hyperlink",
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
if (appState.showHyperlinkPopup === "editor") {
|
if (appState.showHyperlinkPopup === "editor") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements,
|
elements,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
showHyperlinkPopup: "editor",
|
showHyperlinkPopup: "editor",
|
||||||
|
openMenu: null,
|
||||||
},
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
trackEvent: (action, source) => {
|
||||||
|
trackEvent("hyperlink", "edit", source);
|
||||||
|
},
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
|
||||||
contextItemLabel: (elements, appState) =>
|
contextItemLabel: (elements, appState) =>
|
||||||
getContextMenuLabel(elements, appState),
|
getContextMenuLabel(elements, appState),
|
||||||
@@ -398,6 +411,7 @@ const renderTooltip = (
|
|||||||
},
|
},
|
||||||
"top",
|
"top",
|
||||||
);
|
);
|
||||||
|
trackEvent("hyperlink", "tooltip", "link-icon");
|
||||||
|
|
||||||
IS_HYPERLINK_TOOLTIP_VISIBLE = true;
|
IS_HYPERLINK_TOOLTIP_VISIBLE = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ const getCorners = (
|
|||||||
|
|
||||||
// Returns intersection of `line` with `segment`, with `segment` moved by
|
// Returns intersection of `line` with `segment`, with `segment` moved by
|
||||||
// `gap` in its polar direction.
|
// `gap` in its polar direction.
|
||||||
// If intersection conincides with second segment point returns empty array.
|
// If intersection coincides with second segment point returns empty array.
|
||||||
const intersectSegment = (
|
const intersectSegment = (
|
||||||
line: GA.Line,
|
line: GA.Line,
|
||||||
segment: [GA.Point, GA.Point],
|
segment: [GA.Point, GA.Point],
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { SHAPES } from "../shapes";
|
|
||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import { getCommonBounds } from "./bounds";
|
import { getCommonBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import { NonDeletedExcalidrawElement } from "./types";
|
import { NonDeletedExcalidrawElement } from "./types";
|
||||||
import { AppState, PointerDownState } from "../types";
|
import { AppState, PointerDownState } from "../types";
|
||||||
import { getBoundTextElementId } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { isSelectedViaGroup } from "../groups";
|
import { isSelectedViaGroup } from "../groups";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
@@ -39,16 +37,14 @@ export const dragSelectedElements = (
|
|||||||
// container is part of a group, but we're dragging the container directly
|
// container is part of a group, but we're dragging the container directly
|
||||||
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
||||||
) {
|
) {
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const textElement = getBoundTextElement(element);
|
||||||
if (boundTextElementId) {
|
if (textElement) {
|
||||||
const textElement =
|
|
||||||
Scene.getScene(element)!.getElement(boundTextElementId);
|
|
||||||
updateElementCoords(
|
updateElementCoords(
|
||||||
lockDirection,
|
lockDirection,
|
||||||
distanceX,
|
distanceX,
|
||||||
distanceY,
|
distanceY,
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
textElement!,
|
textElement,
|
||||||
offset,
|
offset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -96,7 +92,7 @@ export const getDragOffsetXY = (
|
|||||||
|
|
||||||
export const dragNewElement = (
|
export const dragNewElement = (
|
||||||
draggingElement: NonDeletedExcalidrawElement,
|
draggingElement: NonDeletedExcalidrawElement,
|
||||||
elementType: typeof SHAPES[number]["value"],
|
elementType: AppState["elementType"],
|
||||||
originX: number,
|
originX: number,
|
||||||
originY: number,
|
originY: number,
|
||||||
x: number,
|
x: number,
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ export class LinearElementEditor {
|
|||||||
ret.hitElement = element;
|
ret.hitElement = element;
|
||||||
} else {
|
} else {
|
||||||
// You might be wandering why we are storing the binding elements on
|
// You might be wandering why we are storing the binding elements on
|
||||||
// LinearElementEditor and passing them in, insted of calculating them
|
// LinearElementEditor and passing them in, instead of calculating them
|
||||||
// from the end points of the `linearElement` - this is to allow disabling
|
// from the end points of the `linearElement` - this is to allow disabling
|
||||||
// binding (which needs to happen at the point the user finishes moving
|
// binding (which needs to happen at the point the user finishes moving
|
||||||
// the point).
|
// the point).
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ import { getElementAbsoluteCoords } from ".";
|
|||||||
import { adjustXYWithRotation } from "../math";
|
import { adjustXYWithRotation } from "../math";
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import { getContainerElement, measureText, wrapText } from "./textElement";
|
import { getContainerElement, measureText, wrapText } from "./textElement";
|
||||||
import { isBoundToContainer } from "./typeChecks";
|
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
||||||
import { BOUND_TEXT_PADDING } from "../constants";
|
|
||||||
|
|
||||||
type ElementConstructorOpts = MarkOptional<
|
type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
@@ -175,7 +174,7 @@ const getAdjustedDimensions = (
|
|||||||
let y: number;
|
let y: number;
|
||||||
if (
|
if (
|
||||||
textAlign === "center" &&
|
textAlign === "center" &&
|
||||||
verticalAlign === "middle" &&
|
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
||||||
!element.containerId
|
!element.containerId
|
||||||
) {
|
) {
|
||||||
const prevMetrics = measureText(
|
const prevMetrics = measureText(
|
||||||
@@ -221,8 +220,7 @@ const getAdjustedDimensions = (
|
|||||||
|
|
||||||
// make sure container dimensions are set properly when
|
// make sure container dimensions are set properly when
|
||||||
// text editor overflows beyond viewport dimensions
|
// text editor overflows beyond viewport dimensions
|
||||||
if (isBoundToContainer(element)) {
|
if (container) {
|
||||||
const container = getContainerElement(element)!;
|
|
||||||
let height = container.height;
|
let height = container.height;
|
||||||
let width = container.width;
|
let width = container.width;
|
||||||
if (nextHeight > height - BOUND_TEXT_PADDING * 2) {
|
if (nextHeight > height - BOUND_TEXT_PADDING * 2) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
import { BOUND_TEXT_PADDING, SHIFT_LOCKING_ANGLE } from "../constants";
|
||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
import { Point, PointerDownState } from "../types";
|
import { Point, PointerDownState } from "../types";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
|
getApproxMinLineHeight,
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
@@ -105,7 +106,7 @@ export const transformElements = (
|
|||||||
updateBoundElements(element);
|
updateBoundElements(element);
|
||||||
} else if (transformHandleType) {
|
} else if (transformHandleType) {
|
||||||
resizeSingleElement(
|
resizeSingleElement(
|
||||||
pointerDownState.originalElements.get(element.id) as typeof element,
|
pointerDownState.originalElements,
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
element,
|
element,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
@@ -140,7 +141,6 @@ export const transformElements = (
|
|||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
);
|
);
|
||||||
handleBindTextResize(selectedElements, transformHandleType);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,7 +397,7 @@ const resizeSingleTextElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const resizeSingleElement = (
|
export const resizeSingleElement = (
|
||||||
stateAtResizeStart: NonDeletedExcalidrawElement,
|
originalElements: PointerDownState["originalElements"],
|
||||||
shouldMaintainAspectRatio: boolean,
|
shouldMaintainAspectRatio: boolean,
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
transformHandleDirection: TransformHandleDirection,
|
transformHandleDirection: TransformHandleDirection,
|
||||||
@@ -405,6 +405,7 @@ export const resizeSingleElement = (
|
|||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
) => {
|
) => {
|
||||||
|
const stateAtResizeStart = originalElements.get(element.id)!;
|
||||||
// Gets bounds corners
|
// Gets bounds corners
|
||||||
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
|
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
|
||||||
stateAtResizeStart,
|
stateAtResizeStart,
|
||||||
@@ -429,8 +430,6 @@ export const resizeSingleElement = (
|
|||||||
element.height,
|
element.height,
|
||||||
);
|
);
|
||||||
|
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
|
||||||
|
|
||||||
const boundsCurrentWidth = esx2 - esx1;
|
const boundsCurrentWidth = esx2 - esx1;
|
||||||
const boundsCurrentHeight = esy2 - esy1;
|
const boundsCurrentHeight = esy2 - esy1;
|
||||||
|
|
||||||
@@ -441,6 +440,9 @@ export const resizeSingleElement = (
|
|||||||
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
||||||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||||
|
|
||||||
|
let boundTextFont: { fontSize?: number; baseline?: number } = {};
|
||||||
|
const boundTextElement = getBoundTextElement(element);
|
||||||
|
|
||||||
if (transformHandleDirection.includes("e")) {
|
if (transformHandleDirection.includes("e")) {
|
||||||
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
|
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
|
||||||
}
|
}
|
||||||
@@ -453,6 +455,7 @@ export const resizeSingleElement = (
|
|||||||
if (transformHandleDirection.includes("n")) {
|
if (transformHandleDirection.includes("n")) {
|
||||||
scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight;
|
scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linear elements dimensions differ from bounds dimensions
|
// Linear elements dimensions differ from bounds dimensions
|
||||||
const eleInitialWidth = stateAtResizeStart.width;
|
const eleInitialWidth = stateAtResizeStart.width;
|
||||||
const eleInitialHeight = stateAtResizeStart.height;
|
const eleInitialHeight = stateAtResizeStart.height;
|
||||||
@@ -482,6 +485,37 @@ export const resizeSingleElement = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (boundTextElement) {
|
||||||
|
const stateOfBoundTextElementAtResize = originalElements.get(
|
||||||
|
boundTextElement.id,
|
||||||
|
) as typeof boundTextElement | undefined;
|
||||||
|
if (stateOfBoundTextElementAtResize) {
|
||||||
|
boundTextFont = {
|
||||||
|
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
||||||
|
baseline: stateOfBoundTextElementAtResize.baseline,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (shouldMaintainAspectRatio) {
|
||||||
|
const nextFont = measureFontSizeFromWH(
|
||||||
|
boundTextElement,
|
||||||
|
eleNewWidth - BOUND_TEXT_PADDING * 2,
|
||||||
|
eleNewHeight - BOUND_TEXT_PADDING * 2,
|
||||||
|
);
|
||||||
|
if (nextFont === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boundTextFont = {
|
||||||
|
fontSize: nextFont.size,
|
||||||
|
baseline: nextFont.baseline,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
|
||||||
|
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
|
||||||
|
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
||||||
|
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
|
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
|
||||||
getResizedElementAbsoluteCoords(
|
getResizedElementAbsoluteCoords(
|
||||||
stateAtResizeStart,
|
stateAtResizeStart,
|
||||||
@@ -491,11 +525,6 @@ export const resizeSingleElement = (
|
|||||||
const newBoundsWidth = newBoundsX2 - newBoundsX1;
|
const newBoundsWidth = newBoundsX2 - newBoundsX1;
|
||||||
const newBoundsHeight = newBoundsY2 - newBoundsY1;
|
const newBoundsHeight = newBoundsY2 - newBoundsY1;
|
||||||
|
|
||||||
// don't allow resize to negative dimensions when text is bounded to container
|
|
||||||
if ((newBoundsWidth < 0 || newBoundsHeight < 0) && boundTextElementId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate new topLeft based on fixed corner during resize
|
// Calculate new topLeft based on fixed corner during resize
|
||||||
let newTopLeft = [...startTopLeft] as [number, number];
|
let newTopLeft = [...startTopLeft] as [number, number];
|
||||||
if (["n", "w", "nw"].includes(transformHandleDirection)) {
|
if (["n", "w", "nw"].includes(transformHandleDirection)) {
|
||||||
@@ -588,13 +617,9 @@ export const resizeSingleElement = (
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let minWidth = 0;
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
|
||||||
if (boundTextElement) {
|
|
||||||
minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
resizedElement.width >= minWidth &&
|
resizedElement.width !== 0 &&
|
||||||
resizedElement.height !== 0 &&
|
resizedElement.height !== 0 &&
|
||||||
Number.isFinite(resizedElement.x) &&
|
Number.isFinite(resizedElement.x) &&
|
||||||
Number.isFinite(resizedElement.y)
|
Number.isFinite(resizedElement.y)
|
||||||
@@ -603,7 +628,10 @@ export const resizeSingleElement = (
|
|||||||
newSize: { width: resizedElement.width, height: resizedElement.height },
|
newSize: { width: resizedElement.width, height: resizedElement.height },
|
||||||
});
|
});
|
||||||
mutateElement(element, resizedElement);
|
mutateElement(element, resizedElement);
|
||||||
handleBindTextResize([element], transformHandleDirection);
|
if (boundTextElement && boundTextFont) {
|
||||||
|
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
|
||||||
|
}
|
||||||
|
handleBindTextResize(element, transformHandleDirection);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -674,7 +702,25 @@ const resizeMultipleElements = (
|
|||||||
}
|
}
|
||||||
const width = element.width * scale;
|
const width = element.width * scale;
|
||||||
const height = element.height * scale;
|
const height = element.height * scale;
|
||||||
|
const boundTextElement = getBoundTextElement(element);
|
||||||
let font: { fontSize?: number; baseline?: number } = {};
|
let font: { fontSize?: number; baseline?: number } = {};
|
||||||
|
|
||||||
|
if (boundTextElement) {
|
||||||
|
const nextFont = measureFontSizeFromWH(
|
||||||
|
boundTextElement,
|
||||||
|
width - BOUND_TEXT_PADDING * 2,
|
||||||
|
height - BOUND_TEXT_PADDING * 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextFont === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
font = {
|
||||||
|
fontSize: nextFont.size,
|
||||||
|
baseline: nextFont.baseline,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
const nextFont = measureFontSizeFromWH(element, width, height);
|
const nextFont = measureFontSizeFromWH(element, width, height);
|
||||||
if (nextFont === null) {
|
if (nextFont === null) {
|
||||||
@@ -718,6 +764,15 @@ const resizeMultipleElements = (
|
|||||||
if (updates) {
|
if (updates) {
|
||||||
elements.forEach((element, index) => {
|
elements.forEach((element, index) => {
|
||||||
mutateElement(element, updates[index]);
|
mutateElement(element, updates[index]);
|
||||||
|
const boundTextElement = getBoundTextElement(element);
|
||||||
|
|
||||||
|
if (boundTextElement) {
|
||||||
|
mutateElement(boundTextElement, {
|
||||||
|
fontSize: updates[index].fontSize,
|
||||||
|
baseline: updates[index].baseline,
|
||||||
|
});
|
||||||
|
handleBindTextResize(element, transformHandleType);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ export const showSelectedShapeActions = (
|
|||||||
!appState.viewModeEnabled &&
|
!appState.viewModeEnabled &&
|
||||||
(appState.editingElement ||
|
(appState.editingElement ||
|
||||||
getSelectedElements(elements, appState).length ||
|
getSelectedElements(elements, appState).length ||
|
||||||
appState.elementType !== "selection"),
|
(appState.elementType !== "selection" &&
|
||||||
|
appState.elementType !== "eraser")),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe("getPerfectElementSize", () => {
|
|||||||
expect(width).toEqual(135);
|
expect(width).toEqual(135);
|
||||||
expect(height).toEqual(135);
|
expect(height).toEqual(135);
|
||||||
});
|
});
|
||||||
it("should return height:0 and width:0 when width and heigh are 0", () => {
|
it("should return height:0 and width:0 when width and height are 0", () => {
|
||||||
const { height, width } = getPerfectElementSize("arrow", 0, 0);
|
const { height, width } = getPerfectElementSize("arrow", 0, 0);
|
||||||
expect(width).toEqual(0);
|
expect(width).toEqual(0);
|
||||||
expect(height).toEqual(0);
|
expect(height).toEqual(0);
|
||||||
|
|||||||
+107
-78
@@ -1,6 +1,5 @@
|
|||||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
||||||
import {
|
import {
|
||||||
ExcalidrawBindableElement,
|
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
@@ -8,10 +7,11 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { BOUND_TEXT_PADDING } from "../constants";
|
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
||||||
import { MaybeTransformHandleType } from "./transformHandles";
|
import { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { isTextElement } from ".";
|
||||||
|
|
||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (
|
||||||
element: ExcalidrawTextElement,
|
element: ExcalidrawTextElement,
|
||||||
@@ -39,11 +39,19 @@ export const redrawTextBoundingBox = (
|
|||||||
let coordY = element.y;
|
let coordY = element.y;
|
||||||
// Resize container and vertically center align the text
|
// Resize container and vertically center align the text
|
||||||
if (container) {
|
if (container) {
|
||||||
coordY = container.y + container.height / 2 - metrics.height / 2;
|
|
||||||
let nextHeight = container.height;
|
let nextHeight = container.height;
|
||||||
if (metrics.height > container.height - BOUND_TEXT_PADDING * 2) {
|
|
||||||
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
|
if (element.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||||
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
coordY = container.y + BOUND_TEXT_PADDING;
|
||||||
|
} else if (element.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
|
coordY =
|
||||||
|
container.y + container.height - metrics.height - BOUND_TEXT_PADDING;
|
||||||
|
} else {
|
||||||
|
coordY = container.y + container.height / 2 - metrics.height / 2;
|
||||||
|
if (metrics.height > container.height - BOUND_TEXT_PADDING * 2) {
|
||||||
|
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
|
||||||
|
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mutateElement(container, { height: nextHeight });
|
mutateElement(container, { height: nextHeight });
|
||||||
}
|
}
|
||||||
@@ -71,91 +79,99 @@ export const bindTextToShapeAfterDuplication = (
|
|||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElementId = getBoundTextElementId(element);
|
||||||
|
|
||||||
if (boundTextElementId) {
|
if (boundTextElementId) {
|
||||||
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId)!;
|
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId);
|
||||||
mutateElement(
|
if (newTextElementId) {
|
||||||
sceneElementMap.get(newElementId) as ExcalidrawBindableElement,
|
const newContainer = sceneElementMap.get(newElementId);
|
||||||
{
|
if (newContainer) {
|
||||||
boundElements: element.boundElements?.concat({
|
mutateElement(newContainer, {
|
||||||
type: "text",
|
boundElements: element.boundElements?.concat({
|
||||||
id: newTextElementId,
|
type: "text",
|
||||||
}),
|
id: newTextElementId,
|
||||||
},
|
}),
|
||||||
);
|
});
|
||||||
mutateElement(
|
}
|
||||||
sceneElementMap.get(newTextElementId) as ExcalidrawTextElement,
|
const newTextElement = sceneElementMap.get(newTextElementId);
|
||||||
{
|
if (newTextElement && isTextElement(newTextElement)) {
|
||||||
containerId: newElementId,
|
mutateElement(newTextElement, {
|
||||||
},
|
containerId: newContainer ? newElementId : null,
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleBindTextResize = (
|
export const handleBindTextResize = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
element: NonDeletedExcalidrawElement,
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
) => {
|
) => {
|
||||||
elements.forEach((element) => {
|
const boundTextElementId = getBoundTextElementId(element);
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
if (boundTextElementId) {
|
||||||
if (boundTextElementId) {
|
const textElement = Scene.getScene(element)!.getElement(
|
||||||
const textElement = Scene.getScene(element)!.getElement(
|
boundTextElementId,
|
||||||
boundTextElementId,
|
) as ExcalidrawTextElement;
|
||||||
) as ExcalidrawTextElement;
|
if (textElement && textElement.text) {
|
||||||
if (textElement && textElement.text) {
|
if (!element) {
|
||||||
if (!element) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
let text = textElement.text;
|
||||||
let text = textElement.text;
|
let nextHeight = textElement.height;
|
||||||
let nextHeight = textElement.height;
|
let containerHeight = element.height;
|
||||||
let containerHeight = element.height;
|
let nextBaseLine = textElement.baseline;
|
||||||
let nextBaseLine = textElement.baseline;
|
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
if (text) {
|
||||||
if (text) {
|
text = wrapText(
|
||||||
text = wrapText(
|
textElement.originalText,
|
||||||
textElement.originalText,
|
|
||||||
getFontString(textElement),
|
|
||||||
element.width,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dimensions = measureText(
|
|
||||||
text,
|
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
element.width,
|
element.width,
|
||||||
);
|
);
|
||||||
nextHeight = dimensions.height;
|
|
||||||
nextBaseLine = dimensions.baseline;
|
|
||||||
}
|
|
||||||
// increase height in case text element height exceeds
|
|
||||||
if (nextHeight > element.height - BOUND_TEXT_PADDING * 2) {
|
|
||||||
containerHeight = nextHeight + BOUND_TEXT_PADDING * 2;
|
|
||||||
const diff = containerHeight - element.height;
|
|
||||||
// fix the y coord when resizing from ne/nw/n
|
|
||||||
const updatedY =
|
|
||||||
transformHandleType === "ne" ||
|
|
||||||
transformHandleType === "nw" ||
|
|
||||||
transformHandleType === "n"
|
|
||||||
? element.y - diff
|
|
||||||
: element.y;
|
|
||||||
mutateElement(element, {
|
|
||||||
height: containerHeight,
|
|
||||||
y: updatedY,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedY = element.y + containerHeight / 2 - nextHeight / 2;
|
const dimensions = measureText(
|
||||||
mutateElement(textElement, {
|
|
||||||
text,
|
text,
|
||||||
// preserve padding and set width correctly
|
getFontString(textElement),
|
||||||
width: element.width - BOUND_TEXT_PADDING * 2,
|
element.width,
|
||||||
height: nextHeight,
|
);
|
||||||
x: element.x + BOUND_TEXT_PADDING,
|
nextHeight = dimensions.height;
|
||||||
|
nextBaseLine = dimensions.baseline;
|
||||||
|
}
|
||||||
|
// increase height in case text element height exceeds
|
||||||
|
if (nextHeight > element.height - BOUND_TEXT_PADDING * 2) {
|
||||||
|
containerHeight = nextHeight + BOUND_TEXT_PADDING * 2;
|
||||||
|
const diff = containerHeight - element.height;
|
||||||
|
// fix the y coord when resizing from ne/nw/n
|
||||||
|
const updatedY =
|
||||||
|
transformHandleType === "ne" ||
|
||||||
|
transformHandleType === "nw" ||
|
||||||
|
transformHandleType === "n"
|
||||||
|
? element.y - diff
|
||||||
|
: element.y;
|
||||||
|
mutateElement(element, {
|
||||||
|
height: containerHeight,
|
||||||
y: updatedY,
|
y: updatedY,
|
||||||
baseline: nextBaseLine,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let updatedY;
|
||||||
|
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||||
|
updatedY = element.y + BOUND_TEXT_PADDING;
|
||||||
|
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
|
updatedY = element.y + element.height - nextHeight - BOUND_TEXT_PADDING;
|
||||||
|
} else {
|
||||||
|
updatedY = element.y + element.height / 2 - nextHeight / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(textElement, {
|
||||||
|
text,
|
||||||
|
// preserve padding and set width correctly
|
||||||
|
width: element.width - BOUND_TEXT_PADDING * 2,
|
||||||
|
height: nextHeight,
|
||||||
|
x: element.x + BOUND_TEXT_PADDING,
|
||||||
|
y: updatedY,
|
||||||
|
baseline: nextBaseLine,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
||||||
@@ -355,6 +371,7 @@ export const charWidth = (() => {
|
|||||||
const width = getTextWidth(char, font);
|
const width = getTextWidth(char, font);
|
||||||
cachedCharWidth[font][ascii] = width;
|
cachedCharWidth[font][ascii] = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cachedCharWidth[font][ascii];
|
return cachedCharWidth[font][ascii];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -367,14 +384,14 @@ export const charWidth = (() => {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
export const getApproxMinLineWidth = (font: FontString) => {
|
export const getApproxMinLineWidth = (font: FontString) => {
|
||||||
const minCharWidth = getMinCharWidth(font);
|
const maxCharWidth = getMaxCharWidth(font);
|
||||||
if (minCharWidth === 0) {
|
if (maxCharWidth === 0) {
|
||||||
return (
|
return (
|
||||||
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
||||||
BOUND_TEXT_PADDING * 2
|
BOUND_TEXT_PADDING * 2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return minCharWidth + BOUND_TEXT_PADDING * 2;
|
return maxCharWidth + BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getApproxMinLineHeight = (font: FontString) => {
|
export const getApproxMinLineHeight = (font: FontString) => {
|
||||||
@@ -391,6 +408,15 @@ export const getMinCharWidth = (font: FontString) => {
|
|||||||
return Math.min(...cacheWithOutEmpty);
|
return Math.min(...cacheWithOutEmpty);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMaxCharWidth = (font: FontString) => {
|
||||||
|
const cache = charWidth.getCache(font);
|
||||||
|
if (!cache) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const cacheWithOutEmpty = cache.filter((val) => val !== undefined);
|
||||||
|
return Math.max(...cacheWithOutEmpty);
|
||||||
|
};
|
||||||
|
|
||||||
export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
||||||
// Generally lower case is used so converting to lower case
|
// Generally lower case is used so converting to lower case
|
||||||
const dummyText = DUMMY_TEXT.toLocaleLowerCase();
|
const dummyText = DUMMY_TEXT.toLocaleLowerCase();
|
||||||
@@ -416,7 +442,10 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
||||||
return container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id;
|
return container?.boundElements?.length
|
||||||
|
? container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id ||
|
||||||
|
null
|
||||||
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
|
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import * as textElementUtils from "./textElement";
|
import * as textElementUtils from "./textElement";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { mutateElement } from "./mutateElement";
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
@@ -19,7 +21,201 @@ const tab = " ";
|
|||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
describe("textWysiwyg", () => {
|
describe("textWysiwyg", () => {
|
||||||
describe("Test unbounded text", () => {
|
describe("start text editing", () => {
|
||||||
|
const { h } = window;
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
h.elements = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer editing selected text element (non-bindable container present)", async () => {
|
||||||
|
const line = API.createElement({
|
||||||
|
type: "line",
|
||||||
|
width: 100,
|
||||||
|
height: 0,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[100, 0],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const textSize = 20;
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: line.width / 2 - textSize / 2,
|
||||||
|
y: -textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
});
|
||||||
|
h.elements = [text, line];
|
||||||
|
|
||||||
|
API.setSelectedElements([text]);
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
|
expect(
|
||||||
|
(h.state.editingElement as ExcalidrawTextElement).containerId,
|
||||||
|
).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer editing selected text element (bindable container present)", async () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
width: 100,
|
||||||
|
boundElements: [],
|
||||||
|
});
|
||||||
|
const textSize = 20;
|
||||||
|
|
||||||
|
const boundText = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: container.width / 2 - textSize / 2,
|
||||||
|
y: container.height / 2 - textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const boundText2 = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: container.width / 2 - textSize / 2,
|
||||||
|
y: container.height / 2 - textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [container, boundText, boundText2];
|
||||||
|
|
||||||
|
mutateElement(container, {
|
||||||
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setSelectedElements([boundText2]);
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
expect(h.state.editingElement?.id).toBe(boundText2.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create bound text on ENTER if text exists at container center", () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
width: 100,
|
||||||
|
});
|
||||||
|
const textSize = 20;
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: container.width / 2 - textSize / 2,
|
||||||
|
y: container.height / 2 - textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [container, text];
|
||||||
|
|
||||||
|
API.setSelectedElements([container]);
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should edit existing bound text on ENTER even if higher z-index unbound text exists at container center", () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
width: 100,
|
||||||
|
boundElements: [],
|
||||||
|
});
|
||||||
|
const textSize = 20;
|
||||||
|
|
||||||
|
const boundText = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: container.width / 2 - textSize / 2,
|
||||||
|
y: container.height / 2 - textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const boundText2 = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: container.width / 2 - textSize / 2,
|
||||||
|
y: container.height / 2 - textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [container, boundText, boundText2];
|
||||||
|
|
||||||
|
mutateElement(container, {
|
||||||
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setSelectedElements([container]);
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
expect(h.state.editingElement?.id).toBe(boundText.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should edit text under cursor when clicked with text tool", () => {
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: 60,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [text];
|
||||||
|
UI.clickTool("text");
|
||||||
|
|
||||||
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
expect(editor).not.toBe(null);
|
||||||
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should edit text under cursor when double-clicked with selection tool", () => {
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: 60,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [text];
|
||||||
|
UI.clickTool("selection");
|
||||||
|
|
||||||
|
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
expect(editor).not.toBe(null);
|
||||||
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test container-unbound text", () => {
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
let textarea: HTMLTextAreaElement;
|
let textarea: HTMLTextAreaElement;
|
||||||
@@ -235,7 +431,7 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test bounded text", () => {
|
describe("Test container-bound text", () => {
|
||||||
let rectangle: any;
|
let rectangle: any;
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -315,6 +511,39 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shouldn't bind to non-text-bindable containers", async () => {
|
||||||
|
const line = API.createElement({
|
||||||
|
type: "line",
|
||||||
|
width: 100,
|
||||||
|
height: 0,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[100, 0],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
h.elements = [line];
|
||||||
|
|
||||||
|
UI.clickTool("text");
|
||||||
|
|
||||||
|
mouse.clickAt(line.x + line.width / 2, line.y + line.height / 2);
|
||||||
|
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
fireEvent.change(editor, {
|
||||||
|
target: {
|
||||||
|
value: "Hello World!",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||||
|
editor.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
expect(line.boundElements).toBe(null);
|
||||||
|
expect(h.elements[1].type).toBe("text");
|
||||||
|
expect((h.elements[1] as ExcalidrawTextElement).containerId).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
|
it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from "../utils";
|
} from "../utils";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isBoundToContainer, isTextElement } from "./typeChecks";
|
import { isBoundToContainer, isTextElement } from "./typeChecks";
|
||||||
import { CLASSES, BOUND_TEXT_PADDING } from "../constants";
|
import { CLASSES, BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -105,6 +105,8 @@ export const textWysiwyg = ({
|
|||||||
const updatedElement = Scene.getScene(element)?.getElement(
|
const updatedElement = Scene.getScene(element)?.getElement(
|
||||||
id,
|
id,
|
||||||
) as ExcalidrawTextElement;
|
) as ExcalidrawTextElement;
|
||||||
|
const { textAlign, verticalAlign } = updatedElement;
|
||||||
|
|
||||||
const approxLineHeight = getApproxLineHeight(getFontString(updatedElement));
|
const approxLineHeight = getApproxLineHeight(getFontString(updatedElement));
|
||||||
if (updatedElement && isTextElement(updatedElement)) {
|
if (updatedElement && isTextElement(updatedElement)) {
|
||||||
let coordX = updatedElement.x;
|
let coordX = updatedElement.x;
|
||||||
@@ -114,7 +116,7 @@ export const textWysiwyg = ({
|
|||||||
|
|
||||||
let maxHeight = updatedElement.height;
|
let maxHeight = updatedElement.height;
|
||||||
let width = updatedElement.width;
|
let width = updatedElement.width;
|
||||||
// Set to element height by default since thats
|
// Set to element height by default since that's
|
||||||
// what is going to be used for unbounded text
|
// what is going to be used for unbounded text
|
||||||
let height = updatedElement.height;
|
let height = updatedElement.height;
|
||||||
if (container && updatedElement.containerId) {
|
if (container && updatedElement.containerId) {
|
||||||
@@ -140,7 +142,7 @@ export const textWysiwyg = ({
|
|||||||
maxHeight = container.height - BOUND_TEXT_PADDING * 2;
|
maxHeight = container.height - BOUND_TEXT_PADDING * 2;
|
||||||
width = maxWidth;
|
width = maxWidth;
|
||||||
// The coordinates of text box set a distance of
|
// The coordinates of text box set a distance of
|
||||||
// 30px to preserve padding
|
// 5px to preserve padding
|
||||||
coordX = container.x + BOUND_TEXT_PADDING;
|
coordX = container.x + BOUND_TEXT_PADDING;
|
||||||
// autogrow container height if text exceeds
|
// autogrow container height if text exceeds
|
||||||
if (height > maxHeight) {
|
if (height > maxHeight) {
|
||||||
@@ -160,12 +162,34 @@ export const textWysiwyg = ({
|
|||||||
// is reached
|
// is reached
|
||||||
else {
|
else {
|
||||||
// vertically center align the text
|
// vertically center align the text
|
||||||
coordY = container.y + container.height / 2 - height / 2;
|
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
||||||
|
coordY = container.y + container.height / 2 - height / 2;
|
||||||
|
}
|
||||||
|
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
|
coordY =
|
||||||
|
container.y + container.height - height - BOUND_TEXT_PADDING;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
||||||
const { textAlign } = updatedElement;
|
const initialSelectionStart = editable.selectionStart;
|
||||||
|
const initialSelectionEnd = editable.selectionEnd;
|
||||||
|
const initialLength = editable.value.length;
|
||||||
editable.value = updatedElement.originalText;
|
editable.value = updatedElement.originalText;
|
||||||
|
|
||||||
|
// restore cursor position after value updated so it doesn't
|
||||||
|
// go to the end of text when container auto expanded
|
||||||
|
if (
|
||||||
|
initialSelectionStart === initialSelectionEnd &&
|
||||||
|
initialSelectionEnd !== initialLength
|
||||||
|
) {
|
||||||
|
// get diff between length and selection end and shift
|
||||||
|
// the cursor by "diff" times to position correctly
|
||||||
|
const diff = initialLength - initialSelectionEnd;
|
||||||
|
editable.selectionStart = editable.value.length - diff;
|
||||||
|
editable.selectionEnd = editable.value.length - diff;
|
||||||
|
}
|
||||||
|
|
||||||
const lines = updatedElement.originalText.split("\n");
|
const lines = updatedElement.originalText.split("\n");
|
||||||
const lineHeight = updatedElement.containerId
|
const lineHeight = updatedElement.containerId
|
||||||
? approxLineHeight
|
? approxLineHeight
|
||||||
@@ -195,6 +219,7 @@ export const textWysiwyg = ({
|
|||||||
editorMaxHeight,
|
editorMaxHeight,
|
||||||
),
|
),
|
||||||
textAlign,
|
textAlign,
|
||||||
|
verticalAlign,
|
||||||
color: updatedElement.strokeColor,
|
color: updatedElement.strokeColor,
|
||||||
opacity: updatedElement.opacity / 100,
|
opacity: updatedElement.opacity / 100,
|
||||||
filter: "var(--theme-filter)",
|
filter: "var(--theme-filter)",
|
||||||
@@ -258,7 +283,7 @@ export const textWysiwyg = ({
|
|||||||
// as that gets updated below
|
// as that gets updated below
|
||||||
const lines = editable.scrollHeight / getApproxLineHeight(font);
|
const lines = editable.scrollHeight / getApproxLineHeight(font);
|
||||||
// auto increase height only when lines > 1 so its
|
// auto increase height only when lines > 1 so its
|
||||||
// measured correctly and vertically alignes for
|
// measured correctly and vertically aligns for
|
||||||
// first line as well as setting height to "auto"
|
// first line as well as setting height to "auto"
|
||||||
// doubles the height as soon as user starts typing
|
// doubles the height as soon as user starts typing
|
||||||
if (isBoundToContainer(element) && lines > 1) {
|
if (isBoundToContainer(element) && lines > 1) {
|
||||||
@@ -397,7 +422,7 @@ export const textWysiwyg = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns indeces of start positions of selected lines, in reverse order
|
* @returns indices of start positions of selected lines, in reverse order
|
||||||
*/
|
*/
|
||||||
const getSelectedLinesStartIndices = () => {
|
const getSelectedLinesStartIndices = () => {
|
||||||
let { selectionStart, selectionEnd, value } = editable;
|
let { selectionStart, selectionEnd, value } = editable;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AppState } from "../types";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
|
ExcalidrawTextContainer,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const isGenericElement = (
|
export const isGenericElement = (
|
||||||
@@ -59,7 +61,7 @@ export const isLinearElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isLinearElementType = (
|
export const isLinearElementType = (
|
||||||
elementType: ExcalidrawElement["type"],
|
elementType: AppState["elementType"],
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return (
|
return (
|
||||||
elementType === "arrow" || elementType === "line" // || elementType === "freedraw"
|
elementType === "arrow" || elementType === "line" // || elementType === "freedraw"
|
||||||
@@ -73,7 +75,7 @@ export const isBindingElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isBindingElementType = (
|
export const isBindingElementType = (
|
||||||
elementType: ExcalidrawElement["type"],
|
elementType: AppState["elementType"],
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return elementType === "arrow";
|
return elementType === "arrow";
|
||||||
};
|
};
|
||||||
@@ -91,7 +93,9 @@ export const isBindableElement = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isTextBindableContainer = (element: ExcalidrawElement | null) => {
|
export const isTextBindableContainer = (
|
||||||
|
element: ExcalidrawElement | null,
|
||||||
|
): element is ExcalidrawTextContainer => {
|
||||||
return (
|
return (
|
||||||
element != null &&
|
element != null &&
|
||||||
(element.type === "rectangle" ||
|
(element.type === "rectangle" ||
|
||||||
|
|||||||
+11
-3
@@ -1,5 +1,5 @@
|
|||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import { FONT_FAMILY, THEME } from "../constants";
|
import { FONT_FAMILY, THEME, VERTICAL_ALIGN } from "../constants";
|
||||||
|
|
||||||
export type ChartType = "bar" | "line";
|
export type ChartType = "bar" | "line";
|
||||||
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||||
@@ -12,7 +12,9 @@ export type PointerType = "mouse" | "pen" | "touch";
|
|||||||
export type StrokeSharpness = "round" | "sharp";
|
export type StrokeSharpness = "round" | "sharp";
|
||||||
export type StrokeStyle = "solid" | "dashed" | "dotted";
|
export type StrokeStyle = "solid" | "dashed" | "dotted";
|
||||||
export type TextAlign = "left" | "center" | "right";
|
export type TextAlign = "left" | "center" | "right";
|
||||||
export type VerticalAlign = "top" | "middle";
|
|
||||||
|
type VerticalAlignKeys = keyof typeof VERTICAL_ALIGN;
|
||||||
|
export type VerticalAlign = typeof VERTICAL_ALIGN[VerticalAlignKeys];
|
||||||
|
|
||||||
type _ExcalidrawElementBase = Readonly<{
|
type _ExcalidrawElementBase = Readonly<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -133,8 +135,14 @@ export type ExcalidrawBindableElement =
|
|||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawImageElement;
|
| ExcalidrawImageElement;
|
||||||
|
|
||||||
|
export type ExcalidrawTextContainer =
|
||||||
|
| ExcalidrawRectangleElement
|
||||||
|
| ExcalidrawDiamondElement
|
||||||
|
| ExcalidrawEllipseElement
|
||||||
|
| ExcalidrawImageElement;
|
||||||
|
|
||||||
export type ExcalidrawTextElementWithContainer = {
|
export type ExcalidrawTextElementWithContainer = {
|
||||||
containerId: ExcalidrawGenericElement["id"];
|
containerId: ExcalidrawTextContainer["id"];
|
||||||
} & ExcalidrawTextElement;
|
} & ExcalidrawTextElement;
|
||||||
|
|
||||||
export type PointBinding = {
|
export type PointBinding = {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const FILE_UPLOAD_TIMEOUT = 300;
|
|||||||
export const LOAD_IMAGES_TIMEOUT = 500;
|
export const LOAD_IMAGES_TIMEOUT = 500;
|
||||||
export const SYNC_FULL_SCENE_INTERVAL_MS = 20000;
|
export const SYNC_FULL_SCENE_INTERVAL_MS = 20000;
|
||||||
export const SYNC_BROWSER_TABS_TIMEOUT = 50;
|
export const SYNC_BROWSER_TABS_TIMEOUT = 50;
|
||||||
|
export const CURSOR_SYNC_TIMEOUT = 33; // ~30fps
|
||||||
|
|
||||||
export const FILE_UPLOAD_MAX_BYTES = 3 * 1024 * 1024; // 3 MiB
|
export const FILE_UPLOAD_MAX_BYTES = 3 * 1024 * 1024; // 3 MiB
|
||||||
// 1 year (https://stackoverflow.com/a/25201898/927631)
|
// 1 year (https://stackoverflow.com/a/25201898/927631)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
withBatchedUpdates,
|
withBatchedUpdates,
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import {
|
import {
|
||||||
|
CURSOR_SYNC_TIMEOUT,
|
||||||
FILE_UPLOAD_MAX_BYTES,
|
FILE_UPLOAD_MAX_BYTES,
|
||||||
FIREBASE_STORAGE_PREFIXES,
|
FIREBASE_STORAGE_PREFIXES,
|
||||||
INITIAL_SCENE_UPDATE_TIMEOUT,
|
INITIAL_SCENE_UPDATE_TIMEOUT,
|
||||||
@@ -27,8 +28,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
generateCollaborationLinkData,
|
generateCollaborationLinkData,
|
||||||
getCollaborationLink,
|
getCollaborationLink,
|
||||||
|
getCollabServer,
|
||||||
SocketUpdateDataSource,
|
SocketUpdateDataSource,
|
||||||
SOCKET_SERVER,
|
|
||||||
} from "../data";
|
} from "../data";
|
||||||
import {
|
import {
|
||||||
isSavedToFirebase,
|
isSavedToFirebase,
|
||||||
@@ -353,32 +354,29 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
this.isCollaborating = true;
|
this.isCollaborating = true;
|
||||||
|
|
||||||
const { default: socketIOClient }: any = await import(
|
const { default: socketIOClient } = await import(
|
||||||
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
||||||
);
|
);
|
||||||
|
|
||||||
this.portal.open(socketIOClient(SOCKET_SERVER), roomId, roomKey);
|
try {
|
||||||
|
const socketServerData = await getCollabServer();
|
||||||
|
|
||||||
if (existingRoomLinkData) {
|
this.portal.socket = this.portal.open(
|
||||||
this.excalidrawAPI.resetScene();
|
socketIOClient(socketServerData.url, {
|
||||||
|
transports: socketServerData.polling
|
||||||
|
? ["websocket", "polling"]
|
||||||
|
: ["websocket"],
|
||||||
|
}),
|
||||||
|
roomId,
|
||||||
|
roomKey,
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
this.setState({ errorMessage: error.message });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
if (!existingRoomLinkData) {
|
||||||
const elements = await loadFromFirebase(
|
|
||||||
roomId,
|
|
||||||
roomKey,
|
|
||||||
this.portal.socket,
|
|
||||||
);
|
|
||||||
if (elements) {
|
|
||||||
scenePromise.resolve({
|
|
||||||
elements,
|
|
||||||
scrollToContent: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
// log the error and move on. other peers will sync us the scene.
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const elements = this.excalidrawAPI.getSceneElements().map((element) => {
|
const elements = this.excalidrawAPI.getSceneElements().map((element) => {
|
||||||
if (isImageElement(element) && element.status === "saved") {
|
if (isImageElement(element) && element.status === "saved") {
|
||||||
return newElementWith(element, { status: "pending" });
|
return newElementWith(element, { status: "pending" });
|
||||||
@@ -402,14 +400,17 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fallback in case you're not alone in the room but still don't receive
|
// fallback in case you're not alone in the room but still don't receive
|
||||||
// initial SCENE_UPDATE message
|
// initial SCENE_INIT message
|
||||||
this.socketInitializationTimer = window.setTimeout(() => {
|
this.socketInitializationTimer = window.setTimeout(() => {
|
||||||
this.initializeSocket();
|
this.initializeRoom({
|
||||||
|
roomLinkData: existingRoomLinkData,
|
||||||
|
fetchScene: true,
|
||||||
|
});
|
||||||
scenePromise.resolve(null);
|
scenePromise.resolve(null);
|
||||||
}, INITIAL_SCENE_UPDATE_TIMEOUT);
|
}, INITIAL_SCENE_UPDATE_TIMEOUT);
|
||||||
|
|
||||||
// All socket listeners are moving to Portal
|
// All socket listeners are moving to Portal
|
||||||
this.portal.socket!.on(
|
this.portal.socket.on(
|
||||||
"client-broadcast",
|
"client-broadcast",
|
||||||
async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
|
async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
|
||||||
if (!this.portal.roomKey) {
|
if (!this.portal.roomKey) {
|
||||||
@@ -427,7 +428,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
return;
|
return;
|
||||||
case SCENE.INIT: {
|
case SCENE.INIT: {
|
||||||
if (!this.portal.socketInitialized) {
|
if (!this.portal.socketInitialized) {
|
||||||
this.initializeSocket();
|
this.initializeRoom({ fetchScene: false });
|
||||||
const remoteElements = decryptedData.payload.elements;
|
const remoteElements = decryptedData.payload.elements;
|
||||||
const reconciledElements = this.reconcileElements(remoteElements);
|
const reconciledElements = this.reconcileElements(remoteElements);
|
||||||
this.handleRemoteSceneUpdate(reconciledElements, {
|
this.handleRemoteSceneUpdate(reconciledElements, {
|
||||||
@@ -481,12 +482,15 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.portal.socket!.on("first-in-room", () => {
|
this.portal.socket.on("first-in-room", async () => {
|
||||||
if (this.portal.socket) {
|
if (this.portal.socket) {
|
||||||
this.portal.socket.off("first-in-room");
|
this.portal.socket.off("first-in-room");
|
||||||
}
|
}
|
||||||
this.initializeSocket();
|
const sceneData = await this.initializeRoom({
|
||||||
scenePromise.resolve(null);
|
fetchScene: true,
|
||||||
|
roomLinkData: existingRoomLinkData,
|
||||||
|
});
|
||||||
|
scenePromise.resolve(sceneData);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.initializeIdleDetector();
|
this.initializeIdleDetector();
|
||||||
@@ -498,9 +502,45 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
return scenePromise;
|
return scenePromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
private initializeSocket = () => {
|
private initializeRoom = async ({
|
||||||
this.portal.socketInitialized = true;
|
fetchScene,
|
||||||
|
roomLinkData,
|
||||||
|
}:
|
||||||
|
| {
|
||||||
|
fetchScene: true;
|
||||||
|
roomLinkData: { roomId: string; roomKey: string } | null;
|
||||||
|
}
|
||||||
|
| { fetchScene: false; roomLinkData?: null }) => {
|
||||||
clearTimeout(this.socketInitializationTimer!);
|
clearTimeout(this.socketInitializationTimer!);
|
||||||
|
if (fetchScene && roomLinkData && this.portal.socket) {
|
||||||
|
this.excalidrawAPI.resetScene();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const elements = await loadFromFirebase(
|
||||||
|
roomLinkData.roomId,
|
||||||
|
roomLinkData.roomKey,
|
||||||
|
this.portal.socket,
|
||||||
|
);
|
||||||
|
if (elements) {
|
||||||
|
this.setLastBroadcastedOrReceivedSceneVersion(
|
||||||
|
getSceneVersion(elements),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements,
|
||||||
|
scrollToContent: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// log the error and move on. other peers will sync us the scene.
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
this.portal.socketInitialized = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.portal.socketInitialized = true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
private reconcileElements = (
|
private reconcileElements = (
|
||||||
@@ -564,7 +604,9 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
window.clearTimeout(this.idleTimeoutId);
|
window.clearTimeout(this.idleTimeoutId);
|
||||||
this.idleTimeoutId = null;
|
this.idleTimeoutId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD);
|
this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD);
|
||||||
|
|
||||||
if (!this.activeIntervalId) {
|
if (!this.activeIntervalId) {
|
||||||
this.activeIntervalId = window.setInterval(
|
this.activeIntervalId = window.setInterval(
|
||||||
this.reportActive,
|
this.reportActive,
|
||||||
@@ -639,15 +681,18 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
return this.excalidrawAPI.getSceneElementsIncludingDeleted();
|
return this.excalidrawAPI.getSceneElementsIncludingDeleted();
|
||||||
};
|
};
|
||||||
|
|
||||||
onPointerUpdate = (payload: {
|
onPointerUpdate = throttle(
|
||||||
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
|
(payload: {
|
||||||
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
|
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
|
||||||
pointersMap: Gesture["pointers"];
|
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
|
||||||
}) => {
|
pointersMap: Gesture["pointers"];
|
||||||
payload.pointersMap.size < 2 &&
|
}) => {
|
||||||
this.portal.socket &&
|
payload.pointersMap.size < 2 &&
|
||||||
this.portal.broadcastMouseLocation(payload);
|
this.portal.socket &&
|
||||||
};
|
this.portal.broadcastMouseLocation(payload);
|
||||||
|
},
|
||||||
|
CURSOR_SYNC_TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
onIdleStateChange = (userState: UserIdleState) => {
|
onIdleStateChange = (userState: UserIdleState) => {
|
||||||
this.setState({ userState });
|
this.setState({ userState });
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ class Portal {
|
|||||||
this.socket.on("room-user-change", (clients: string[]) => {
|
this.socket.on("room-user-change", (clients: string[]) => {
|
||||||
this.collab.setCollaborators(clients);
|
this.collab.setCollaborators(clients);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
|||||||
@@ -30,7 +30,34 @@ const generateRoomId = async () => {
|
|||||||
return bytesToHexString(buffer);
|
return bytesToHexString(buffer);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SOCKET_SERVER = process.env.REACT_APP_SOCKET_SERVER_URL;
|
/**
|
||||||
|
* Right now the reason why we resolve connection params (url, polling...)
|
||||||
|
* from upstream is to allow changing the params immediately when needed without
|
||||||
|
* having to wait for clients to update the SW.
|
||||||
|
*
|
||||||
|
* If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks)
|
||||||
|
*/
|
||||||
|
export const getCollabServer = async (): Promise<{
|
||||||
|
url: string;
|
||||||
|
polling: boolean;
|
||||||
|
}> => {
|
||||||
|
if (process.env.REACT_APP_WS_SERVER_URL) {
|
||||||
|
return {
|
||||||
|
url: process.env.REACT_APP_WS_SERVER_URL,
|
||||||
|
polling: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(
|
||||||
|
`${process.env.REACT_APP_PORTAL_URL}/collab-server`,
|
||||||
|
);
|
||||||
|
return await resp.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(t("errors.cannotResolveCollabServer"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export type EncryptedData = {
|
export type EncryptedData = {
|
||||||
data: ArrayBuffer;
|
data: ArrayBuffer;
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
.excalidraw {
|
.excalidraw {
|
||||||
|
--color-primary-contrast-offset: #625ee0; // to offset Chubb illusion
|
||||||
|
|
||||||
|
&.theme--dark {
|
||||||
|
--color-primary-contrast-offset: #726dff; // to offset Chubb illusion
|
||||||
|
}
|
||||||
.layer-ui__wrapper__footer-center {
|
.layer-ui__wrapper__footer-center {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const NVECTOR_BASE = ["1", "e0", "e1", "e2", "e01", "e20", "e12", "e012"];
|
|||||||
export const nvector = (value: number = 0, index: number = 0): NVector => {
|
export const nvector = (value: number = 0, index: number = 0): NVector => {
|
||||||
const result = [0, 0, 0, 0, 0, 0, 0, 0];
|
const result = [0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
if (index < 0 || index > 7) {
|
if (index < 0 || index > 7) {
|
||||||
throw new Error(`Expected \`index\` betwen 0 and 7, got \`${index}\``);
|
throw new Error(`Expected \`index\` between 0 and 7, got \`${index}\``);
|
||||||
}
|
}
|
||||||
if (value !== 0) {
|
if (value !== 0) {
|
||||||
result[index] = value;
|
result[index] = value;
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import { Line, Point } from "./ga";
|
|||||||
*
|
*
|
||||||
* This maps to a standard formula `a * x + b * y + c`.
|
* This maps to a standard formula `a * x + b * y + c`.
|
||||||
*
|
*
|
||||||
* `(-b, a)` correponds to a 2D vector parallel to the line. The lines
|
* `(-b, a)` corresponds to a 2D vector parallel to the line. The lines
|
||||||
* have a natural orientation, corresponding to that vector.
|
* have a natural orientation, corresponding to that vector.
|
||||||
*
|
*
|
||||||
* The magnitude ("norm") of the line is `sqrt(a ^ 2 + b ^ 2)`.
|
* The magnitude ("norm") of the line is `sqrt(a ^ 2 + b ^ 2)`.
|
||||||
|
|||||||
Vendored
+5
-1
@@ -21,7 +21,7 @@ declare namespace NodeJS {
|
|||||||
interface ProcessEnv {
|
interface ProcessEnv {
|
||||||
readonly REACT_APP_BACKEND_V2_GET_URL: string;
|
readonly REACT_APP_BACKEND_V2_GET_URL: string;
|
||||||
readonly REACT_APP_BACKEND_V2_POST_URL: string;
|
readonly REACT_APP_BACKEND_V2_POST_URL: string;
|
||||||
readonly REACT_APP_SOCKET_SERVER_URL: string;
|
readonly REACT_APP_PORTAL_URL: string;
|
||||||
readonly REACT_APP_FIREBASE_CONFIG: string;
|
readonly REACT_APP_FIREBASE_CONFIG: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,10 @@ type Mutable<T> = {
|
|||||||
-readonly [P in keyof T]: T[P];
|
-readonly [P in keyof T]: T[P];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** utility type to assert that the second type is a subtype of the first type.
|
||||||
|
* Returns the subtype. */
|
||||||
|
type SubtypeOf<Supertype, Subtype extends Supertype> = Subtype;
|
||||||
|
|
||||||
type ResolutionType<T extends (...args: any) => any> = T extends (
|
type ResolutionType<T extends (...args: any) => any> = T extends (
|
||||||
...args: any
|
...args: any
|
||||||
) => Promise<infer R>
|
) => Promise<infer R>
|
||||||
|
|||||||
+6
-15
@@ -1,13 +1,7 @@
|
|||||||
import {
|
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
|
||||||
GroupId,
|
|
||||||
ExcalidrawElement,
|
|
||||||
NonDeleted,
|
|
||||||
ExcalidrawTextElementWithContainer,
|
|
||||||
} from "./element/types";
|
|
||||||
import { AppState } from "./types";
|
import { AppState } from "./types";
|
||||||
import { getSelectedElements } from "./scene";
|
import { getSelectedElements } from "./scene";
|
||||||
import { getBoundTextElementId } from "./element/textElement";
|
import { getBoundTextElement } from "./element/textElement";
|
||||||
import Scene from "./scene/Scene";
|
|
||||||
|
|
||||||
export const selectGroup = (
|
export const selectGroup = (
|
||||||
groupId: GroupId,
|
groupId: GroupId,
|
||||||
@@ -182,13 +176,10 @@ export const getMaximumGroups = (
|
|||||||
|
|
||||||
const currentGroupMembers = groups.get(groupId) || [];
|
const currentGroupMembers = groups.get(groupId) || [];
|
||||||
|
|
||||||
// Include bounded text if present when grouping
|
// Include bound text if present when grouping
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElementId) {
|
if (boundTextElement) {
|
||||||
const textElement = Scene.getScene(element)!.getElement(
|
currentGroupMembers.push(boundTextElement);
|
||||||
boundTextElementId,
|
|
||||||
) as ExcalidrawTextElementWithContainer;
|
|
||||||
currentGroupMembers.push(textElement);
|
|
||||||
}
|
}
|
||||||
groups.set(groupId, [...currentGroupMembers, element]);
|
groups.set(groupId, [...currentGroupMembers, element]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const KEYS = {
|
|||||||
COMMA: ",",
|
COMMA: ",",
|
||||||
|
|
||||||
A: "a",
|
A: "a",
|
||||||
|
C: "c",
|
||||||
D: "d",
|
D: "d",
|
||||||
E: "e",
|
E: "e",
|
||||||
G: "g",
|
G: "g",
|
||||||
|
|||||||
+29
-27
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "كرتوني",
|
"cartoonist": "كرتوني",
|
||||||
"fileTitle": "إسم الملف",
|
"fileTitle": "إسم الملف",
|
||||||
"colorPicker": "منتقي اللون",
|
"colorPicker": "منتقي اللون",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "خلفية اللوحة",
|
"canvasBackground": "خلفية اللوحة",
|
||||||
"drawingCanvas": "لوحة الرسم",
|
"drawingCanvas": "لوحة الرسم",
|
||||||
"layers": "الطبقات",
|
"layers": "الطبقات",
|
||||||
@@ -107,9 +108,9 @@
|
|||||||
"increaseFontSize": "تكبير حجم الخط",
|
"increaseFontSize": "تكبير حجم الخط",
|
||||||
"unbindText": "",
|
"unbindText": "",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "تعديل الرابط",
|
||||||
"create": "",
|
"create": "إنشاء رابط",
|
||||||
"label": ""
|
"label": "رابط"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
@@ -167,7 +168,7 @@
|
|||||||
"errorAddingToLibrary": "تعذر إضافة العنصر للمكتبة",
|
"errorAddingToLibrary": "تعذر إضافة العنصر للمكتبة",
|
||||||
"errorRemovingFromLibrary": "تعذر إزالة العنصر من المكتبة",
|
"errorRemovingFromLibrary": "تعذر إزالة العنصر من المكتبة",
|
||||||
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
|
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
|
||||||
"imageDoesNotContainScene": "",
|
"imageDoesNotContainScene": "يبدو أن هذه الصورة لا تحتوي على أي بيانات مشهد. هل قمت بتمكين تضمين المشهد أثناء التصدير؟",
|
||||||
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
|
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
|
||||||
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
|
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
|
||||||
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "تعذر إدراج الصورة. حاول مرة أخرى لاحقاً...",
|
"imageInsertError": "تعذر إدراج الصورة. حاول مرة أخرى لاحقاً...",
|
||||||
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
|
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
|
||||||
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
|
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
|
||||||
"invalidSVGString": "SVG غير صالح."
|
"invalidSVGString": "SVG غير صالح.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "تحديد",
|
"selection": "تحديد",
|
||||||
@@ -217,7 +219,7 @@
|
|||||||
"lineEditor_pointSelected": "",
|
"lineEditor_pointSelected": "",
|
||||||
"lineEditor_nothingSelected": "",
|
"lineEditor_nothingSelected": "",
|
||||||
"placeImage": "",
|
"placeImage": "",
|
||||||
"publishLibrary": "",
|
"publishLibrary": "نشر مكتبتك",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": ""
|
"deepBoxSelect": ""
|
||||||
},
|
},
|
||||||
@@ -289,46 +291,46 @@
|
|||||||
"zoomToSelection": "تكبير للعنصر المحدد"
|
"zoomToSelection": "تكبير للعنصر المحدد"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": ""
|
"title": "مسح اللوحة"
|
||||||
},
|
},
|
||||||
"publishDialog": {
|
"publishDialog": {
|
||||||
"title": "",
|
"title": "نشر المكتبة",
|
||||||
"itemName": "",
|
"itemName": "إسم العنصر",
|
||||||
"authorName": "",
|
"authorName": "إسم المؤلف",
|
||||||
"githubUsername": "",
|
"githubUsername": "اسم المستخدم في جيت هب",
|
||||||
"twitterUsername": "",
|
"twitterUsername": "اسم المستخدم في تويتر",
|
||||||
"libraryName": "",
|
"libraryName": "اسم المكتبة",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "وصف المكتبة",
|
||||||
"website": "",
|
"website": "الموقع",
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"authorName": "",
|
"authorName": "اسمك أو اسم المستخدم",
|
||||||
"libraryName": "",
|
"libraryName": "اسم مكتبتك",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "وصف مكتبتك لمساعدة الناس على فهم استخدامها",
|
||||||
"githubHandle": "",
|
"githubHandle": "",
|
||||||
"twitterHandle": "",
|
"twitterHandle": "",
|
||||||
"website": ""
|
"website": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"required": "مطلوب",
|
"required": "مطلوب",
|
||||||
"website": ""
|
"website": "أدخل عنوان URL صالح"
|
||||||
},
|
},
|
||||||
"noteDescription": {
|
"noteDescription": {
|
||||||
"pre": "",
|
"pre": "",
|
||||||
"link": "",
|
"link": "مستودع المكتبة العامة",
|
||||||
"post": ""
|
"post": "ليستخدمها الآخرون في رسوماتهم."
|
||||||
},
|
},
|
||||||
"noteGuidelines": {
|
"noteGuidelines": {
|
||||||
"pre": "",
|
"pre": "يجب الموافقة على المكتبة يدويًا أولاً. يرجى قراءة ",
|
||||||
"link": "",
|
"link": "الإرشادات",
|
||||||
"post": ""
|
"post": ""
|
||||||
},
|
},
|
||||||
"noteLicense": {
|
"noteLicense": {
|
||||||
"pre": "",
|
"pre": "",
|
||||||
"link": "",
|
"link": "رخصة إم أي تي ",
|
||||||
"post": ""
|
"post": "وهو ما يعني باختصار أنه يمكن لأي شخص استخدامها دون قيود."
|
||||||
},
|
},
|
||||||
"noteItems": "",
|
"noteItems": "يجب أن يكون لكل عنصر مكتبة اسمه الخاص حتى يكون قابلاً للتصفية. سيتم تضمين عناصر المكتبة التالية:",
|
||||||
"atleastOneLibItem": ""
|
"atleastOneLibItem": "يرجى تحديد عنصر مكتبة واحد على الأقل للبدء"
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "تم إرسال المكتبة",
|
"title": "تم إرسال المكتبة",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Карикатурист",
|
"cartoonist": "Карикатурист",
|
||||||
"fileTitle": "Име на файл",
|
"fileTitle": "Име на файл",
|
||||||
"colorPicker": "Избор на цвят",
|
"colorPicker": "Избор на цвят",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Фон на платно",
|
"canvasBackground": "Фон на платно",
|
||||||
"drawingCanvas": "Платно за рисуване",
|
"drawingCanvas": "Платно за рисуване",
|
||||||
"layers": "Слоеве",
|
"layers": "Слоеве",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Селекция",
|
"selection": "Селекция",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "",
|
"cartoonist": "",
|
||||||
"fileTitle": "",
|
"fileTitle": "",
|
||||||
"colorPicker": "",
|
"colorPicker": "",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "",
|
"canvasBackground": "",
|
||||||
"drawingCanvas": "",
|
"drawingCanvas": "",
|
||||||
"layers": "",
|
"layers": "",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "",
|
"selection": "",
|
||||||
|
|||||||
+17
-15
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Dibuixant",
|
"cartoonist": "Dibuixant",
|
||||||
"fileTitle": "Nom del fitxer",
|
"fileTitle": "Nom del fitxer",
|
||||||
"colorPicker": "Selector de colors",
|
"colorPicker": "Selector de colors",
|
||||||
|
"canvasColors": "Usat al llenç",
|
||||||
"canvasBackground": "Fons del llenç",
|
"canvasBackground": "Fons del llenç",
|
||||||
"drawingCanvas": "Llenç de dibuix",
|
"drawingCanvas": "Llenç de dibuix",
|
||||||
"layers": "Capes",
|
"layers": "Capes",
|
||||||
@@ -103,13 +104,13 @@
|
|||||||
"toggleTheme": "Activa o desactiva el tema",
|
"toggleTheme": "Activa o desactiva el tema",
|
||||||
"personalLib": "Biblioteca personal",
|
"personalLib": "Biblioteca personal",
|
||||||
"excalidrawLib": "Biblioteca d'Excalidraw",
|
"excalidrawLib": "Biblioteca d'Excalidraw",
|
||||||
"decreaseFontSize": "",
|
"decreaseFontSize": "Redueix la mida de la lletra",
|
||||||
"increaseFontSize": "",
|
"increaseFontSize": "Augmenta la mida de la lletra",
|
||||||
"unbindText": "",
|
"unbindText": "Desvincular el text",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "Edita l'enllaç",
|
||||||
"create": "",
|
"create": "Crea un enllaç",
|
||||||
"label": ""
|
"label": "Enllaç"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
@@ -167,7 +168,7 @@
|
|||||||
"errorAddingToLibrary": "No s'ha pogut afegir l'element a la biblioteca",
|
"errorAddingToLibrary": "No s'ha pogut afegir l'element a la biblioteca",
|
||||||
"errorRemovingFromLibrary": "No s'ha pogut eliminar l'element de la biblioteca",
|
"errorRemovingFromLibrary": "No s'ha pogut eliminar l'element de la biblioteca",
|
||||||
"confirmAddLibrary": "Això afegirà {{numShapes}} forma(es) a la vostra biblioteca. Estàs segur?",
|
"confirmAddLibrary": "Això afegirà {{numShapes}} forma(es) a la vostra biblioteca. Estàs segur?",
|
||||||
"imageDoesNotContainScene": "",
|
"imageDoesNotContainScene": "Aquesta imatge no sembla contenir cap dada d'escena. Heu activat l'incrustació de l'escena durant l'exportació?",
|
||||||
"cannotRestoreFromImage": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge",
|
"cannotRestoreFromImage": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge",
|
||||||
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
|
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
|
||||||
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
|
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "No s'ha pogut insertar la imatge, torneu-ho a provar més tard...",
|
"imageInsertError": "No s'ha pogut insertar la imatge, torneu-ho a provar més tard...",
|
||||||
"fileTooBig": "El fitxer és massa gros. La mida màxima permesa és {{maxSize}}.",
|
"fileTooBig": "El fitxer és massa gros. La mida màxima permesa és {{maxSize}}.",
|
||||||
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
|
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
|
||||||
"invalidSVGString": "SVG no vàlid."
|
"invalidSVGString": "SVG no vàlid.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selecció",
|
"selection": "Selecció",
|
||||||
@@ -193,8 +195,8 @@
|
|||||||
"text": "Text",
|
"text": "Text",
|
||||||
"library": "Biblioteca",
|
"library": "Biblioteca",
|
||||||
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
|
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
|
||||||
"penMode": "",
|
"penMode": "Evita el zoom i accepta solament el dibuix lliure amb bolígraf",
|
||||||
"link": ""
|
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Accions del llenç",
|
"canvasActions": "Accions del llenç",
|
||||||
@@ -214,12 +216,12 @@
|
|||||||
"resizeImage": "Podeu redimensionar lliurement prement MAJÚSCULA;\nper a redimensionar des del centre, premeu ALT",
|
"resizeImage": "Podeu redimensionar lliurement prement MAJÚSCULA;\nper a redimensionar des del centre, premeu ALT",
|
||||||
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
|
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
|
||||||
"lineEditor_info": "Fes doble clic o premi Enter per editar punts",
|
"lineEditor_info": "Fes doble clic o premi Enter per editar punts",
|
||||||
"lineEditor_pointSelected": "",
|
"lineEditor_pointSelected": "Premeu Suprimir per a eliminar el(s) punt(s), CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l",
|
||||||
"lineEditor_nothingSelected": "",
|
"lineEditor_nothingSelected": "Seleccioneu un punt per a editar-lo (premeu SHIFT si voleu\nselecció múltiple), o manteniu Alt i feu clic per a afegir més punts",
|
||||||
"placeImage": "Feu clic per a col·locar la imatge o clic i arrossegar per a establir-ne la mida manualment",
|
"placeImage": "Feu clic per a col·locar la imatge o clic i arrossegar per a establir-ne la mida manualment",
|
||||||
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
|
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
|
||||||
"bindTextToElement": "Premeu enter per a afegir-hi text",
|
"bindTextToElement": "Premeu enter per a afegir-hi text",
|
||||||
"deepBoxSelect": ""
|
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "No es pot mostrar la previsualització",
|
"cannotShowPreview": "No es pot mostrar la previsualització",
|
||||||
@@ -266,7 +268,7 @@
|
|||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Llegiu el nostre blog",
|
"blog": "Llegiu el nostre blog",
|
||||||
"click": "clic",
|
"click": "clic",
|
||||||
"deepSelect": "",
|
"deepSelect": "Selecció profunda",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"curvedArrow": "Fletxa corba",
|
"curvedArrow": "Fletxa corba",
|
||||||
"curvedLine": "Línia corba",
|
"curvedLine": "Línia corba",
|
||||||
@@ -305,7 +307,7 @@
|
|||||||
"libraryName": "Nom de la vostra biblioteca",
|
"libraryName": "Nom de la vostra biblioteca",
|
||||||
"libraryDesc": "Descripció de la biblioteca per a ajudar a la gent a entendre'n el funcionament",
|
"libraryDesc": "Descripció de la biblioteca per a ajudar a la gent a entendre'n el funcionament",
|
||||||
"githubHandle": "",
|
"githubHandle": "",
|
||||||
"twitterHandle": "",
|
"twitterHandle": "Usuari de twitter (opcional), per tal que puguem donar-vos crèdit quan fem la promoció a Twitter",
|
||||||
"website": "Enllaç al vostre lloc web personal o a qualsevol altre (opcional)"
|
"website": "Enllaç al vostre lloc web personal o a qualsevol altre (opcional)"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "",
|
"cartoonist": "",
|
||||||
"fileTitle": "",
|
"fileTitle": "",
|
||||||
"colorPicker": "",
|
"colorPicker": "",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Pozadí plátna",
|
"canvasBackground": "Pozadí plátna",
|
||||||
"drawingCanvas": "",
|
"drawingCanvas": "",
|
||||||
"layers": "",
|
"layers": "",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Výběr",
|
"selection": "Výběr",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"selectAll": "Marker alle",
|
"selectAll": "Marker alle",
|
||||||
"multiSelect": "",
|
"multiSelect": "",
|
||||||
"moveCanvas": "",
|
"moveCanvas": "",
|
||||||
"cut": "",
|
"cut": "Klip",
|
||||||
"copy": "Kopier",
|
"copy": "Kopier",
|
||||||
"copyAsPng": "Kopier til klippebord som PNG",
|
"copyAsPng": "Kopier til klippebord som PNG",
|
||||||
"copyAsSvg": "Kopier til klippebord som SVG",
|
"copyAsSvg": "Kopier til klippebord som SVG",
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "",
|
"cartoonist": "",
|
||||||
"fileTitle": "Filnavn",
|
"fileTitle": "Filnavn",
|
||||||
"colorPicker": "Farvevælger",
|
"colorPicker": "Farvevælger",
|
||||||
|
"canvasColors": "Brugt på lærred",
|
||||||
"canvasBackground": "",
|
"canvasBackground": "",
|
||||||
"drawingCanvas": "",
|
"drawingCanvas": "",
|
||||||
"layers": "",
|
"layers": "",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "",
|
"selection": "",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Karikaturist",
|
"cartoonist": "Karikaturist",
|
||||||
"fileTitle": "Dateiname",
|
"fileTitle": "Dateiname",
|
||||||
"colorPicker": "Farbauswähler",
|
"colorPicker": "Farbauswähler",
|
||||||
|
"canvasColors": "Auf Leinwand verwendet",
|
||||||
"canvasBackground": "Zeichenflächenhintergrund",
|
"canvasBackground": "Zeichenflächenhintergrund",
|
||||||
"drawingCanvas": "Leinwand",
|
"drawingCanvas": "Leinwand",
|
||||||
"layers": "Ebenen",
|
"layers": "Ebenen",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Das Bild konnte nicht eingefügt werden. Versuche es später erneut...",
|
"imageInsertError": "Das Bild konnte nicht eingefügt werden. Versuche es später erneut...",
|
||||||
"fileTooBig": "Die Datei ist zu groß. Die maximal zulässige Größe ist {{maxSize}}.",
|
"fileTooBig": "Die Datei ist zu groß. Die maximal zulässige Größe ist {{maxSize}}.",
|
||||||
"svgImageInsertError": "SVG-Bild konnte nicht eingefügt werden. Das SVG-Markup sieht ungültig aus.",
|
"svgImageInsertError": "SVG-Bild konnte nicht eingefügt werden. Das SVG-Markup sieht ungültig aus.",
|
||||||
"invalidSVGString": "Ungültige SVG."
|
"invalidSVGString": "Ungültige SVG.",
|
||||||
|
"cannotResolveCollabServer": "Konnte keine Verbindung zum Collab-Server herstellen. Bitte lade die Seite neu und versuche es erneut."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Auswahl",
|
"selection": "Auswahl",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Σκιτσογράφος",
|
"cartoonist": "Σκιτσογράφος",
|
||||||
"fileTitle": "Όνομα αρχείου",
|
"fileTitle": "Όνομα αρχείου",
|
||||||
"colorPicker": "Επιλογή Χρώματος",
|
"colorPicker": "Επιλογή Χρώματος",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Φόντο καμβά",
|
"canvasBackground": "Φόντο καμβά",
|
||||||
"drawingCanvas": "Σχεδίαση καμβά",
|
"drawingCanvas": "Σχεδίαση καμβά",
|
||||||
"layers": "Στρώματα",
|
"layers": "Στρώματα",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
|
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
|
||||||
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
|
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": "Μη έγκυρο SVG."
|
"invalidSVGString": "Μη έγκυρο SVG.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Επιλογή",
|
"selection": "Επιλογή",
|
||||||
|
|||||||
+8
-3
@@ -16,6 +16,7 @@
|
|||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"copyStyles": "Copy styles",
|
"copyStyles": "Copy styles",
|
||||||
"pasteStyles": "Paste styles",
|
"pasteStyles": "Paste styles",
|
||||||
|
"fontColor": "Font color",
|
||||||
"stroke": "Stroke",
|
"stroke": "Stroke",
|
||||||
"background": "Background",
|
"background": "Background",
|
||||||
"fill": "Fill",
|
"fill": "Fill",
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
"cartoonist": "Cartoonist",
|
"cartoonist": "Cartoonist",
|
||||||
"fileTitle": "File name",
|
"fileTitle": "File name",
|
||||||
"colorPicker": "Color picker",
|
"colorPicker": "Color picker",
|
||||||
|
"canvasColors": "Used on canvas",
|
||||||
"canvasBackground": "Canvas background",
|
"canvasBackground": "Canvas background",
|
||||||
"drawingCanvas": "Drawing canvas",
|
"drawingCanvas": "Drawing canvas",
|
||||||
"layers": "Layers",
|
"layers": "Layers",
|
||||||
@@ -179,7 +181,8 @@
|
|||||||
"imageInsertError": "Couldn't insert image. Try again later...",
|
"imageInsertError": "Couldn't insert image. Try again later...",
|
||||||
"fileTooBig": "File is too big. Maximum allowed size is {{maxSize}}.",
|
"fileTooBig": "File is too big. Maximum allowed size is {{maxSize}}.",
|
||||||
"svgImageInsertError": "Couldn't insert SVG image. The SVG markup looks invalid.",
|
"svgImageInsertError": "Couldn't insert SVG image. The SVG markup looks invalid.",
|
||||||
"invalidSVGString": "Invalid SVG."
|
"invalidSVGString": "Invalid SVG.",
|
||||||
|
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selection",
|
"selection": "Selection",
|
||||||
@@ -194,7 +197,8 @@
|
|||||||
"library": "Library",
|
"library": "Library",
|
||||||
"lock": "Keep selected tool active after drawing",
|
"lock": "Keep selected tool active after drawing",
|
||||||
"penMode": "Prevent pinch-zoom and accept freedraw input only from pen",
|
"penMode": "Prevent pinch-zoom and accept freedraw input only from pen",
|
||||||
"link": "Add/ Update link for a selected shape"
|
"link": "Add/ Update link for a selected shape",
|
||||||
|
"eraser": "Eraser"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas actions",
|
"canvasActions": "Canvas actions",
|
||||||
@@ -219,7 +223,8 @@
|
|||||||
"placeImage": "Click to place the image, or click and drag to set its size manually",
|
"placeImage": "Click to place the image, or click and drag to set its size manually",
|
||||||
"publishLibrary": "Publish your own library",
|
"publishLibrary": "Publish your own library",
|
||||||
"bindTextToElement": "Press enter to add text",
|
"bindTextToElement": "Press enter to add text",
|
||||||
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging"
|
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
|
||||||
|
"eraserRevert": "Hold Alt to revert the elements marked for deletion"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Cannot show preview",
|
"cannotShowPreview": "Cannot show preview",
|
||||||
|
|||||||
+59
-57
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Caricatura",
|
"cartoonist": "Caricatura",
|
||||||
"fileTitle": "Nombre del archivo",
|
"fileTitle": "Nombre del archivo",
|
||||||
"colorPicker": "Selector de color",
|
"colorPicker": "Selector de color",
|
||||||
|
"canvasColors": "Usado en lienzo",
|
||||||
"canvasBackground": "Fondo del lienzo",
|
"canvasBackground": "Fondo del lienzo",
|
||||||
"drawingCanvas": "Lienzo de dibujo",
|
"drawingCanvas": "Lienzo de dibujo",
|
||||||
"layers": "Capas",
|
"layers": "Capas",
|
||||||
@@ -101,15 +102,15 @@
|
|||||||
"showStroke": "Mostrar selector de color de trazo",
|
"showStroke": "Mostrar selector de color de trazo",
|
||||||
"showBackground": "Mostrar el selector de color de fondo",
|
"showBackground": "Mostrar el selector de color de fondo",
|
||||||
"toggleTheme": "Alternar tema",
|
"toggleTheme": "Alternar tema",
|
||||||
"personalLib": "",
|
"personalLib": "Biblioteca personal",
|
||||||
"excalidrawLib": "",
|
"excalidrawLib": "Biblioteca Excalidraw",
|
||||||
"decreaseFontSize": "",
|
"decreaseFontSize": "Disminuir tamaño de letra",
|
||||||
"increaseFontSize": "",
|
"increaseFontSize": "Aumentar el tamaño de letra",
|
||||||
"unbindText": "",
|
"unbindText": "Desvincular texto",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "Editar enlace",
|
||||||
"create": "",
|
"create": "Crear enlace",
|
||||||
"label": ""
|
"label": "Enlace"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
@@ -146,10 +147,10 @@
|
|||||||
"exitZenMode": "Salir del modo Zen",
|
"exitZenMode": "Salir del modo Zen",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"clear": "Borrar",
|
"clear": "Borrar",
|
||||||
"remove": "",
|
"remove": "Eliminar",
|
||||||
"publishLibrary": "",
|
"publishLibrary": "Publicar",
|
||||||
"submit": "",
|
"submit": "Enviar",
|
||||||
"confirm": ""
|
"confirm": "Confirmar"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
|
"clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
|
||||||
@@ -171,7 +172,7 @@
|
|||||||
"cannotRestoreFromImage": "No se pudo restaurar la escena desde este archivo de imagen",
|
"cannotRestoreFromImage": "No se pudo restaurar la escena desde este archivo de imagen",
|
||||||
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
|
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
|
||||||
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
|
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
|
||||||
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
|
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "No se pudo insertar la imagen. Inténtelo de nuevo más tarde...",
|
"imageInsertError": "No se pudo insertar la imagen. Inténtelo de nuevo más tarde...",
|
||||||
"fileTooBig": "Archivo demasiado grande. El tamaño máximo permitido es {{maxSize}}.",
|
"fileTooBig": "Archivo demasiado grande. El tamaño máximo permitido es {{maxSize}}.",
|
||||||
"svgImageInsertError": "No se pudo insertar la imagen SVG. El código SVG parece inválido.",
|
"svgImageInsertError": "No se pudo insertar la imagen SVG. El código SVG parece inválido.",
|
||||||
"invalidSVGString": "SVG no válido."
|
"invalidSVGString": "SVG no válido.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selección",
|
"selection": "Selección",
|
||||||
@@ -193,8 +195,8 @@
|
|||||||
"text": "Texto",
|
"text": "Texto",
|
||||||
"library": "Biblioteca",
|
"library": "Biblioteca",
|
||||||
"lock": "Mantener la herramienta seleccionada activa después de dibujar",
|
"lock": "Mantener la herramienta seleccionada activa después de dibujar",
|
||||||
"penMode": "",
|
"penMode": "Evitar el zoom de pellizco y aceptar la entrada libre sólo desde el lápiz",
|
||||||
"link": ""
|
"link": "Añadir/Actualizar enlace para una forma seleccionada"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Acciones del lienzo",
|
"canvasActions": "Acciones del lienzo",
|
||||||
@@ -202,7 +204,7 @@
|
|||||||
"shapes": "Formas"
|
"shapes": "Formas"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Para mover lienzo, mantenga la rueda del ratón o la barra espaciadora mientras arrastra",
|
"canvasPanning": "Para mover el lienzo, mantenga la rueda del ratón o la barra de espacio mientras arrastra",
|
||||||
"linearElement": "Haz clic para dibujar múltiples puntos, arrastrar para solo una línea",
|
"linearElement": "Haz clic para dibujar múltiples puntos, arrastrar para solo una línea",
|
||||||
"freeDraw": "Haz clic y arrastra, suelta al terminar",
|
"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",
|
"text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección",
|
||||||
@@ -214,12 +216,12 @@
|
|||||||
"resizeImage": "Puede redimensionar libremente pulsando SHIFT,\npulse ALT para redimensionar desde el centro",
|
"resizeImage": "Puede redimensionar libremente pulsando SHIFT,\npulse ALT para redimensionar desde el centro",
|
||||||
"rotate": "Puedes restringir los ángulos manteniendo presionado SHIFT mientras giras",
|
"rotate": "Puedes restringir los ángulos manteniendo presionado SHIFT mientras giras",
|
||||||
"lineEditor_info": "Doble clic o pulse Enter para editar puntos",
|
"lineEditor_info": "Doble clic o pulse Enter para editar puntos",
|
||||||
"lineEditor_pointSelected": "",
|
"lineEditor_pointSelected": "Presione Suprimir para eliminar el/los punto(s), CtrlOrCmd+D para duplicarlo, o arrástrelo para moverlo",
|
||||||
"lineEditor_nothingSelected": "",
|
"lineEditor_nothingSelected": "Seleccione un punto a editar (mantenga MAYÚSCULAS para seleccionar múltiples),\no mantenga pulsado Alt y haga clic para añadir nuevos puntos",
|
||||||
"placeImage": "Haga clic para colocar la imagen o haga clic y arrastre para establecer su tamaño manualmente",
|
"placeImage": "Haga clic para colocar la imagen o haga clic y arrastre para establecer su tamaño manualmente",
|
||||||
"publishLibrary": "",
|
"publishLibrary": "Publica tu propia biblioteca",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "Presione Entrar para agregar",
|
||||||
"deepBoxSelect": ""
|
"deepBoxSelect": "Mantén CtrlOrCmd para seleccionar en profundidad, y para evitar arrastrar"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "No se puede mostrar la vista previa",
|
"cannotShowPreview": "No se puede mostrar la vista previa",
|
||||||
@@ -266,8 +268,8 @@
|
|||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Lea nuestro blog",
|
"blog": "Lea nuestro blog",
|
||||||
"click": "clic",
|
"click": "clic",
|
||||||
"deepSelect": "",
|
"deepSelect": "Selección profunda",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "Seleccione en profundidad dentro de la caja, y evite arrastrar",
|
||||||
"curvedArrow": "Flecha curva",
|
"curvedArrow": "Flecha curva",
|
||||||
"curvedLine": "Línea curva",
|
"curvedLine": "Línea curva",
|
||||||
"documentation": "Documentación",
|
"documentation": "Documentación",
|
||||||
@@ -292,52 +294,52 @@
|
|||||||
"title": "Borrar lienzo"
|
"title": "Borrar lienzo"
|
||||||
},
|
},
|
||||||
"publishDialog": {
|
"publishDialog": {
|
||||||
"title": "",
|
"title": "Publicar biblioteca",
|
||||||
"itemName": "",
|
"itemName": "Nombre del artículo",
|
||||||
"authorName": "",
|
"authorName": "Nombre del autor",
|
||||||
"githubUsername": "",
|
"githubUsername": "Nombre de usuario de Github",
|
||||||
"twitterUsername": "",
|
"twitterUsername": "Nombre de usuario de Twitter",
|
||||||
"libraryName": "",
|
"libraryName": "Nombre de la librería",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "Descripción de la biblioteca",
|
||||||
"website": "",
|
"website": "Sitio Web",
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"authorName": "",
|
"authorName": "Nombre o nombre de usuario",
|
||||||
"libraryName": "",
|
"libraryName": "Nombre de tu biblioteca",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "Descripción de su biblioteca para ayudar a la gente a entender su uso",
|
||||||
"githubHandle": "",
|
"githubHandle": "GitHub maneja (opcional), así que puede editar la biblioteca una vez enviada para su revisión",
|
||||||
"twitterHandle": "",
|
"twitterHandle": "Nombre de usuario de Twitter (opcional), así que sabemos a quién acreditar cuando se promociona en Twitter",
|
||||||
"website": ""
|
"website": "Enlace a su sitio web personal o en cualquier otro lugar (opcional)"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"required": "",
|
"required": "Requerido",
|
||||||
"website": ""
|
"website": "Introduce una URL válida"
|
||||||
},
|
},
|
||||||
"noteDescription": {
|
"noteDescription": {
|
||||||
"pre": "",
|
"pre": "Envía tu biblioteca para ser incluida en el ",
|
||||||
"link": "",
|
"link": "repositorio de librería pública",
|
||||||
"post": ""
|
"post": "para que otras personas utilicen en sus dibujos."
|
||||||
},
|
},
|
||||||
"noteGuidelines": {
|
"noteGuidelines": {
|
||||||
"pre": "",
|
"pre": "La biblioteca debe ser aprobada manualmente primero. Por favor, lea la ",
|
||||||
"link": "",
|
"link": "pautas",
|
||||||
"post": ""
|
"post": " antes de enviar. Necesitará una cuenta de GitHub para comunicarse y hacer cambios si se solicita, pero no es estrictamente necesario."
|
||||||
},
|
},
|
||||||
"noteLicense": {
|
"noteLicense": {
|
||||||
"pre": "",
|
"pre": "Al enviar, usted acepta que la biblioteca se publicará bajo el ",
|
||||||
"link": "",
|
"link": "Licencia MIT ",
|
||||||
"post": ""
|
"post": "que en breve significa que cualquiera puede utilizarlos sin restricciones."
|
||||||
},
|
},
|
||||||
"noteItems": "",
|
"noteItems": "Cada elemento de la biblioteca debe tener su propio nombre para que sea filtrable. Los siguientes elementos de la biblioteca serán incluidos:",
|
||||||
"atleastOneLibItem": ""
|
"atleastOneLibItem": "Por favor, seleccione al menos un elemento de la biblioteca para empezar"
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "",
|
"title": "Biblioteca enviada",
|
||||||
"content": "",
|
"content": "Gracias {{authorName}}. Su biblioteca ha sido enviada para ser revisada. Puede seguir el estado",
|
||||||
"link": ""
|
"link": "aquí"
|
||||||
},
|
},
|
||||||
"confirmDialog": {
|
"confirmDialog": {
|
||||||
"resetLibrary": "",
|
"resetLibrary": "Reiniciar biblioteca",
|
||||||
"removeItemsFromLib": ""
|
"removeItemsFromLib": "Eliminar elementos seleccionados de la biblioteca"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán.",
|
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán.",
|
||||||
@@ -359,7 +361,7 @@
|
|||||||
"width": "Ancho"
|
"width": "Ancho"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"addedToLibrary": "",
|
"addedToLibrary": "Añadido a la biblioteca",
|
||||||
"copyStyles": "Estilos copiados.",
|
"copyStyles": "Estilos copiados.",
|
||||||
"copyToClipboard": "Copiado en el portapapeles.",
|
"copyToClipboard": "Copiado en el portapapeles.",
|
||||||
"copyToClipboardAsPng": "Copiado {{exportSelection}} al portapapeles como PNG\n({{exportColorScheme}})",
|
"copyToClipboardAsPng": "Copiado {{exportSelection}} al portapapeles como PNG\n({{exportColorScheme}})",
|
||||||
|
|||||||
+16
-14
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Marrazkilaria",
|
"cartoonist": "Marrazkilaria",
|
||||||
"fileTitle": "Fitxategi izena",
|
"fileTitle": "Fitxategi izena",
|
||||||
"colorPicker": "Kolore-hautatzailea",
|
"colorPicker": "Kolore-hautatzailea",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Oihalaren atzeko planoa",
|
"canvasBackground": "Oihalaren atzeko planoa",
|
||||||
"drawingCanvas": "Marrazteko oihala",
|
"drawingCanvas": "Marrazteko oihala",
|
||||||
"layers": "Geruzak",
|
"layers": "Geruzak",
|
||||||
@@ -107,9 +108,9 @@
|
|||||||
"increaseFontSize": "Handitu letra tamaina",
|
"increaseFontSize": "Handitu letra tamaina",
|
||||||
"unbindText": "Askatu testua",
|
"unbindText": "Askatu testua",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "Editatu esteka",
|
||||||
"create": "",
|
"create": "Sortu esteka",
|
||||||
"label": ""
|
"label": "Esteka"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
@@ -149,7 +150,7 @@
|
|||||||
"remove": "Kendu",
|
"remove": "Kendu",
|
||||||
"publishLibrary": "Argitaratu",
|
"publishLibrary": "Argitaratu",
|
||||||
"submit": "Bidali",
|
"submit": "Bidali",
|
||||||
"confirm": "Baieztatu"
|
"confirm": "Bai"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Honek oihal osoa garbituko du. Ziur zaude?",
|
"clearReset": "Honek oihal osoa garbituko du. Ziur zaude?",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Ezin izan da irudia txertatu. Saiatu berriro geroago...",
|
"imageInsertError": "Ezin izan da irudia txertatu. Saiatu berriro geroago...",
|
||||||
"fileTooBig": "Fitxategia handiegia da. Onartutako gehienezko tamaina {{maxSize}} da.",
|
"fileTooBig": "Fitxategia handiegia da. Onartutako gehienezko tamaina {{maxSize}} da.",
|
||||||
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
|
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
|
||||||
"invalidSVGString": "SVG baliogabea."
|
"invalidSVGString": "SVG baliogabea.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Hautapena",
|
"selection": "Hautapena",
|
||||||
@@ -193,8 +195,8 @@
|
|||||||
"text": "Testua",
|
"text": "Testua",
|
||||||
"library": "Liburutegia",
|
"library": "Liburutegia",
|
||||||
"lock": "Mantendu aktibo hautatutako tresna marraztu ondoren",
|
"lock": "Mantendu aktibo hautatutako tresna marraztu ondoren",
|
||||||
"penMode": "",
|
"penMode": "Saihestu txikiagotzea eta onartu marrazte libreko idazketa solik arkatza bidez",
|
||||||
"link": ""
|
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas ekintzak",
|
"canvasActions": "Canvas ekintzak",
|
||||||
@@ -219,15 +221,15 @@
|
|||||||
"placeImage": "Egin klik irudia kokatzeko, edo egin klik eta arrastatu bere tamaina eskuz ezartzeko",
|
"placeImage": "Egin klik irudia kokatzeko, edo egin klik eta arrastatu bere tamaina eskuz ezartzeko",
|
||||||
"publishLibrary": "Argitaratu zure liburutegia",
|
"publishLibrary": "Argitaratu zure liburutegia",
|
||||||
"bindTextToElement": "Sakatu Sartu testua gehitzeko",
|
"bindTextToElement": "Sakatu Sartu testua gehitzeko",
|
||||||
"deepBoxSelect": ""
|
"deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "",
|
"cannotShowPreview": "Ezin da oihala aurreikusi",
|
||||||
"canvasTooBig": "",
|
"canvasTooBig": "Agian oihala handiegia da.",
|
||||||
"canvasTooBigTip": ""
|
"canvasTooBigTip": "Aholkua: saiatu urrunen dauden elementuak pixka bat hurbiltzen."
|
||||||
},
|
},
|
||||||
"errorSplash": {
|
"errorSplash": {
|
||||||
"headingMain_pre": "",
|
"headingMain_pre": "Errore bat aurkitu da. Saiatu ",
|
||||||
"headingMain_button": "orria birkargatzen.",
|
"headingMain_button": "orria birkargatzen.",
|
||||||
"clearCanvasMessage": "Birkargatzea ez bada burutzen, saiatu ",
|
"clearCanvasMessage": "Birkargatzea ez bada burutzen, saiatu ",
|
||||||
"clearCanvasMessage_button": "oihala garbitzen.",
|
"clearCanvasMessage_button": "oihala garbitzen.",
|
||||||
@@ -266,8 +268,8 @@
|
|||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Irakurri gure bloga",
|
"blog": "Irakurri gure bloga",
|
||||||
"click": "sakatu",
|
"click": "sakatu",
|
||||||
"deepSelect": "",
|
"deepSelect": "Hautapen sakona",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "Hautapen sakona egin laukizuzen bidez, eta saihestu arrastatzea",
|
||||||
"curvedArrow": "Gezi kurbatua",
|
"curvedArrow": "Gezi kurbatua",
|
||||||
"curvedLine": "Lerro kurbatua",
|
"curvedLine": "Lerro kurbatua",
|
||||||
"documentation": "Dokumentazioa",
|
"documentation": "Dokumentazioa",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "کارتونیست",
|
"cartoonist": "کارتونیست",
|
||||||
"fileTitle": "نام فایل",
|
"fileTitle": "نام فایل",
|
||||||
"colorPicker": "انتخابگر رنگ",
|
"colorPicker": "انتخابگر رنگ",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "بوم",
|
"canvasBackground": "بوم",
|
||||||
"drawingCanvas": "بوم نقاشی",
|
"drawingCanvas": "بوم نقاشی",
|
||||||
"layers": "لایه ها",
|
"layers": "لایه ها",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "گزینش",
|
"selection": "گزینش",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Sarjakuva",
|
"cartoonist": "Sarjakuva",
|
||||||
"fileTitle": "Tiedostonimi",
|
"fileTitle": "Tiedostonimi",
|
||||||
"colorPicker": "Värin valinta",
|
"colorPicker": "Värin valinta",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Piirtoalueen tausta",
|
"canvasBackground": "Piirtoalueen tausta",
|
||||||
"drawingCanvas": "Piirtoalue",
|
"drawingCanvas": "Piirtoalue",
|
||||||
"layers": "Tasot",
|
"layers": "Tasot",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Kuvan lisääminen epäonnistui. Yritä myöhemmin uudelleen...",
|
"imageInsertError": "Kuvan lisääminen epäonnistui. Yritä myöhemmin uudelleen...",
|
||||||
"fileTooBig": "Tiedosto on liian suuri. Suurin sallittu koko on {{maxSize}}.",
|
"fileTooBig": "Tiedosto on liian suuri. Suurin sallittu koko on {{maxSize}}.",
|
||||||
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
|
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
|
||||||
"invalidSVGString": "Virheellinen SVG."
|
"invalidSVGString": "Virheellinen SVG.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Valinta",
|
"selection": "Valinta",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Caricaturiste",
|
"cartoonist": "Caricaturiste",
|
||||||
"fileTitle": "Nom du fichier",
|
"fileTitle": "Nom du fichier",
|
||||||
"colorPicker": "Sélecteur de couleur",
|
"colorPicker": "Sélecteur de couleur",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Arrière-plan du canevas",
|
"canvasBackground": "Arrière-plan du canevas",
|
||||||
"drawingCanvas": "Zone de dessin",
|
"drawingCanvas": "Zone de dessin",
|
||||||
"layers": "Calques",
|
"layers": "Calques",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Impossible d'insérer l'image. Réessayez plus tard...",
|
"imageInsertError": "Impossible d'insérer l'image. Réessayez plus tard...",
|
||||||
"fileTooBig": "Le fichier est trop volumineux. La taille maximale autorisée est de {{maxSize}}.",
|
"fileTooBig": "Le fichier est trop volumineux. La taille maximale autorisée est de {{maxSize}}.",
|
||||||
"svgImageInsertError": "Impossible d'insérer l'image SVG. Le balisage SVG semble invalide.",
|
"svgImageInsertError": "Impossible d'insérer l'image SVG. Le balisage SVG semble invalide.",
|
||||||
"invalidSVGString": "SVG invalide."
|
"invalidSVGString": "SVG invalide.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Sélection",
|
"selection": "Sélection",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "קריקטוריסט",
|
"cartoonist": "קריקטוריסט",
|
||||||
"fileTitle": "שם קובץ",
|
"fileTitle": "שם קובץ",
|
||||||
"colorPicker": "בחירת צבע",
|
"colorPicker": "בחירת צבע",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "רקע הלוח",
|
"canvasBackground": "רקע הלוח",
|
||||||
"drawingCanvas": "לוח ציור",
|
"drawingCanvas": "לוח ציור",
|
||||||
"layers": "שכבות",
|
"layers": "שכבות",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "בחירה",
|
"selection": "בחירה",
|
||||||
|
|||||||
+15
-13
@@ -35,11 +35,11 @@
|
|||||||
"arrowhead_arrow": "तीर",
|
"arrowhead_arrow": "तीर",
|
||||||
"arrowhead_bar": "बार",
|
"arrowhead_bar": "बार",
|
||||||
"arrowhead_dot": "बिंदु",
|
"arrowhead_dot": "बिंदु",
|
||||||
"arrowhead_triangle": "",
|
"arrowhead_triangle": "त्रिकोण",
|
||||||
"fontSize": "फ़ॉन्ट का आकार",
|
"fontSize": "फ़ॉन्ट का आकार",
|
||||||
"fontFamily": "फ़ॉन्ट का परिवार",
|
"fontFamily": "फ़ॉन्ट का परिवार",
|
||||||
"onlySelected": "केवल चयनित",
|
"onlySelected": "केवल चयनित",
|
||||||
"withBackground": "",
|
"withBackground": "पृष्ठभूमि",
|
||||||
"exportEmbedScene": "",
|
"exportEmbedScene": "",
|
||||||
"exportEmbedScene_details": "निर्यात एम्बेड दृश्य विवरण",
|
"exportEmbedScene_details": "निर्यात एम्बेड दृश्य विवरण",
|
||||||
"addWatermark": "ऐड \"मेड विथ एक्सकैलिडराव\"",
|
"addWatermark": "ऐड \"मेड विथ एक्सकैलिडराव\"",
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "व्यंग्य चित्रकार",
|
"cartoonist": "व्यंग्य चित्रकार",
|
||||||
"fileTitle": "फ़ाइल का नाम",
|
"fileTitle": "फ़ाइल का नाम",
|
||||||
"colorPicker": "रंग चयन",
|
"colorPicker": "रंग चयन",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "कैनवास बैकग्राउंड",
|
"canvasBackground": "कैनवास बैकग्राउंड",
|
||||||
"drawingCanvas": "कैनवास बना रहे हैं",
|
"drawingCanvas": "कैनवास बना रहे हैं",
|
||||||
"layers": "परतें",
|
"layers": "परतें",
|
||||||
@@ -93,18 +94,18 @@
|
|||||||
"centerHorizontally": "क्षैतिज केन्द्रित",
|
"centerHorizontally": "क्षैतिज केन्द्रित",
|
||||||
"distributeHorizontally": "क्षैतिज रूप से वितरित करें",
|
"distributeHorizontally": "क्षैतिज रूप से वितरित करें",
|
||||||
"distributeVertically": "खड़ी रूप से वितरित करें",
|
"distributeVertically": "खड़ी रूप से वितरित करें",
|
||||||
"flipHorizontal": "",
|
"flipHorizontal": "दायें बायें पलटे",
|
||||||
"flipVertical": "",
|
"flipVertical": "ऊपर नीचे पलटे",
|
||||||
"viewMode": "",
|
"viewMode": "अलग अलग देखें",
|
||||||
"toggleExportColorScheme": "",
|
"toggleExportColorScheme": "",
|
||||||
"share": "",
|
"share": "शेयर करें",
|
||||||
"showStroke": "",
|
"showStroke": "",
|
||||||
"showBackground": "",
|
"showBackground": "",
|
||||||
"toggleTheme": "",
|
"toggleTheme": "",
|
||||||
"personalLib": "",
|
"personalLib": "",
|
||||||
"excalidrawLib": "",
|
"excalidrawLib": "",
|
||||||
"decreaseFontSize": "",
|
"decreaseFontSize": "आकार घटाइऐ",
|
||||||
"increaseFontSize": "",
|
"increaseFontSize": "फ़ॉन्ट आकार बढ़ाएँ",
|
||||||
"unbindText": "",
|
"unbindText": "",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "चयन",
|
"selection": "चयन",
|
||||||
@@ -408,11 +410,11 @@
|
|||||||
"5f3dc4": "",
|
"5f3dc4": "",
|
||||||
"364fc7": "",
|
"364fc7": "",
|
||||||
"1864ab": "",
|
"1864ab": "",
|
||||||
"0b7285": "",
|
"0b7285": "आसमानी",
|
||||||
"087f5b": "",
|
"087f5b": "",
|
||||||
"2b8a3e": "",
|
"2b8a3e": "हरा",
|
||||||
"5c940d": "",
|
"5c940d": "",
|
||||||
"e67700": "",
|
"e67700": "पीला",
|
||||||
"d9480f": ""
|
"d9480f": "नारंगी"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+184
-182
@@ -35,12 +35,12 @@
|
|||||||
"arrowhead_arrow": "Nyíl",
|
"arrowhead_arrow": "Nyíl",
|
||||||
"arrowhead_bar": "Oszlop",
|
"arrowhead_bar": "Oszlop",
|
||||||
"arrowhead_dot": "Pont",
|
"arrowhead_dot": "Pont",
|
||||||
"arrowhead_triangle": "",
|
"arrowhead_triangle": "Háromszög",
|
||||||
"fontSize": "Betűméret",
|
"fontSize": "Betűméret",
|
||||||
"fontFamily": "Betűkészlet család",
|
"fontFamily": "Betűkészlet család",
|
||||||
"onlySelected": "Csak a kijelölt",
|
"onlySelected": "Csak a kijelölt",
|
||||||
"withBackground": "",
|
"withBackground": "Háttér",
|
||||||
"exportEmbedScene": "",
|
"exportEmbedScene": "Jelenet beágyazása",
|
||||||
"exportEmbedScene_details": "A jelenetet leíró adatok hozzá lesznek adva a PNG/SVG fájlhoz, így a jelenetet vissza lehet majd tölteni belőle. Ez megnöveli a fájl méretét.",
|
"exportEmbedScene_details": "A jelenetet leíró adatok hozzá lesznek adva a PNG/SVG fájlhoz, így a jelenetet vissza lehet majd tölteni belőle. Ez megnöveli a fájl méretét.",
|
||||||
"addWatermark": "Add hozzá, hogy \"Excalidraw-val készült\"",
|
"addWatermark": "Add hozzá, hogy \"Excalidraw-val készült\"",
|
||||||
"handDrawn": "Kézzel rajzolt",
|
"handDrawn": "Kézzel rajzolt",
|
||||||
@@ -62,14 +62,15 @@
|
|||||||
"architect": "Tervezői",
|
"architect": "Tervezői",
|
||||||
"artist": "Művészi",
|
"artist": "Művészi",
|
||||||
"cartoonist": "Karikatúrás",
|
"cartoonist": "Karikatúrás",
|
||||||
"fileTitle": "",
|
"fileTitle": "Fájlnév",
|
||||||
"colorPicker": "Színválasztó",
|
"colorPicker": "Színválasztó",
|
||||||
|
"canvasColors": "Rajzvászonon használt",
|
||||||
"canvasBackground": "Vászon háttérszíne",
|
"canvasBackground": "Vászon háttérszíne",
|
||||||
"drawingCanvas": "Rajzvászon",
|
"drawingCanvas": "Rajzvászon",
|
||||||
"layers": "Rétegek",
|
"layers": "Rétegek",
|
||||||
"actions": "Műveletek",
|
"actions": "Műveletek",
|
||||||
"language": "Nyelv",
|
"language": "Nyelv",
|
||||||
"liveCollaboration": "",
|
"liveCollaboration": "Élő együttműködés",
|
||||||
"duplicateSelection": "Duplikálás",
|
"duplicateSelection": "Duplikálás",
|
||||||
"untitled": "Névtelen",
|
"untitled": "Névtelen",
|
||||||
"name": "Név",
|
"name": "Név",
|
||||||
@@ -78,7 +79,7 @@
|
|||||||
"group": "Csoportosítás",
|
"group": "Csoportosítás",
|
||||||
"ungroup": "Csoportbontás",
|
"ungroup": "Csoportbontás",
|
||||||
"collaborators": "Közreműködők",
|
"collaborators": "Közreműködők",
|
||||||
"showGrid": "",
|
"showGrid": "Rács megjelenítése",
|
||||||
"addToLibrary": "Hozzáadás a könyvtárhoz",
|
"addToLibrary": "Hozzáadás a könyvtárhoz",
|
||||||
"removeFromLibrary": "Eltávólítás a könyvtárból",
|
"removeFromLibrary": "Eltávólítás a könyvtárból",
|
||||||
"libraryLoadingMessage": "Könyvtár betöltése…",
|
"libraryLoadingMessage": "Könyvtár betöltése…",
|
||||||
@@ -93,36 +94,36 @@
|
|||||||
"centerHorizontally": "Vízszintesen középre igazított",
|
"centerHorizontally": "Vízszintesen középre igazított",
|
||||||
"distributeHorizontally": "Vízszintes elosztás",
|
"distributeHorizontally": "Vízszintes elosztás",
|
||||||
"distributeVertically": "Függőleges elosztás",
|
"distributeVertically": "Függőleges elosztás",
|
||||||
"flipHorizontal": "",
|
"flipHorizontal": "Vízszintes tükrözés",
|
||||||
"flipVertical": "",
|
"flipVertical": "Függőleges tükrözés",
|
||||||
"viewMode": "",
|
"viewMode": "Nézet",
|
||||||
"toggleExportColorScheme": "",
|
"toggleExportColorScheme": "Exportált színséma váltása",
|
||||||
"share": "",
|
"share": "Megosztás",
|
||||||
"showStroke": "",
|
"showStroke": "Körvonal színválasztó megjelenítése",
|
||||||
"showBackground": "",
|
"showBackground": "Háttérszín-választó megjelenítése",
|
||||||
"toggleTheme": "",
|
"toggleTheme": "Téma váltása",
|
||||||
"personalLib": "",
|
"personalLib": "Személyes könyvtár",
|
||||||
"excalidrawLib": "",
|
"excalidrawLib": "Excalidraw könyvtár",
|
||||||
"decreaseFontSize": "",
|
"decreaseFontSize": "Betűméret csökkentése",
|
||||||
"increaseFontSize": "",
|
"increaseFontSize": "Betűméret növelése",
|
||||||
"unbindText": "",
|
"unbindText": "Szövegkötés feloldása",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "Hivatkozás szerkesztése",
|
||||||
"create": "",
|
"create": "Hivatkozás létrehozása",
|
||||||
"label": ""
|
"label": "Hivatkozás"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Vászon törlése",
|
"clearReset": "Vászon törlése",
|
||||||
"exportJSON": "",
|
"exportJSON": "Exportálás fájlba",
|
||||||
"exportImage": "",
|
"exportImage": "Mentés képként",
|
||||||
"export": "Exportálás",
|
"export": "Exportálás",
|
||||||
"exportToPng": "Exportálás PNG-be",
|
"exportToPng": "Exportálás PNG-be",
|
||||||
"exportToSvg": "Exportálás SVG-be",
|
"exportToSvg": "Exportálás SVG-be",
|
||||||
"copyToClipboard": "Vágólapra másolás",
|
"copyToClipboard": "Vágólapra másolás",
|
||||||
"copyPngToClipboard": "PNG másolása a vágólapra",
|
"copyPngToClipboard": "PNG másolása a vágólapra",
|
||||||
"scale": "Nagyítás",
|
"scale": "Nagyítás",
|
||||||
"save": "",
|
"save": "Mentés az aktuális fájlba",
|
||||||
"saveAs": "Mentés másként",
|
"saveAs": "Mentés másként",
|
||||||
"load": "Betöltés",
|
"load": "Betöltés",
|
||||||
"getShareableLink": "Megosztható link létrehozása",
|
"getShareableLink": "Megosztható link létrehozása",
|
||||||
@@ -137,19 +138,19 @@
|
|||||||
"edit": "Szerkesztés",
|
"edit": "Szerkesztés",
|
||||||
"undo": "Vissza",
|
"undo": "Vissza",
|
||||||
"redo": "Újra",
|
"redo": "Újra",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "Könyvtár alaphelyzetbe állítása",
|
||||||
"createNewRoom": "Új szoba létrehozása",
|
"createNewRoom": "Új szoba létrehozása",
|
||||||
"fullScreen": "Teljes képernyő",
|
"fullScreen": "Teljes képernyő",
|
||||||
"darkMode": "Sötét mód",
|
"darkMode": "Sötét mód",
|
||||||
"lightMode": "Világos mód",
|
"lightMode": "Világos mód",
|
||||||
"zenMode": "Letisztult mód",
|
"zenMode": "Letisztult mód",
|
||||||
"exitZenMode": "Kilépés a letisztult módból",
|
"exitZenMode": "Kilépés a letisztult módból",
|
||||||
"cancel": "",
|
"cancel": "Mégsem",
|
||||||
"clear": "",
|
"clear": "Kiűrítés",
|
||||||
"remove": "",
|
"remove": "Eltávolítás",
|
||||||
"publishLibrary": "",
|
"publishLibrary": "Közzététel",
|
||||||
"submit": "",
|
"submit": "Elküldés",
|
||||||
"confirm": ""
|
"confirm": "Megerősítés"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Ez a művelet törli a vászont. Biztos benne?",
|
"clearReset": "Ez a művelet törli a vászont. Biztos benne?",
|
||||||
@@ -162,39 +163,40 @@
|
|||||||
"decryptFailed": "Nem sikerült visszafejteni a titkosított adatot.",
|
"decryptFailed": "Nem sikerült visszafejteni a titkosított adatot.",
|
||||||
"uploadedSecurly": "A feltöltést végpontok közötti titkosítással biztosítottuk, ami azt jelenti, hogy egy harmadik fél nem tudja megnézni a tartalmát, beleértve az Excalidraw szervereit is.",
|
"uploadedSecurly": "A feltöltést végpontok közötti titkosítással biztosítottuk, ami azt jelenti, hogy egy harmadik fél nem tudja megnézni a tartalmát, beleértve az Excalidraw szervereit is.",
|
||||||
"loadSceneOverridePrompt": "A betöltött külső rajz felül fogja írnia meglévőt. Szeretnéd folytatni?",
|
"loadSceneOverridePrompt": "A betöltött külső rajz felül fogja írnia meglévőt. Szeretnéd folytatni?",
|
||||||
"collabStopOverridePrompt": "",
|
"collabStopOverridePrompt": "A munkamenet leállítása felül fogja írni az előzőleg helyben tárolt rajzot. Biztosan ezt akarod?\n(Ha meg akarod tartani a helyben tárolt rajzot, egyszerűen csak zárd be a böngésző fület)",
|
||||||
"errorLoadingLibrary": "Hibába ütközött a harmarmadik féltől származó könyvtár betöltése.",
|
"errorLoadingLibrary": "Hibába ütközött a harmarmadik féltől származó könyvtár betöltése.",
|
||||||
"errorAddingToLibrary": "",
|
"errorAddingToLibrary": "A tétel nem addható hozzá a könyvtárhoz",
|
||||||
"errorRemovingFromLibrary": "",
|
"errorRemovingFromLibrary": "A tétel nem távolítható el a könyvtárból",
|
||||||
"confirmAddLibrary": "Ez a művelet {{numShapes}} formát fog hozzáadni a könyvtáradhoz. Biztos vagy benne?",
|
"confirmAddLibrary": "Ez a művelet {{numShapes}} formát fog hozzáadni a könyvtáradhoz. Biztos vagy benne?",
|
||||||
"imageDoesNotContainScene": "",
|
"imageDoesNotContainScene": "Úgy tűnik, hogy ez a kép nem tartalmaz jelenetadatokat. Engedélyezted a jelenetbeágyazást az exportálás során?",
|
||||||
"cannotRestoreFromImage": "A jelenet visszaállítása nem sikerült ebből a kép fájlból",
|
"cannotRestoreFromImage": "A jelenet visszaállítása nem sikerült ebből a kép fájlból",
|
||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva."
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "Nem támogatott fájltípus.",
|
||||||
"imageInsertError": "",
|
"imageInsertError": "Nem sikerült beszúrni a képet. Próbáld újra később...",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "A fájl túl nagy. A megengedett maximális méret {{maxSize}}.",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "Nem sikerült beszúrni az SVG-képet. Az SVG szintaktika érvénytelennek tűnik.",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "Érvénytelen SVG.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Kijelölés",
|
"selection": "Kijelölés",
|
||||||
"image": "",
|
"image": "Kép beszúrása",
|
||||||
"rectangle": "Téglalap",
|
"rectangle": "Téglalap",
|
||||||
"diamond": "Rombusz",
|
"diamond": "Rombusz",
|
||||||
"ellipse": "Ellipszis",
|
"ellipse": "Ellipszis",
|
||||||
"arrow": "Nyíl",
|
"arrow": "Nyíl",
|
||||||
"line": "Vonal",
|
"line": "Vonal",
|
||||||
"freedraw": "",
|
"freedraw": "Rajzolás",
|
||||||
"text": "Szöveg",
|
"text": "Szöveg",
|
||||||
"library": "Könyvtár",
|
"library": "Könyvtár",
|
||||||
"lock": "Rajzolás után az aktív eszközt tartsa kijelölve",
|
"lock": "Rajzolás után az aktív eszközt tartsa kijelölve",
|
||||||
"penMode": "",
|
"penMode": "Akadályozza meg a kicsinyítést, és csak tollról fogadja el a szabadkézi bevitelt",
|
||||||
"link": ""
|
"link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Vászon műveletek",
|
"canvasActions": "Vászon műveletek",
|
||||||
@@ -202,24 +204,24 @@
|
|||||||
"shapes": "Alakzatok"
|
"shapes": "Alakzatok"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "",
|
"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",
|
||||||
"linearElement": "Kattintással görbe, az eger húzásával pedig egyenes nyilat rajzolhatsz",
|
"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",
|
"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",
|
"text": "Tipp: A kijelölés eszközzel a dupla kattintás új szöveget hoz létre",
|
||||||
"text_selected": "",
|
"text_selected": "Kattints duplán, vagy nyomj entert a szöveg szerkesztéséhez",
|
||||||
"text_editing": "",
|
"text_editing": "Nyomjd meg az Escape vagy a Ctrl/Cmd+ENTER billentyűkombinációt a szerkesztés befejezéséhez",
|
||||||
"linearElementMulti": "Kattints a következő ív pozíciójára, vagy fejezd be a nyilat az Escape vagy Enter megnyomásával",
|
"linearElementMulti": "Kattints a következő ív pozíciójára, vagy fejezd be a nyilat az Escape vagy Enter megnyomásával",
|
||||||
"lockAngle": "A SHIFT billentyű lenyomva tartásával korlátozhatja forgatás szögét",
|
"lockAngle": "A SHIFT billentyű lenyomva tartásával korlátozhatja forgatás szögét",
|
||||||
"resize": "A SHIFT billentyű lenyomva tartásával az átméretezés megtartja az arányokat,\naz ALT lenyomva tartásával pedig a középpont egy helyben marad",
|
"resize": "A SHIFT billentyű lenyomva tartásával az átméretezés megtartja az arányokat,\naz ALT lenyomva tartásával pedig a középpont egy helyben marad",
|
||||||
"resizeImage": "",
|
"resizeImage": "A SHIFT billentyű lenyomva tartásával szabadon átméretezheted,\ntartsd lenyomva az ALT billentyűt a középről való átméretezéshez",
|
||||||
"rotate": "A SHIFT billentyű lenyomva tartásával korlátozhatja a szögek illesztését",
|
"rotate": "A SHIFT billentyű lenyomva tartásával korlátozhatja a szögek illesztését",
|
||||||
"lineEditor_info": "Kattints duplán, vagy nyomj entert a pontok szerkesztéséhez",
|
"lineEditor_info": "Kattints duplán, vagy nyomj entert a pontok szerkesztéséhez",
|
||||||
"lineEditor_pointSelected": "",
|
"lineEditor_pointSelected": "Nyomd meg a Törlés gombot a pont(ok) eltávolításához,\nA Ctrl/Cmd+D a többszörözéshez, vagy húzással mozgathatja",
|
||||||
"lineEditor_nothingSelected": "",
|
"lineEditor_nothingSelected": "Válaszd ki a szerkeszteni kívánt pontot (több kijelöléséhez tartsd lenyomva a SHIFT billentyűt),\nvagy Alt, és kattintson az új pontok hozzáadásához",
|
||||||
"placeImage": "",
|
"placeImage": "Kattints a kép elhelyezéséhez, vagy kattints és méretezd manuálisan",
|
||||||
"publishLibrary": "",
|
"publishLibrary": "Tedd közzé saját könyvtáradat",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "Nyomd meg az Entert szöveg hozzáadáshoz",
|
||||||
"deepBoxSelect": ""
|
"deepBoxSelect": "Tartsd lenyomva a Ctrl/Cmd billentyűt a mély kijelöléshez és a húzás megakadályozásához"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Előnézet nem jeleníthető meg",
|
"cannotShowPreview": "Előnézet nem jeleníthető meg",
|
||||||
@@ -247,101 +249,101 @@
|
|||||||
"desc_inProgressIntro": "Az élő együttműködési munkamenet folyamatban van.",
|
"desc_inProgressIntro": "Az élő együttműködési munkamenet folyamatban van.",
|
||||||
"desc_shareLink": "Ossza meg ezt a linket bárkivel, akivel együtt szeretne működni:",
|
"desc_shareLink": "Ossza meg ezt a linket bárkivel, akivel együtt szeretne működni:",
|
||||||
"desc_exitSession": "Az munkamenet leállítása kilépteti önt a szobából, de folytathatja a munkát a saját gépén. Vegye figyelembe, hogy ez nem érinti más emberek munkáját és ők továbbra is együttműködhetnek a saját változatukon.",
|
"desc_exitSession": "Az munkamenet leállítása kilépteti önt a szobából, de folytathatja a munkát a saját gépén. Vegye figyelembe, hogy ez nem érinti más emberek munkáját és ők továbbra is együttműködhetnek a saját változatukon.",
|
||||||
"shareTitle": ""
|
"shareTitle": "Csatlakozás egy élő együttműködési munkamenethez az Excalidraw-ban"
|
||||||
},
|
},
|
||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": "Hiba"
|
"title": "Hiba"
|
||||||
},
|
},
|
||||||
"exportDialog": {
|
"exportDialog": {
|
||||||
"disk_title": "",
|
"disk_title": "Mentés lemezre",
|
||||||
"disk_details": "",
|
"disk_details": "Exportálja a jelenetadatokat egy fájlba, amelyből később importálhatja.",
|
||||||
"disk_button": "",
|
"disk_button": "Mentés fájlba",
|
||||||
"link_title": "",
|
"link_title": "Megosztható hivatkozás",
|
||||||
"link_details": "",
|
"link_details": "Exportálás csak olvasható hivatkozásként.",
|
||||||
"link_button": "",
|
"link_button": "Exportálás hivatkozásba",
|
||||||
"excalidrawplus_description": "",
|
"excalidrawplus_description": "Mentse el a jelenetet az Excalidraw+ munkaterületére.",
|
||||||
"excalidrawplus_button": "",
|
"excalidrawplus_button": "Exportálás",
|
||||||
"excalidrawplus_exportError": ""
|
"excalidrawplus_exportError": "Jelenleg nem lehet exportálni az Excalidraw+-ba..."
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "",
|
"blog": "Olvasd a blogunkat",
|
||||||
"click": "",
|
"click": "kattintás",
|
||||||
"deepSelect": "",
|
"deepSelect": "Mély kijelölés",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "Mély kijelölés a dobozon belül, és a húzás megakadályozása",
|
||||||
"curvedArrow": "",
|
"curvedArrow": "Ívelt nyíl",
|
||||||
"curvedLine": "",
|
"curvedLine": "Ívelt vonal",
|
||||||
"documentation": "",
|
"documentation": "Dokumentáció",
|
||||||
"doubleClick": "",
|
"doubleClick": "dupla kattintás",
|
||||||
"drag": "",
|
"drag": "vonszolás",
|
||||||
"editor": "",
|
"editor": "Szerkesztő",
|
||||||
"editSelectedShape": "",
|
"editSelectedShape": "Kijelölt alakzat szerkesztése (szöveg/nyíl/vonal)",
|
||||||
"github": "",
|
"github": "Hibát találtál? Küld be",
|
||||||
"howto": "",
|
"howto": "Kövesd az útmutatóinkat",
|
||||||
"or": "",
|
"or": "vagy",
|
||||||
"preventBinding": "",
|
"preventBinding": "A nyíl ne ragadjon",
|
||||||
"shapes": "",
|
"shapes": "Alakzatok",
|
||||||
"shortcuts": "",
|
"shortcuts": "Gyorsbillentyűk",
|
||||||
"textFinish": "",
|
"textFinish": "Szerkesztés befejezése (szöveg)",
|
||||||
"textNewLine": "",
|
"textNewLine": "Új sor hozzáadása (szöveg)",
|
||||||
"title": "",
|
"title": "Segítség",
|
||||||
"view": "",
|
"view": "Nézet",
|
||||||
"zoomToFit": "",
|
"zoomToFit": "Az összes elem látótérbe hozása",
|
||||||
"zoomToSelection": ""
|
"zoomToSelection": "Kijelölésre nagyítás"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": ""
|
"title": "Rajzvászon alaphelyzetbe"
|
||||||
},
|
},
|
||||||
"publishDialog": {
|
"publishDialog": {
|
||||||
"title": "",
|
"title": "Könyvtár közzététele",
|
||||||
"itemName": "",
|
"itemName": "Tétel neve",
|
||||||
"authorName": "",
|
"authorName": "Szerző neve",
|
||||||
"githubUsername": "",
|
"githubUsername": "GitHub felhasználónév",
|
||||||
"twitterUsername": "",
|
"twitterUsername": "Twitter felhasználónév",
|
||||||
"libraryName": "",
|
"libraryName": "Könyvtár neve",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "Könyvtár leírása",
|
||||||
"website": "",
|
"website": "Weboldal",
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"authorName": "",
|
"authorName": "Neved vagy felhasználóneved",
|
||||||
"libraryName": "",
|
"libraryName": "A könyvtárad neve",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "A könyvtárad használatát segítő leírás",
|
||||||
"githubHandle": "",
|
"githubHandle": "GitHub-handle(opcionális), így szerkesztheted a könyvtárat, miután elküldted ellenőrzésre",
|
||||||
"twitterHandle": "",
|
"twitterHandle": "Twitter-felhasználónév (opcionális), így tudjuk, kinek kell jóváírni a Twitteren keresztüli reklámozást",
|
||||||
"website": ""
|
"website": "Hivatkozás személyes webhelyedre vagy máshová (nem kötelező)"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"required": "",
|
"required": "Kötelező",
|
||||||
"website": ""
|
"website": "Adj meg egy érvényes URL-t"
|
||||||
},
|
},
|
||||||
"noteDescription": {
|
"noteDescription": {
|
||||||
"pre": "",
|
"pre": "Küld be könyvtáradat, hogy bekerüljön a ",
|
||||||
"link": "",
|
"link": "nyilvános könyvtár tárolóba",
|
||||||
"post": ""
|
"post": "hogy mások is felhasználhassák a rajzaikban."
|
||||||
},
|
},
|
||||||
"noteGuidelines": {
|
"noteGuidelines": {
|
||||||
"pre": "",
|
"pre": "A könyvtárat először manuálisan kell jóváhagyni. Kérjük, olvassa el a ",
|
||||||
"link": "",
|
"link": "segédletet",
|
||||||
"post": ""
|
"post": " benyújtása előtt. Szüksége lesz egy GitHub-fiókra a kommunikációhoz és a módosításokhoz, ha kérik, de ez nem feltétlenül szükséges."
|
||||||
},
|
},
|
||||||
"noteLicense": {
|
"noteLicense": {
|
||||||
"pre": "",
|
"pre": "A beküldéssel elfogadja, hogy a könyvtár a következő alatt kerül közzétételre ",
|
||||||
"link": "",
|
"link": "MIT Licensz ",
|
||||||
"post": ""
|
"post": "ami röviden azt jelenti, hogy bárki korlátozás nélkül használhatja őket."
|
||||||
},
|
},
|
||||||
"noteItems": "",
|
"noteItems": "Minden könyvtárelemnek saját nevével kell rendelkeznie, hogy szűrhető legyen. A következő könyvtári tételek kerülnek bele:",
|
||||||
"atleastOneLibItem": ""
|
"atleastOneLibItem": "A kezdéshez válassz ki legalább egy könyvtári elemet"
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "",
|
"title": "A könyvtár beküldve",
|
||||||
"content": "",
|
"content": "Köszönjük {{authorName}}. Könyvtáradat elküldtük felülvizsgálatra. Nyomon követheted az állapotot",
|
||||||
"link": ""
|
"link": "itt"
|
||||||
},
|
},
|
||||||
"confirmDialog": {
|
"confirmDialog": {
|
||||||
"resetLibrary": "",
|
"resetLibrary": "Könyvtár alaphelyzetbe állítása",
|
||||||
"removeItemsFromLib": ""
|
"removeItemsFromLib": "A kiválasztott elemek eltávolítása a könyvtárból"
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "A rajzaidat végpontok közötti titkosítással tároljuk, tehát az Excalidraw szervereiről se tud más belenézni.",
|
"tooltip": "A rajzaidat végpontok közötti titkosítással tároljuk, tehát az Excalidraw szervereiről se tud más belenézni.",
|
||||||
"link": ""
|
"link": "Blogbejegyzés a végpontok közötti titkosításról az Excalidraw-ban"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"angle": "Szög",
|
"angle": "Szög",
|
||||||
@@ -353,66 +355,66 @@
|
|||||||
"storage": "Tárhely",
|
"storage": "Tárhely",
|
||||||
"title": "Statisztikák",
|
"title": "Statisztikák",
|
||||||
"total": "Összesen",
|
"total": "Összesen",
|
||||||
"version": "",
|
"version": "Verzió",
|
||||||
"versionCopy": "",
|
"versionCopy": "Kattints a másoláshoz",
|
||||||
"versionNotAvailable": "",
|
"versionNotAvailable": "A verzió nem elérhető",
|
||||||
"width": "Szélesség"
|
"width": "Szélesség"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"addedToLibrary": "",
|
"addedToLibrary": "Könyvtárhoz adva",
|
||||||
"copyStyles": "",
|
"copyStyles": "Másolt stílusok.",
|
||||||
"copyToClipboard": "",
|
"copyToClipboard": "Vágólapra másolva.",
|
||||||
"copyToClipboardAsPng": "",
|
"copyToClipboardAsPng": "Az {{exportSelection}} PNG formátumban a vágólapra másolva \n({{exportColorScheme}})",
|
||||||
"fileSaved": "",
|
"fileSaved": "Fájl elmentve.",
|
||||||
"fileSavedToFilename": "",
|
"fileSavedToFilename": "Mentve mint {filename}",
|
||||||
"canvas": "",
|
"canvas": "rajzvászon",
|
||||||
"selection": ""
|
"selection": "kijelölés"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"ffffff": "",
|
"ffffff": "Fehér",
|
||||||
"f8f9fa": "",
|
"f8f9fa": "Szürke 0",
|
||||||
"f1f3f5": "",
|
"f1f3f5": "Szürke 1",
|
||||||
"fff5f5": "",
|
"fff5f5": "Piros 0",
|
||||||
"fff0f6": "",
|
"fff0f6": "Pink 0",
|
||||||
"f8f0fc": "",
|
"f8f0fc": "Szőlő 0",
|
||||||
"f3f0ff": "",
|
"f3f0ff": "Ibolya 0",
|
||||||
"edf2ff": "",
|
"edf2ff": "Indigó 0",
|
||||||
"e7f5ff": "",
|
"e7f5ff": "Kék 0",
|
||||||
"e3fafc": "",
|
"e3fafc": "Cián 0",
|
||||||
"e6fcf5": "",
|
"e6fcf5": "Kékes-zöld 0",
|
||||||
"ebfbee": "",
|
"ebfbee": "Zöld 0",
|
||||||
"f4fce3": "",
|
"f4fce3": "Lime 0",
|
||||||
"fff9db": "",
|
"fff9db": "Sárga 0",
|
||||||
"fff4e6": "",
|
"fff4e6": "Narancs 0",
|
||||||
"transparent": "",
|
"transparent": "Átlátszó",
|
||||||
"ced4da": "",
|
"ced4da": "Szürke 4",
|
||||||
"868e96": "",
|
"868e96": "Szürke 6",
|
||||||
"fa5252": "",
|
"fa5252": "Piros 6",
|
||||||
"e64980": "",
|
"e64980": "Pink 6",
|
||||||
"be4bdb": "",
|
"be4bdb": "Szőlő 6",
|
||||||
"7950f2": "",
|
"7950f2": "Ibolya 6",
|
||||||
"4c6ef5": "",
|
"4c6ef5": "Indigó 6",
|
||||||
"228be6": "",
|
"228be6": "Kék 6",
|
||||||
"15aabf": "",
|
"15aabf": "Cián 6",
|
||||||
"12b886": "",
|
"12b886": "Kékes-zöld 6",
|
||||||
"40c057": "",
|
"40c057": "Zöld 6",
|
||||||
"82c91e": "",
|
"82c91e": "Lime 6",
|
||||||
"fab005": "",
|
"fab005": "Sárga 6",
|
||||||
"fd7e14": "",
|
"fd7e14": "Narancs 6",
|
||||||
"000000": "",
|
"000000": "Fekete",
|
||||||
"343a40": "",
|
"343a40": "Szürke 8",
|
||||||
"495057": "",
|
"495057": "Szürke 7",
|
||||||
"c92a2a": "",
|
"c92a2a": "Piros 9",
|
||||||
"a61e4d": "",
|
"a61e4d": "Pink 9",
|
||||||
"862e9c": "",
|
"862e9c": "Szőlő 9",
|
||||||
"5f3dc4": "",
|
"5f3dc4": "Ibolya 9",
|
||||||
"364fc7": "",
|
"364fc7": "Indigó 9",
|
||||||
"1864ab": "",
|
"1864ab": "Kék 9",
|
||||||
"0b7285": "",
|
"0b7285": "Cián 9",
|
||||||
"087f5b": "",
|
"087f5b": "Kékes-zöld 9",
|
||||||
"2b8a3e": "",
|
"2b8a3e": "Zöld 9",
|
||||||
"5c940d": "",
|
"5c940d": "Lime 9",
|
||||||
"e67700": "",
|
"e67700": "Sárga 9",
|
||||||
"d9480f": ""
|
"d9480f": "Narancs 9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Kartunis",
|
"cartoonist": "Kartunis",
|
||||||
"fileTitle": "Nama file",
|
"fileTitle": "Nama file",
|
||||||
"colorPicker": "Pilihan Warna",
|
"colorPicker": "Pilihan Warna",
|
||||||
|
"canvasColors": "Digunakan di kanvas",
|
||||||
"canvasBackground": "Latar Kanvas",
|
"canvasBackground": "Latar Kanvas",
|
||||||
"drawingCanvas": "Kanvas",
|
"drawingCanvas": "Kanvas",
|
||||||
"layers": "Lapisan",
|
"layers": "Lapisan",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Tidak dapat menyisipkan gambar. Coba lagi nanti...",
|
"imageInsertError": "Tidak dapat menyisipkan gambar. Coba lagi nanti...",
|
||||||
"fileTooBig": "File terlalu besar. Ukuran maksimum yang dibolehkan {{maxSize}}.",
|
"fileTooBig": "File terlalu besar. Ukuran maksimum yang dibolehkan {{maxSize}}.",
|
||||||
"svgImageInsertError": "Tidak dapat menyisipkan gambar SVG. Markup SVG sepertinya tidak valid.",
|
"svgImageInsertError": "Tidak dapat menyisipkan gambar SVG. Markup SVG sepertinya tidak valid.",
|
||||||
"invalidSVGString": "SVG tidak valid."
|
"invalidSVGString": "SVG tidak valid.",
|
||||||
|
"cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Pilihan",
|
"selection": "Pilihan",
|
||||||
|
|||||||
+16
-14
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Fumettista",
|
"cartoonist": "Fumettista",
|
||||||
"fileTitle": "Nome del file",
|
"fileTitle": "Nome del file",
|
||||||
"colorPicker": "Selettore colore",
|
"colorPicker": "Selettore colore",
|
||||||
|
"canvasColors": "Usato su tela",
|
||||||
"canvasBackground": "Sfondo tela",
|
"canvasBackground": "Sfondo tela",
|
||||||
"drawingCanvas": "Area di disegno",
|
"drawingCanvas": "Area di disegno",
|
||||||
"layers": "Livelli",
|
"layers": "Livelli",
|
||||||
@@ -103,13 +104,13 @@
|
|||||||
"toggleTheme": "Cambia tema",
|
"toggleTheme": "Cambia tema",
|
||||||
"personalLib": "Libreria Personale",
|
"personalLib": "Libreria Personale",
|
||||||
"excalidrawLib": "Libreria di Excalidraw",
|
"excalidrawLib": "Libreria di Excalidraw",
|
||||||
"decreaseFontSize": "",
|
"decreaseFontSize": "Riduci dimensione dei caratteri",
|
||||||
"increaseFontSize": "",
|
"increaseFontSize": "Aumenta la dimensione dei caratteri",
|
||||||
"unbindText": "",
|
"unbindText": "Scollega testo",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "Modifica link",
|
||||||
"create": "",
|
"create": "Crea link",
|
||||||
"label": ""
|
"label": "Link"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Non è stato possibile inserire l'immagine. Riprova più tardi...",
|
"imageInsertError": "Non è stato possibile inserire l'immagine. Riprova più tardi...",
|
||||||
"fileTooBig": "Il file è troppo grande. La dimensione massima consentita è {{maxSize}}.",
|
"fileTooBig": "Il file è troppo grande. La dimensione massima consentita è {{maxSize}}.",
|
||||||
"svgImageInsertError": "Impossibile inserire l'immagine SVG. Il markup SVG non sembra corretto.",
|
"svgImageInsertError": "Impossibile inserire l'immagine SVG. Il markup SVG non sembra corretto.",
|
||||||
"invalidSVGString": "SVG non valido."
|
"invalidSVGString": "SVG non valido.",
|
||||||
|
"cannotResolveCollabServer": "Impossibile connettersi al server di collab. Ricarica la pagina e riprova."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selezione",
|
"selection": "Selezione",
|
||||||
@@ -193,8 +195,8 @@
|
|||||||
"text": "Testo",
|
"text": "Testo",
|
||||||
"library": "Libreria",
|
"library": "Libreria",
|
||||||
"lock": "Mantieni lo strumento selezionato attivo dopo aver disegnato",
|
"lock": "Mantieni lo strumento selezionato attivo dopo aver disegnato",
|
||||||
"penMode": "",
|
"penMode": "Impedisci il pinch-zoom e accetta l'input di disegno libero solo dalla penna",
|
||||||
"link": ""
|
"link": "Aggiungi/ aggiorna il link per una forma selezionata"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Azioni sulla Tela",
|
"canvasActions": "Azioni sulla Tela",
|
||||||
@@ -214,12 +216,12 @@
|
|||||||
"resizeImage": "Puoi ridimensionare liberamente tenendo premuto SHIFT,\ntieni premuto ALT per ridimensionare dal centro",
|
"resizeImage": "Puoi ridimensionare liberamente tenendo premuto SHIFT,\ntieni premuto ALT per ridimensionare dal centro",
|
||||||
"rotate": "Puoi mantenere gli angoli tenendo premuto SHIFT durante la rotazione",
|
"rotate": "Puoi mantenere gli angoli tenendo premuto SHIFT durante la rotazione",
|
||||||
"lineEditor_info": "Fai doppio click o premi invio per modificare i punti",
|
"lineEditor_info": "Fai doppio click o premi invio per modificare i punti",
|
||||||
"lineEditor_pointSelected": "",
|
"lineEditor_pointSelected": "Premi Elimina per rimuovere il punto(i),\nCtrlOCmd+D per duplicare o trascinare per spostare",
|
||||||
"lineEditor_nothingSelected": "",
|
"lineEditor_nothingSelected": "Seleziona un punto da modificare (tieni premuto MAIUSC per selezionare più punti),\noppure tieni premuto Alt e fai clic per aggiungere nuovi punti",
|
||||||
"placeImage": "Fai click per posizionare l'immagine, o click e trascina per impostarne la dimensione manualmente",
|
"placeImage": "Fai click per posizionare l'immagine, o click e trascina per impostarne la dimensione manualmente",
|
||||||
"publishLibrary": "Pubblica la tua libreria",
|
"publishLibrary": "Pubblica la tua libreria",
|
||||||
"bindTextToElement": "Premi invio per aggiungere il testo",
|
"bindTextToElement": "Premi invio per aggiungere il testo",
|
||||||
"deepBoxSelect": ""
|
"deepBoxSelect": "Tieni premuto CtrlOCmd per selezionare in profondità e per impedire il trascinamento"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Impossibile visualizzare l'anteprima",
|
"cannotShowPreview": "Impossibile visualizzare l'anteprima",
|
||||||
@@ -266,8 +268,8 @@
|
|||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Leggi il nostro blog",
|
"blog": "Leggi il nostro blog",
|
||||||
"click": "click",
|
"click": "click",
|
||||||
"deepSelect": "",
|
"deepSelect": "Selezione profonda",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "Seleziona in profondità all'interno della casella e previene il trascinamento",
|
||||||
"curvedArrow": "Freccia curva",
|
"curvedArrow": "Freccia curva",
|
||||||
"curvedLine": "Linea curva",
|
"curvedLine": "Linea curva",
|
||||||
"documentation": "Documentazione",
|
"documentation": "Documentazione",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "漫画風",
|
"cartoonist": "漫画風",
|
||||||
"fileTitle": "ファイル名",
|
"fileTitle": "ファイル名",
|
||||||
"colorPicker": "色選択",
|
"colorPicker": "色選択",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "キャンバスの背景",
|
"canvasBackground": "キャンバスの背景",
|
||||||
"drawingCanvas": "キャンバスの描画",
|
"drawingCanvas": "キャンバスの描画",
|
||||||
"layers": "レイヤー",
|
"layers": "レイヤー",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "画像を挿入できませんでした。後でもう一度お試しください...",
|
"imageInsertError": "画像を挿入できませんでした。後でもう一度お試しください...",
|
||||||
"fileTooBig": "ファイルが大きすぎます。許可される最大サイズは {{maxSize}} です。",
|
"fileTooBig": "ファイルが大きすぎます。許可される最大サイズは {{maxSize}} です。",
|
||||||
"svgImageInsertError": "SVGイメージを挿入できませんでした。SVGマークアップは無効に見えます。",
|
"svgImageInsertError": "SVGイメージを挿入できませんでした。SVGマークアップは無効に見えます。",
|
||||||
"invalidSVGString": "無効なSVGです。"
|
"invalidSVGString": "無効なSVGです。",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "選択",
|
"selection": "選択",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "",
|
"cartoonist": "",
|
||||||
"fileTitle": "Isem n ufaylu",
|
"fileTitle": "Isem n ufaylu",
|
||||||
"colorPicker": "Amafran n yini",
|
"colorPicker": "Amafran n yini",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Agilal n teɣzut n usuneɣ",
|
"canvasBackground": "Agilal n teɣzut n usuneɣ",
|
||||||
"drawingCanvas": "Taɣzut n usuneɣ",
|
"drawingCanvas": "Taɣzut n usuneɣ",
|
||||||
"layers": "Tissiyin",
|
"layers": "Tissiyin",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "D awezɣi tugra n tugna. Eɛreḍ tikkelt-nniḍen ardeqqal...",
|
"imageInsertError": "D awezɣi tugra n tugna. Eɛreḍ tikkelt-nniḍen ardeqqal...",
|
||||||
"fileTooBig": "Afaylu meqqer aṭas. Tiddi tafellayt yurgen d {{maxSize}}.",
|
"fileTooBig": "Afaylu meqqer aṭas. Tiddi tafellayt yurgen d {{maxSize}}.",
|
||||||
"svgImageInsertError": "D awezɣi tugra n tugna SVG. Acraḍ SVG yettban-d d armeɣtu.",
|
"svgImageInsertError": "D awezɣi tugra n tugna SVG. Acraḍ SVG yettban-d d armeɣtu.",
|
||||||
"invalidSVGString": "SVG armeɣtu."
|
"invalidSVGString": "SVG armeɣtu.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Tafrayt",
|
"selection": "Tafrayt",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "",
|
"cartoonist": "",
|
||||||
"fileTitle": "Файл атауы",
|
"fileTitle": "Файл атауы",
|
||||||
"colorPicker": "",
|
"colorPicker": "",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "",
|
"canvasBackground": "",
|
||||||
"drawingCanvas": "",
|
"drawingCanvas": "",
|
||||||
"layers": "",
|
"layers": "",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Суретті жүктеу мүмкін болмады. Кейінірек қайталап көріңіз...",
|
"imageInsertError": "Суретті жүктеу мүмкін болмады. Кейінірек қайталап көріңіз...",
|
||||||
"fileTooBig": "Файл өте үлкен. Максималды рұқсат етілген көлем {{maxSize}}.",
|
"fileTooBig": "Файл өте үлкен. Максималды рұқсат етілген көлем {{maxSize}}.",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "",
|
"selection": "",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "만화가",
|
"cartoonist": "만화가",
|
||||||
"fileTitle": "파일 이름",
|
"fileTitle": "파일 이름",
|
||||||
"colorPicker": "색상 선택기",
|
"colorPicker": "색상 선택기",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "캔버스 배경",
|
"canvasBackground": "캔버스 배경",
|
||||||
"drawingCanvas": "캔버스 그리기",
|
"drawingCanvas": "캔버스 그리기",
|
||||||
"layers": "레이어",
|
"layers": "레이어",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "이미지를 삽입할 수 없습니다. 나중에 다시 시도 하십시오",
|
"imageInsertError": "이미지를 삽입할 수 없습니다. 나중에 다시 시도 하십시오",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "선택",
|
"selection": "선택",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "",
|
"cartoonist": "",
|
||||||
"fileTitle": "Failo pavadinimas",
|
"fileTitle": "Failo pavadinimas",
|
||||||
"colorPicker": "",
|
"colorPicker": "",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "",
|
"canvasBackground": "",
|
||||||
"drawingCanvas": "",
|
"drawingCanvas": "",
|
||||||
"layers": "",
|
"layers": "",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "",
|
"selection": "",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Karikatūrists",
|
"cartoonist": "Karikatūrists",
|
||||||
"fileTitle": "Datnes nosaukums",
|
"fileTitle": "Datnes nosaukums",
|
||||||
"colorPicker": "Krāsu atlasītājs",
|
"colorPicker": "Krāsu atlasītājs",
|
||||||
|
"canvasColors": "Izmantots tāfelei",
|
||||||
"canvasBackground": "Ainas fons",
|
"canvasBackground": "Ainas fons",
|
||||||
"drawingCanvas": "Tāfele",
|
"drawingCanvas": "Tāfele",
|
||||||
"layers": "Slāņi",
|
"layers": "Slāņi",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Nevarēja ievietot attēlu. Mēģiniet vēlāk...",
|
"imageInsertError": "Nevarēja ievietot attēlu. Mēģiniet vēlāk...",
|
||||||
"fileTooBig": "Datne ir par lielu. Lielākais atļautais izmērs ir {{maxSize}}.",
|
"fileTooBig": "Datne ir par lielu. Lielākais atļautais izmērs ir {{maxSize}}.",
|
||||||
"svgImageInsertError": "Nevarēja ievietot SVG attēlu. Šķiet, ka SVG marķējums nav derīgs.",
|
"svgImageInsertError": "Nevarēja ievietot SVG attēlu. Šķiet, ka SVG marķējums nav derīgs.",
|
||||||
"invalidSVGString": "Nederīgs SVG."
|
"invalidSVGString": "Nederīgs SVG.",
|
||||||
|
"cannotResolveCollabServer": "Nevarēja savienoties ar sadarbošanās serveri. Lūdzu, pārlādējiet lapu un mēģiniet vēlreiz."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Atlase",
|
"selection": "Atlase",
|
||||||
|
|||||||
@@ -0,0 +1,420 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"paste": "चिपकवा",
|
||||||
|
"pasteCharts": "चार्ट चिपकवा",
|
||||||
|
"selectAll": "समस्त निवडा",
|
||||||
|
"multiSelect": "",
|
||||||
|
"moveCanvas": "",
|
||||||
|
"cut": "",
|
||||||
|
"copy": "",
|
||||||
|
"copyAsPng": "",
|
||||||
|
"copyAsSvg": "",
|
||||||
|
"bringForward": "",
|
||||||
|
"sendToBack": "",
|
||||||
|
"bringToFront": "",
|
||||||
|
"sendBackward": "",
|
||||||
|
"delete": "",
|
||||||
|
"copyStyles": "",
|
||||||
|
"pasteStyles": "",
|
||||||
|
"stroke": "",
|
||||||
|
"background": "",
|
||||||
|
"fill": "",
|
||||||
|
"strokeWidth": "",
|
||||||
|
"strokeStyle": "",
|
||||||
|
"strokeStyle_solid": "",
|
||||||
|
"strokeStyle_dashed": "",
|
||||||
|
"strokeStyle_dotted": "",
|
||||||
|
"sloppiness": "",
|
||||||
|
"opacity": "",
|
||||||
|
"textAlign": "",
|
||||||
|
"edges": "",
|
||||||
|
"sharp": "",
|
||||||
|
"round": "",
|
||||||
|
"arrowheads": "",
|
||||||
|
"arrowhead_none": "",
|
||||||
|
"arrowhead_arrow": "",
|
||||||
|
"arrowhead_bar": "",
|
||||||
|
"arrowhead_dot": "",
|
||||||
|
"arrowhead_triangle": "",
|
||||||
|
"fontSize": "",
|
||||||
|
"fontFamily": "",
|
||||||
|
"onlySelected": "",
|
||||||
|
"withBackground": "",
|
||||||
|
"exportEmbedScene": "",
|
||||||
|
"exportEmbedScene_details": "",
|
||||||
|
"addWatermark": "",
|
||||||
|
"handDrawn": "",
|
||||||
|
"normal": "",
|
||||||
|
"code": "",
|
||||||
|
"small": "",
|
||||||
|
"medium": "",
|
||||||
|
"large": "",
|
||||||
|
"veryLarge": "",
|
||||||
|
"solid": "",
|
||||||
|
"hachure": "",
|
||||||
|
"crossHatch": "",
|
||||||
|
"thin": "",
|
||||||
|
"bold": "",
|
||||||
|
"left": "",
|
||||||
|
"center": "",
|
||||||
|
"right": "",
|
||||||
|
"extraBold": "",
|
||||||
|
"architect": "",
|
||||||
|
"artist": "",
|
||||||
|
"cartoonist": "",
|
||||||
|
"fileTitle": "",
|
||||||
|
"colorPicker": "",
|
||||||
|
"canvasColors": "",
|
||||||
|
"canvasBackground": "",
|
||||||
|
"drawingCanvas": "",
|
||||||
|
"layers": "",
|
||||||
|
"actions": "",
|
||||||
|
"language": "",
|
||||||
|
"liveCollaboration": "",
|
||||||
|
"duplicateSelection": "",
|
||||||
|
"untitled": "",
|
||||||
|
"name": "",
|
||||||
|
"yourName": "",
|
||||||
|
"madeWithExcalidraw": "",
|
||||||
|
"group": "",
|
||||||
|
"ungroup": "",
|
||||||
|
"collaborators": "",
|
||||||
|
"showGrid": "",
|
||||||
|
"addToLibrary": "",
|
||||||
|
"removeFromLibrary": "",
|
||||||
|
"libraryLoadingMessage": "",
|
||||||
|
"libraries": "",
|
||||||
|
"loadingScene": "",
|
||||||
|
"align": "",
|
||||||
|
"alignTop": "",
|
||||||
|
"alignBottom": "",
|
||||||
|
"alignLeft": "",
|
||||||
|
"alignRight": "",
|
||||||
|
"centerVertically": "",
|
||||||
|
"centerHorizontally": "",
|
||||||
|
"distributeHorizontally": "",
|
||||||
|
"distributeVertically": "",
|
||||||
|
"flipHorizontal": "",
|
||||||
|
"flipVertical": "",
|
||||||
|
"viewMode": "",
|
||||||
|
"toggleExportColorScheme": "",
|
||||||
|
"share": "",
|
||||||
|
"showStroke": "",
|
||||||
|
"showBackground": "",
|
||||||
|
"toggleTheme": "",
|
||||||
|
"personalLib": "",
|
||||||
|
"excalidrawLib": "",
|
||||||
|
"decreaseFontSize": "",
|
||||||
|
"increaseFontSize": "",
|
||||||
|
"unbindText": "",
|
||||||
|
"link": {
|
||||||
|
"edit": "",
|
||||||
|
"create": "",
|
||||||
|
"label": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"clearReset": "",
|
||||||
|
"exportJSON": "",
|
||||||
|
"exportImage": "",
|
||||||
|
"export": "",
|
||||||
|
"exportToPng": "",
|
||||||
|
"exportToSvg": "",
|
||||||
|
"copyToClipboard": "",
|
||||||
|
"copyPngToClipboard": "",
|
||||||
|
"scale": "",
|
||||||
|
"save": "",
|
||||||
|
"saveAs": "",
|
||||||
|
"load": "",
|
||||||
|
"getShareableLink": "",
|
||||||
|
"close": "",
|
||||||
|
"selectLanguage": "",
|
||||||
|
"scrollBackToContent": "",
|
||||||
|
"zoomIn": "",
|
||||||
|
"zoomOut": "",
|
||||||
|
"resetZoom": "",
|
||||||
|
"menu": "",
|
||||||
|
"done": "",
|
||||||
|
"edit": "",
|
||||||
|
"undo": "",
|
||||||
|
"redo": "",
|
||||||
|
"resetLibrary": "",
|
||||||
|
"createNewRoom": "",
|
||||||
|
"fullScreen": "",
|
||||||
|
"darkMode": "",
|
||||||
|
"lightMode": "",
|
||||||
|
"zenMode": "",
|
||||||
|
"exitZenMode": "",
|
||||||
|
"cancel": "",
|
||||||
|
"clear": "",
|
||||||
|
"remove": "",
|
||||||
|
"publishLibrary": "",
|
||||||
|
"submit": "",
|
||||||
|
"confirm": ""
|
||||||
|
},
|
||||||
|
"alerts": {
|
||||||
|
"clearReset": "",
|
||||||
|
"couldNotCreateShareableLink": "",
|
||||||
|
"couldNotCreateShareableLinkTooBig": "",
|
||||||
|
"couldNotLoadInvalidFile": "",
|
||||||
|
"importBackendFailed": "",
|
||||||
|
"cannotExportEmptyCanvas": "",
|
||||||
|
"couldNotCopyToClipboard": "",
|
||||||
|
"decryptFailed": "",
|
||||||
|
"uploadedSecurly": "",
|
||||||
|
"loadSceneOverridePrompt": "",
|
||||||
|
"collabStopOverridePrompt": "",
|
||||||
|
"errorLoadingLibrary": "",
|
||||||
|
"errorAddingToLibrary": "",
|
||||||
|
"errorRemovingFromLibrary": "",
|
||||||
|
"confirmAddLibrary": "",
|
||||||
|
"imageDoesNotContainScene": "",
|
||||||
|
"cannotRestoreFromImage": "",
|
||||||
|
"invalidSceneUrl": "",
|
||||||
|
"resetLibrary": "",
|
||||||
|
"removeItemsFromsLibrary": "",
|
||||||
|
"invalidEncryptionKey": ""
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"unsupportedFileType": "",
|
||||||
|
"imageInsertError": "",
|
||||||
|
"fileTooBig": "",
|
||||||
|
"svgImageInsertError": "",
|
||||||
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
|
},
|
||||||
|
"toolBar": {
|
||||||
|
"selection": "",
|
||||||
|
"image": "",
|
||||||
|
"rectangle": "",
|
||||||
|
"diamond": "",
|
||||||
|
"ellipse": "",
|
||||||
|
"arrow": "",
|
||||||
|
"line": "",
|
||||||
|
"freedraw": "",
|
||||||
|
"text": "",
|
||||||
|
"library": "",
|
||||||
|
"lock": "",
|
||||||
|
"penMode": "",
|
||||||
|
"link": ""
|
||||||
|
},
|
||||||
|
"headings": {
|
||||||
|
"canvasActions": "",
|
||||||
|
"selectedShapeActions": "",
|
||||||
|
"shapes": ""
|
||||||
|
},
|
||||||
|
"hints": {
|
||||||
|
"canvasPanning": "",
|
||||||
|
"linearElement": "",
|
||||||
|
"freeDraw": "",
|
||||||
|
"text": "",
|
||||||
|
"text_selected": "",
|
||||||
|
"text_editing": "",
|
||||||
|
"linearElementMulti": "",
|
||||||
|
"lockAngle": "",
|
||||||
|
"resize": "",
|
||||||
|
"resizeImage": "",
|
||||||
|
"rotate": "",
|
||||||
|
"lineEditor_info": "",
|
||||||
|
"lineEditor_pointSelected": "",
|
||||||
|
"lineEditor_nothingSelected": "",
|
||||||
|
"placeImage": "",
|
||||||
|
"publishLibrary": "",
|
||||||
|
"bindTextToElement": "",
|
||||||
|
"deepBoxSelect": ""
|
||||||
|
},
|
||||||
|
"canvasError": {
|
||||||
|
"cannotShowPreview": "",
|
||||||
|
"canvasTooBig": "",
|
||||||
|
"canvasTooBigTip": ""
|
||||||
|
},
|
||||||
|
"errorSplash": {
|
||||||
|
"headingMain_pre": "",
|
||||||
|
"headingMain_button": "",
|
||||||
|
"clearCanvasMessage": "",
|
||||||
|
"clearCanvasMessage_button": "",
|
||||||
|
"clearCanvasCaveat": "",
|
||||||
|
"trackedToSentry_pre": "",
|
||||||
|
"trackedToSentry_post": "",
|
||||||
|
"openIssueMessage_pre": "",
|
||||||
|
"openIssueMessage_button": "",
|
||||||
|
"openIssueMessage_post": "",
|
||||||
|
"sceneContent": ""
|
||||||
|
},
|
||||||
|
"roomDialog": {
|
||||||
|
"desc_intro": "",
|
||||||
|
"desc_privacy": "",
|
||||||
|
"button_startSession": "",
|
||||||
|
"button_stopSession": "",
|
||||||
|
"desc_inProgressIntro": "",
|
||||||
|
"desc_shareLink": "",
|
||||||
|
"desc_exitSession": "",
|
||||||
|
"shareTitle": ""
|
||||||
|
},
|
||||||
|
"errorDialog": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"exportDialog": {
|
||||||
|
"disk_title": "",
|
||||||
|
"disk_details": "",
|
||||||
|
"disk_button": "",
|
||||||
|
"link_title": "",
|
||||||
|
"link_details": "",
|
||||||
|
"link_button": "",
|
||||||
|
"excalidrawplus_description": "",
|
||||||
|
"excalidrawplus_button": "",
|
||||||
|
"excalidrawplus_exportError": ""
|
||||||
|
},
|
||||||
|
"helpDialog": {
|
||||||
|
"blog": "",
|
||||||
|
"click": "",
|
||||||
|
"deepSelect": "",
|
||||||
|
"deepBoxSelect": "",
|
||||||
|
"curvedArrow": "",
|
||||||
|
"curvedLine": "",
|
||||||
|
"documentation": "",
|
||||||
|
"doubleClick": "",
|
||||||
|
"drag": "",
|
||||||
|
"editor": "",
|
||||||
|
"editSelectedShape": "",
|
||||||
|
"github": "",
|
||||||
|
"howto": "",
|
||||||
|
"or": "",
|
||||||
|
"preventBinding": "",
|
||||||
|
"shapes": "",
|
||||||
|
"shortcuts": "",
|
||||||
|
"textFinish": "",
|
||||||
|
"textNewLine": "",
|
||||||
|
"title": "",
|
||||||
|
"view": "",
|
||||||
|
"zoomToFit": "",
|
||||||
|
"zoomToSelection": ""
|
||||||
|
},
|
||||||
|
"clearCanvasDialog": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"publishDialog": {
|
||||||
|
"title": "",
|
||||||
|
"itemName": "",
|
||||||
|
"authorName": "",
|
||||||
|
"githubUsername": "",
|
||||||
|
"twitterUsername": "",
|
||||||
|
"libraryName": "",
|
||||||
|
"libraryDesc": "",
|
||||||
|
"website": "",
|
||||||
|
"placeholder": {
|
||||||
|
"authorName": "",
|
||||||
|
"libraryName": "",
|
||||||
|
"libraryDesc": "",
|
||||||
|
"githubHandle": "",
|
||||||
|
"twitterHandle": "",
|
||||||
|
"website": ""
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"required": "",
|
||||||
|
"website": ""
|
||||||
|
},
|
||||||
|
"noteDescription": {
|
||||||
|
"pre": "",
|
||||||
|
"link": "",
|
||||||
|
"post": ""
|
||||||
|
},
|
||||||
|
"noteGuidelines": {
|
||||||
|
"pre": "",
|
||||||
|
"link": "",
|
||||||
|
"post": ""
|
||||||
|
},
|
||||||
|
"noteLicense": {
|
||||||
|
"pre": "",
|
||||||
|
"link": "",
|
||||||
|
"post": ""
|
||||||
|
},
|
||||||
|
"noteItems": "",
|
||||||
|
"atleastOneLibItem": ""
|
||||||
|
},
|
||||||
|
"publishSuccessDialog": {
|
||||||
|
"title": "",
|
||||||
|
"content": "",
|
||||||
|
"link": ""
|
||||||
|
},
|
||||||
|
"confirmDialog": {
|
||||||
|
"resetLibrary": "",
|
||||||
|
"removeItemsFromLib": ""
|
||||||
|
},
|
||||||
|
"encrypted": {
|
||||||
|
"tooltip": "",
|
||||||
|
"link": ""
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"angle": "",
|
||||||
|
"element": "",
|
||||||
|
"elements": "",
|
||||||
|
"height": "",
|
||||||
|
"scene": "",
|
||||||
|
"selected": "",
|
||||||
|
"storage": "",
|
||||||
|
"title": "",
|
||||||
|
"total": "",
|
||||||
|
"version": "",
|
||||||
|
"versionCopy": "",
|
||||||
|
"versionNotAvailable": "",
|
||||||
|
"width": ""
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"addedToLibrary": "",
|
||||||
|
"copyStyles": "",
|
||||||
|
"copyToClipboard": "",
|
||||||
|
"copyToClipboardAsPng": "",
|
||||||
|
"fileSaved": "",
|
||||||
|
"fileSavedToFilename": "",
|
||||||
|
"canvas": "",
|
||||||
|
"selection": ""
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"ffffff": "",
|
||||||
|
"f8f9fa": "",
|
||||||
|
"f1f3f5": "",
|
||||||
|
"fff5f5": "",
|
||||||
|
"fff0f6": "",
|
||||||
|
"f8f0fc": "",
|
||||||
|
"f3f0ff": "",
|
||||||
|
"edf2ff": "",
|
||||||
|
"e7f5ff": "",
|
||||||
|
"e3fafc": "",
|
||||||
|
"e6fcf5": "",
|
||||||
|
"ebfbee": "",
|
||||||
|
"f4fce3": "",
|
||||||
|
"fff9db": "",
|
||||||
|
"fff4e6": "",
|
||||||
|
"transparent": "",
|
||||||
|
"ced4da": "",
|
||||||
|
"868e96": "",
|
||||||
|
"fa5252": "",
|
||||||
|
"e64980": "",
|
||||||
|
"be4bdb": "",
|
||||||
|
"7950f2": "",
|
||||||
|
"4c6ef5": "",
|
||||||
|
"228be6": "",
|
||||||
|
"15aabf": "",
|
||||||
|
"12b886": "",
|
||||||
|
"40c057": "",
|
||||||
|
"82c91e": "",
|
||||||
|
"fab005": "",
|
||||||
|
"fd7e14": "",
|
||||||
|
"000000": "",
|
||||||
|
"343a40": "",
|
||||||
|
"495057": "",
|
||||||
|
"c92a2a": "",
|
||||||
|
"a61e4d": "",
|
||||||
|
"862e9c": "",
|
||||||
|
"5f3dc4": "",
|
||||||
|
"364fc7": "",
|
||||||
|
"1864ab": "",
|
||||||
|
"0b7285": "",
|
||||||
|
"087f5b": "",
|
||||||
|
"2b8a3e": "",
|
||||||
|
"5c940d": "",
|
||||||
|
"e67700": "",
|
||||||
|
"d9480f": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "ကာတွန်း",
|
"cartoonist": "ကာတွန်း",
|
||||||
"fileTitle": "",
|
"fileTitle": "",
|
||||||
"colorPicker": "အရောင်ရွေး",
|
"colorPicker": "အရောင်ရွေး",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "ကားချပ်နောက်ခံ",
|
"canvasBackground": "ကားချပ်နောက်ခံ",
|
||||||
"drawingCanvas": "ပုံဆွဲကားချပ်",
|
"drawingCanvas": "ပုံဆွဲကားချပ်",
|
||||||
"layers": "အလွှာများ",
|
"layers": "အလွှာများ",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "ရွေးချယ်",
|
"selection": "ရွေးချယ်",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Tegner",
|
"cartoonist": "Tegner",
|
||||||
"fileTitle": "Filnavn",
|
"fileTitle": "Filnavn",
|
||||||
"colorPicker": "Fargevelger",
|
"colorPicker": "Fargevelger",
|
||||||
|
"canvasColors": "Brukes på lerretet",
|
||||||
"canvasBackground": "Lerretsbakgrunn",
|
"canvasBackground": "Lerretsbakgrunn",
|
||||||
"drawingCanvas": "Lerret",
|
"drawingCanvas": "Lerret",
|
||||||
"layers": "Lag",
|
"layers": "Lag",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Kunne ikke sette inn bildet. Prøv igjen senere...",
|
"imageInsertError": "Kunne ikke sette inn bildet. Prøv igjen senere...",
|
||||||
"fileTooBig": "Filen er for stor. Maksimal tillatt størrelse er {{maxSize}}.",
|
"fileTooBig": "Filen er for stor. Maksimal tillatt størrelse er {{maxSize}}.",
|
||||||
"svgImageInsertError": "Kunne ikke sette inn SVG-bilde. SVG-koden ser ugyldig ut.",
|
"svgImageInsertError": "Kunne ikke sette inn SVG-bilde. SVG-koden ser ugyldig ut.",
|
||||||
"invalidSVGString": "Ugyldig SVG."
|
"invalidSVGString": "Ugyldig SVG.",
|
||||||
|
"cannotResolveCollabServer": "Kunne ikke koble til samarbeidsserveren. Vennligst oppdater siden og prøv på nytt."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Velg",
|
"selection": "Velg",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Cartoonist",
|
"cartoonist": "Cartoonist",
|
||||||
"fileTitle": "Bestandsnaam",
|
"fileTitle": "Bestandsnaam",
|
||||||
"colorPicker": "Kleurenkiezer",
|
"colorPicker": "Kleurenkiezer",
|
||||||
|
"canvasColors": "Gebruikt op canvas",
|
||||||
"canvasBackground": "Canvas achtergrond",
|
"canvasBackground": "Canvas achtergrond",
|
||||||
"drawingCanvas": "Canvas",
|
"drawingCanvas": "Canvas",
|
||||||
"layers": "Lagen",
|
"layers": "Lagen",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Afbeelding invoegen mislukt. Probeer het later opnieuw...",
|
"imageInsertError": "Afbeelding invoegen mislukt. Probeer het later opnieuw...",
|
||||||
"fileTooBig": "Bestand is te groot. Maximale grootte is {{maxSize}}.",
|
"fileTooBig": "Bestand is te groot. Maximale grootte is {{maxSize}}.",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": "Ongeldige SVG."
|
"invalidSVGString": "Ongeldige SVG.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selectie",
|
"selection": "Selectie",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Teiknar",
|
"cartoonist": "Teiknar",
|
||||||
"fileTitle": "Filnamn",
|
"fileTitle": "Filnamn",
|
||||||
"colorPicker": "Fargeveljar",
|
"colorPicker": "Fargeveljar",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Lerretsbakgrunn",
|
"canvasBackground": "Lerretsbakgrunn",
|
||||||
"drawingCanvas": "Lerret",
|
"drawingCanvas": "Lerret",
|
||||||
"layers": "Lag",
|
"layers": "Lag",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Kunne ikkje sette inn biletet. Prøv igjen seinare...",
|
"imageInsertError": "Kunne ikkje sette inn biletet. Prøv igjen seinare...",
|
||||||
"fileTooBig": "Fila er for stor. Maksimal tillate storleik er {{maxSize}}.",
|
"fileTooBig": "Fila er for stor. Maksimal tillate storleik er {{maxSize}}.",
|
||||||
"svgImageInsertError": "Kunne ikkje sette inn SVG-biletet. SVG-koden ser ugyldig ut.",
|
"svgImageInsertError": "Kunne ikkje sette inn SVG-biletet. SVG-koden ser ugyldig ut.",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Vel",
|
"selection": "Vel",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Dessenhaire",
|
"cartoonist": "Dessenhaire",
|
||||||
"fileTitle": "Nom del fichièr",
|
"fileTitle": "Nom del fichièr",
|
||||||
"colorPicker": "Selector de color",
|
"colorPicker": "Selector de color",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Rèireplan del canabàs",
|
"canvasBackground": "Rèireplan del canabàs",
|
||||||
"drawingCanvas": "Zòna de dessenh",
|
"drawingCanvas": "Zòna de dessenh",
|
||||||
"layers": "Calques",
|
"layers": "Calques",
|
||||||
@@ -107,9 +108,9 @@
|
|||||||
"increaseFontSize": "Aumentar talha poliça",
|
"increaseFontSize": "Aumentar talha poliça",
|
||||||
"unbindText": "",
|
"unbindText": "",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "Modificar lo ligam",
|
||||||
"create": "",
|
"create": "Crear un ligam",
|
||||||
"label": ""
|
"label": "Ligam"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Insercion d’imatge impossibla. Tornatz ensajar mai tard...",
|
"imageInsertError": "Insercion d’imatge impossibla. Tornatz ensajar mai tard...",
|
||||||
"fileTooBig": "Fichièr tròp pesuc. La talha maximala autorizada es {{maxSize}}.",
|
"fileTooBig": "Fichièr tròp pesuc. La talha maximala autorizada es {{maxSize}}.",
|
||||||
"svgImageInsertError": "Insercion d’imatge SVG impossibla. Las balisas SVG semblan invalidas.",
|
"svgImageInsertError": "Insercion d’imatge SVG impossibla. Las balisas SVG semblan invalidas.",
|
||||||
"invalidSVGString": "SVG invalid."
|
"invalidSVGString": "SVG invalid.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Seleccion",
|
"selection": "Seleccion",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "ਕਾਰਟੂਨਿਸਟ",
|
"cartoonist": "ਕਾਰਟੂਨਿਸਟ",
|
||||||
"fileTitle": "ਫਾਈਲ ਦਾ ਨਾਂ",
|
"fileTitle": "ਫਾਈਲ ਦਾ ਨਾਂ",
|
||||||
"colorPicker": "ਰੰਗ ਚੋਣਕਾਰ",
|
"colorPicker": "ਰੰਗ ਚੋਣਕਾਰ",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "ਕੈਨਵਸ ਦਾ ਬੈਕਗਰਾਉਂਡ",
|
"canvasBackground": "ਕੈਨਵਸ ਦਾ ਬੈਕਗਰਾਉਂਡ",
|
||||||
"drawingCanvas": "ਡਰਾਇੰਗ ਕੈਨਵਸ",
|
"drawingCanvas": "ਡਰਾਇੰਗ ਕੈਨਵਸ",
|
||||||
"layers": "ਪਰਤਾਂ",
|
"layers": "ਪਰਤਾਂ",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": "SVG ਨਜਾਇਜ਼ ਹੈ।"
|
"invalidSVGString": "SVG ਨਜਾਇਜ਼ ਹੈ।",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "ਚੋਣਕਾਰ",
|
"selection": "ਚੋਣਕਾਰ",
|
||||||
|
|||||||
@@ -1,46 +1,47 @@
|
|||||||
{
|
{
|
||||||
"ar-SA": 88,
|
"ar-SA": 94,
|
||||||
"bg-BG": 61,
|
"bg-BG": 61,
|
||||||
"bn-BD": 0,
|
"bn-BD": 0,
|
||||||
"ca-ES": 95,
|
"ca-ES": 99,
|
||||||
"cs-CZ": 24,
|
"cs-CZ": 24,
|
||||||
"da-DK": 16,
|
"da-DK": 17,
|
||||||
"de-DE": 99,
|
"de-DE": 99,
|
||||||
"el-GR": 87,
|
"el-GR": 86,
|
||||||
"en": 100,
|
"en": 100,
|
||||||
"es-ES": 84,
|
"es-ES": 99,
|
||||||
"eu-ES": 96,
|
"eu-ES": 99,
|
||||||
"fa-IR": 63,
|
"fa-IR": 63,
|
||||||
"fi-FI": 98,
|
"fi-FI": 98,
|
||||||
"fr-FR": 100,
|
"fr-FR": 99,
|
||||||
"he-IL": 80,
|
"he-IL": 80,
|
||||||
"hi-IN": 55,
|
"hi-IN": 58,
|
||||||
"hu-HU": 49,
|
"hu-HU": 99,
|
||||||
"id-ID": 100,
|
"id-ID": 100,
|
||||||
"it-IT": 96,
|
"it-IT": 100,
|
||||||
"ja-JP": 98,
|
"ja-JP": 97,
|
||||||
"kab-KAB": 95,
|
"kab-KAB": 95,
|
||||||
"kk-KZ": 23,
|
"kk-KZ": 23,
|
||||||
"ko-KR": 72,
|
"ko-KR": 72,
|
||||||
"lt-LT": 24,
|
"lt-LT": 23,
|
||||||
"lv-LV": 100,
|
"lv-LV": 100,
|
||||||
|
"mr-IN": 0,
|
||||||
"my-MM": 46,
|
"my-MM": 46,
|
||||||
"nb-NO": 100,
|
"nb-NO": 100,
|
||||||
"nl-NL": 90,
|
"nl-NL": 90,
|
||||||
"nn-NO": 83,
|
"nn-NO": 83,
|
||||||
"oc-FR": 97,
|
"oc-FR": 97,
|
||||||
"pa-IN": 87,
|
"pa-IN": 86,
|
||||||
"pl-PL": 93,
|
"pl-PL": 93,
|
||||||
"pt-BR": 98,
|
"pt-BR": 99,
|
||||||
"pt-PT": 83,
|
"pt-PT": 83,
|
||||||
"ro-RO": 100,
|
"ro-RO": 100,
|
||||||
"ru-RU": 99,
|
"ru-RU": 99,
|
||||||
"si-LK": 9,
|
"si-LK": 9,
|
||||||
"sk-SK": 100,
|
"sk-SK": 99,
|
||||||
"sv-SE": 100,
|
"sv-SE": 100,
|
||||||
"ta-IN": 99,
|
"ta-IN": 99,
|
||||||
"tr-TR": 85,
|
"tr-TR": 84,
|
||||||
"uk-UA": 82,
|
"uk-UA": 81,
|
||||||
"zh-CN": 100,
|
"zh-CN": 100,
|
||||||
"zh-HK": 28,
|
"zh-HK": 28,
|
||||||
"zh-TW": 100
|
"zh-TW": 100
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Rysunkowy",
|
"cartoonist": "Rysunkowy",
|
||||||
"fileTitle": "Nazwa pliku",
|
"fileTitle": "Nazwa pliku",
|
||||||
"colorPicker": "Paleta kolorów",
|
"colorPicker": "Paleta kolorów",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Kolor dokumentu",
|
"canvasBackground": "Kolor dokumentu",
|
||||||
"drawingCanvas": "Obszar roboczy",
|
"drawingCanvas": "Obszar roboczy",
|
||||||
"layers": "Warstwy",
|
"layers": "Warstwy",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Nie udało się wstawić obrazu. Spróbuj ponownie później...",
|
"imageInsertError": "Nie udało się wstawić obrazu. Spróbuj ponownie później...",
|
||||||
"fileTooBig": "Plik jest zbyt duży. Maksymalny dozwolony rozmiar to {{maxSize}}.",
|
"fileTooBig": "Plik jest zbyt duży. Maksymalny dozwolony rozmiar to {{maxSize}}.",
|
||||||
"svgImageInsertError": "Nie udało się wstawić obrazu SVG. Znacznik SVG wygląda na nieprawidłowy.",
|
"svgImageInsertError": "Nie udało się wstawić obrazu SVG. Znacznik SVG wygląda na nieprawidłowy.",
|
||||||
"invalidSVGString": "Nieprawidłowy SVG."
|
"invalidSVGString": "Nieprawidłowy SVG.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Zaznaczenie",
|
"selection": "Zaznaczenie",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Cartunista",
|
"cartoonist": "Cartunista",
|
||||||
"fileTitle": "Nome do arquivo",
|
"fileTitle": "Nome do arquivo",
|
||||||
"colorPicker": "Seletor de cores",
|
"colorPicker": "Seletor de cores",
|
||||||
|
"canvasColors": "Usado na tela",
|
||||||
"canvasBackground": "Fundo da tela",
|
"canvasBackground": "Fundo da tela",
|
||||||
"drawingCanvas": "Tela de desenho",
|
"drawingCanvas": "Tela de desenho",
|
||||||
"layers": "Camadas",
|
"layers": "Camadas",
|
||||||
@@ -107,9 +108,9 @@
|
|||||||
"increaseFontSize": "Aumentar o tamanho da fonte",
|
"increaseFontSize": "Aumentar o tamanho da fonte",
|
||||||
"unbindText": "Desvincular texto",
|
"unbindText": "Desvincular texto",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "Editar link",
|
||||||
"create": "",
|
"create": "Criar link",
|
||||||
"label": ""
|
"label": "Link"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Não foi possível inserir imagem. Tente novamente mais tarde...",
|
"imageInsertError": "Não foi possível inserir imagem. Tente novamente mais tarde...",
|
||||||
"fileTooBig": "O arquivo é muito grande. O tamanho máximo permitido é {{maxSize}}.",
|
"fileTooBig": "O arquivo é muito grande. O tamanho máximo permitido é {{maxSize}}.",
|
||||||
"svgImageInsertError": "Não foi possível inserir a imagem SVG. A marcação SVG parece inválida.",
|
"svgImageInsertError": "Não foi possível inserir a imagem SVG. A marcação SVG parece inválida.",
|
||||||
"invalidSVGString": "SVG Inválido."
|
"invalidSVGString": "SVG Inválido.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Seleção",
|
"selection": "Seleção",
|
||||||
@@ -194,7 +196,7 @@
|
|||||||
"library": "Biblioteca",
|
"library": "Biblioteca",
|
||||||
"lock": "Manter ativa a ferramenta selecionada após desenhar",
|
"lock": "Manter ativa a ferramenta selecionada após desenhar",
|
||||||
"penMode": "Prevenir a ação de tocar-ampliar e permitir apenas interações da caneta",
|
"penMode": "Prevenir a ação de tocar-ampliar e permitir apenas interações da caneta",
|
||||||
"link": ""
|
"link": "Adicionar/Atualizar link para uma forma selecionada"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Ações da tela",
|
"canvasActions": "Ações da tela",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Caricaturista",
|
"cartoonist": "Caricaturista",
|
||||||
"fileTitle": "Nome do ficheiro",
|
"fileTitle": "Nome do ficheiro",
|
||||||
"colorPicker": "Seletor de cores",
|
"colorPicker": "Seletor de cores",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Fundo da área de desenho",
|
"canvasBackground": "Fundo da área de desenho",
|
||||||
"drawingCanvas": "Área de desenho",
|
"drawingCanvas": "Área de desenho",
|
||||||
"layers": "Camadas",
|
"layers": "Camadas",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Não foi possível inserir a imagem, tente novamente mais tarde...",
|
"imageInsertError": "Não foi possível inserir a imagem, tente novamente mais tarde...",
|
||||||
"fileTooBig": "O ficheiro é muito grande. O tamanho máximo permitido é {{maxSize}}.",
|
"fileTooBig": "O ficheiro é muito grande. O tamanho máximo permitido é {{maxSize}}.",
|
||||||
"svgImageInsertError": "Não foi possível inserir a imagem SVG. A marcação SVG parece inválida.",
|
"svgImageInsertError": "Não foi possível inserir a imagem SVG. A marcação SVG parece inválida.",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Seleção",
|
"selection": "Seleção",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Caricaturist",
|
"cartoonist": "Caricaturist",
|
||||||
"fileTitle": "Nume de fișier",
|
"fileTitle": "Nume de fișier",
|
||||||
"colorPicker": "Selector de culoare",
|
"colorPicker": "Selector de culoare",
|
||||||
|
"canvasColors": "Folosite pe pânză",
|
||||||
"canvasBackground": "Fundalul pânzei",
|
"canvasBackground": "Fundalul pânzei",
|
||||||
"drawingCanvas": "Pânză pentru desenat",
|
"drawingCanvas": "Pânză pentru desenat",
|
||||||
"layers": "Straturi",
|
"layers": "Straturi",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Imaginea nu a putut fi introdusă. Reîncearcă mai târziu...",
|
"imageInsertError": "Imaginea nu a putut fi introdusă. Reîncearcă mai târziu...",
|
||||||
"fileTooBig": "Fișierul este prea mare. Dimensiunea maximă permisă este de {{maxSize}}.",
|
"fileTooBig": "Fișierul este prea mare. Dimensiunea maximă permisă este de {{maxSize}}.",
|
||||||
"svgImageInsertError": "Imaginea SVG nu a putut fi introdus. Marcajul SVG pare invalid.",
|
"svgImageInsertError": "Imaginea SVG nu a putut fi introdus. Marcajul SVG pare invalid.",
|
||||||
"invalidSVGString": "SVG invalid."
|
"invalidSVGString": "SVG invalid.",
|
||||||
|
"cannotResolveCollabServer": "Nu a putut fi realizată conexiunea la serverul de colaborare. Reîncarcă pagina și încearcă din nou."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selecție",
|
"selection": "Selecție",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Карикатурист",
|
"cartoonist": "Карикатурист",
|
||||||
"fileTitle": "Имя файла",
|
"fileTitle": "Имя файла",
|
||||||
"colorPicker": "Выбор цвета",
|
"colorPicker": "Выбор цвета",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Фон холста",
|
"canvasBackground": "Фон холста",
|
||||||
"drawingCanvas": "Полотно",
|
"drawingCanvas": "Полотно",
|
||||||
"layers": "Слои",
|
"layers": "Слои",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Не удалось вставить изображение. Попробуйте позже...",
|
"imageInsertError": "Не удалось вставить изображение. Попробуйте позже...",
|
||||||
"fileTooBig": "Очень большой файл. Максимально разрешенный размер {{maxSize}}.",
|
"fileTooBig": "Очень большой файл. Максимально разрешенный размер {{maxSize}}.",
|
||||||
"svgImageInsertError": "Не удалось вставить изображение SVG. Разметка SVG выглядит недействительной.",
|
"svgImageInsertError": "Не удалось вставить изображение SVG. Разметка SVG выглядит недействительной.",
|
||||||
"invalidSVGString": "Некорректный SVG."
|
"invalidSVGString": "Некорректный SVG.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Выделение области",
|
"selection": "Выделение области",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "සැකිලිරූකරු",
|
"cartoonist": "සැකිලිරූකරු",
|
||||||
"fileTitle": "ගොනු නාමය",
|
"fileTitle": "ගොනු නාමය",
|
||||||
"colorPicker": "පාට තෝරකය",
|
"colorPicker": "පාට තෝරකය",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "කැන්වස පසුබිම",
|
"canvasBackground": "කැන්වස පසුබිම",
|
||||||
"drawingCanvas": "චිත්රක කැන්වසය",
|
"drawingCanvas": "චිත්රක කැන්වසය",
|
||||||
"layers": "ලේයර",
|
"layers": "ලේයර",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": ""
|
"invalidSVGString": "",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "",
|
"selection": "",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Ilustrátor",
|
"cartoonist": "Ilustrátor",
|
||||||
"fileTitle": "Názov súboru",
|
"fileTitle": "Názov súboru",
|
||||||
"colorPicker": "Výber farby",
|
"colorPicker": "Výber farby",
|
||||||
|
"canvasColors": "Použité na plátne",
|
||||||
"canvasBackground": "Pozadie plátna",
|
"canvasBackground": "Pozadie plátna",
|
||||||
"drawingCanvas": "Kresliace plátno",
|
"drawingCanvas": "Kresliace plátno",
|
||||||
"layers": "Vrstvy",
|
"layers": "Vrstvy",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Nepodarilo sa vložiť obrázok. Skúste to znova neskôr...",
|
"imageInsertError": "Nepodarilo sa vložiť obrázok. Skúste to znova neskôr...",
|
||||||
"fileTooBig": "Súbor je príliš veľký. Maximálna povolená veľkosť je {{maxSize}}.",
|
"fileTooBig": "Súbor je príliš veľký. Maximálna povolená veľkosť je {{maxSize}}.",
|
||||||
"svgImageInsertError": "Nepodarilo sa vložiť SVG obrázok. SVG formát je pravdepodobne nevalidný.",
|
"svgImageInsertError": "Nepodarilo sa vložiť SVG obrázok. SVG formát je pravdepodobne nevalidný.",
|
||||||
"invalidSVGString": "Nevalidné SVG."
|
"invalidSVGString": "Nevalidné SVG.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Výber",
|
"selection": "Výber",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Serietecknare",
|
"cartoonist": "Serietecknare",
|
||||||
"fileTitle": "Filnamn",
|
"fileTitle": "Filnamn",
|
||||||
"colorPicker": "Färgväljare",
|
"colorPicker": "Färgväljare",
|
||||||
|
"canvasColors": "Används på canvas",
|
||||||
"canvasBackground": "Canvas-bakgrund",
|
"canvasBackground": "Canvas-bakgrund",
|
||||||
"drawingCanvas": "Ritar canvas",
|
"drawingCanvas": "Ritar canvas",
|
||||||
"layers": "Lager",
|
"layers": "Lager",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Kunde inte infoga bild. Försök igen senare...",
|
"imageInsertError": "Kunde inte infoga bild. Försök igen senare...",
|
||||||
"fileTooBig": "Filen är för stor. Maximal tillåten storlek är {{maxSize}}.",
|
"fileTooBig": "Filen är för stor. Maximal tillåten storlek är {{maxSize}}.",
|
||||||
"svgImageInsertError": "Kunde inte infoga SVG-bild. SVG-koden ser ogiltig ut.",
|
"svgImageInsertError": "Kunde inte infoga SVG-bild. SVG-koden ser ogiltig ut.",
|
||||||
"invalidSVGString": "Ogiltig SVG."
|
"invalidSVGString": "Ogiltig SVG.",
|
||||||
|
"cannotResolveCollabServer": "Det gick inte att ansluta till samarbets-servern. Ladda om sidan och försök igen."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Markering",
|
"selection": "Markering",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "கேலிச்சித்திர ஓவியர்",
|
"cartoonist": "கேலிச்சித்திர ஓவியர்",
|
||||||
"fileTitle": "கோப்புப் பெயர்",
|
"fileTitle": "கோப்புப் பெயர்",
|
||||||
"colorPicker": "நிறத் தேர்வி",
|
"colorPicker": "நிறத் தேர்வி",
|
||||||
|
"canvasColors": "கித்தானில் பயன்படுத்தப்பட்டது",
|
||||||
"canvasBackground": "கித்தான் பின்னணி",
|
"canvasBackground": "கித்தான் பின்னணி",
|
||||||
"drawingCanvas": "கித்தான் வரைகிறது",
|
"drawingCanvas": "கித்தான் வரைகிறது",
|
||||||
"layers": "அடுக்குகள்",
|
"layers": "அடுக்குகள்",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "படத்தைப் புகுத்தவியலா. பிறகு மீண்டும் முயலவும்...",
|
"imageInsertError": "படத்தைப் புகுத்தவியலா. பிறகு மீண்டும் முயலவும்...",
|
||||||
"fileTooBig": "கோப்பு மிகப்பெரிது. அனுமதிக்கப்பட்ட அதிகபட்ச அளவு {{maxSize}}.",
|
"fileTooBig": "கோப்பு மிகப்பெரிது. அனுமதிக்கப்பட்ட அதிகபட்ச அளவு {{maxSize}}.",
|
||||||
"svgImageInsertError": "எஸ்விஜி படத்தைப் புகுத்தவியலா. எஸ்விஜியின் மார்க்அப் செல்லாததாக தெரிகிறது.",
|
"svgImageInsertError": "எஸ்விஜி படத்தைப் புகுத்தவியலா. எஸ்விஜியின் மார்க்அப் செல்லாததாக தெரிகிறது.",
|
||||||
"invalidSVGString": "செல்லாத SVG."
|
"invalidSVGString": "செல்லாத SVG.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "தெரிவு",
|
"selection": "தெரிவு",
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
"cartoonist": "Karikatürist",
|
"cartoonist": "Karikatürist",
|
||||||
"fileTitle": "Dosya adı",
|
"fileTitle": "Dosya adı",
|
||||||
"colorPicker": "Renk seçici",
|
"colorPicker": "Renk seçici",
|
||||||
|
"canvasColors": "",
|
||||||
"canvasBackground": "Tuval arka planı",
|
"canvasBackground": "Tuval arka planı",
|
||||||
"drawingCanvas": "Çizim tuvali",
|
"drawingCanvas": "Çizim tuvali",
|
||||||
"layers": "Katmanlar",
|
"layers": "Katmanlar",
|
||||||
@@ -179,7 +180,8 @@
|
|||||||
"imageInsertError": "Görsel eklenemedi. Daha sonra tekrar deneyin...",
|
"imageInsertError": "Görsel eklenemedi. Daha sonra tekrar deneyin...",
|
||||||
"fileTooBig": "Dosya çok büyük. İzin verilen maksimum boyut {{maxSize}}.",
|
"fileTooBig": "Dosya çok büyük. İzin verilen maksimum boyut {{maxSize}}.",
|
||||||
"svgImageInsertError": "SVG resmi eklenemedi. SVG işaretlemesi geçersiz görünüyor.",
|
"svgImageInsertError": "SVG resmi eklenemedi. SVG işaretlemesi geçersiz görünüyor.",
|
||||||
"invalidSVGString": "Geçersiz SVG."
|
"invalidSVGString": "Geçersiz SVG.",
|
||||||
|
"cannotResolveCollabServer": ""
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Seçme",
|
"selection": "Seçme",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user