Compare commits

..

3 Commits

Author SHA1 Message Date
dwelle 75e2d9e359 remove debug 2021-11-22 23:20:40 +01:00
dwelle 6592517122 import lazily 2021-11-22 22:11:10 +01:00
dwelle bd953a6287 feat: compress non-transparent PNGs as JPGs and allow larger dimensions 2021-11-22 17:39:59 +01:00
157 changed files with 2236 additions and 7276 deletions
View File
-1
View File
@@ -1,2 +1 @@
#!/bin/sh
yarn lint-staged yarn lint-staged
+10 -11
View File
@@ -21,15 +21,15 @@
"dependencies": { "dependencies": {
"@sentry/browser": "6.2.5", "@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5", "@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.15.1", "@testing-library/jest-dom": "5.15.0",
"@testing-library/react": "12.1.2", "@testing-library/react": "12.1.2",
"@tldraw/vec": "1.1.5", "@tldraw/vec": "0.1.3",
"@types/jest": "27.0.3", "@types/jest": "27.0.2",
"@types/pica": "5.1.3", "@types/pica": "5.1.3",
"@types/react": "17.0.37", "@types/react": "17.0.34",
"@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.21.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",
@@ -49,8 +49,8 @@
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"roughjs": "4.5.2", "roughjs": "4.5.0",
"sass": "1.43.5", "sass": "1.43.4",
"socket.io-client": "2.3.1", "socket.io-client": "2.3.1",
"typescript": "4.5.2" "typescript": "4.5.2"
}, },
@@ -62,15 +62,14 @@
"@types/pako": "1.0.2", "@types/pako": "1.0.2",
"@types/resize-observer-browser": "0.1.6", "@types/resize-observer-browser": "0.1.6",
"chai": "4.3.4", "chai": "4.3.4",
"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", "firebase-tools": "9.22.0",
"husky": "7.0.4", "husky": "7.0.4",
"jest-canvas-mock": "2.3.1", "jest-canvas-mock": "2.3.1",
"lint-staged": "12.1.2", "lint-staged": "12.0.1",
"pepjs": "0.5.3", "pepjs": "0.5.3",
"prettier": "2.5.0", "prettier": "2.4.1",
"rewire": "5.0.0" "rewire": "5.0.0"
}, },
"resolutions": { "resolutions": {
+5 -7
View File
@@ -8,12 +8,7 @@ import { t } from "../i18n";
export const actionAddToLibrary = register({ export const actionAddToLibrary = register({
name: "addToLibrary", name: "addToLibrary",
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( if (elements.some((element) => element.type === "image")) {
getNonDeletedElements(elements),
appState,
true,
);
if (selectedElements.some((element) => element.type === "image")) {
return { return {
commitToHistory: false, commitToHistory: false,
appState: { appState: {
@@ -30,7 +25,10 @@ export const actionAddToLibrary = register({
{ {
id: randomId(), id: randomId(),
status: "unpublished", status: "unpublished",
elements: selectedElements.map(deepCopyElement), elements: getSelectedElements(
getNonDeletedElements(elements),
appState,
).map(deepCopyElement),
created: Date.now(), created: Date.now(),
}, },
...items, ...items,
+4 -6
View File
@@ -8,13 +8,13 @@ import {
CenterVerticallyIcon, CenterVerticallyIcon,
} from "../components/icons"; } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { getNonDeletedElements } from "../element"; import { getElementMap, getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { KEYS } from "../keys"; import { 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 { getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
const enableActionGroup = ( const enableActionGroup = (
@@ -34,11 +34,9 @@ const alignSelectedElements = (
const updatedElements = alignElements(selectedElements, alignment); const updatedElements = alignElements(selectedElements, alignment);
const updatedElementsMap = arrayToMap(updatedElements); const updatedElementsMap = getElementMap(updatedElements);
return elements.map( return elements.map((element) => updatedElementsMap[element.id] || element);
(element) => updatedElementsMap.get(element.id) || element,
);
}; };
export const actionAlignTop = register({ export const actionAlignTop = register({
+1 -1
View File
@@ -160,7 +160,7 @@ export const actionResetZoom = register({
}; };
}, },
PanelComponent: ({ updateData, appState }) => ( PanelComponent: ({ updateData, appState }) => (
<Tooltip label={t("buttons.resetZoom")} style={{ height: "100%" }}> <Tooltip label={t("buttons.resetZoom")}>
<ToolButton <ToolButton
type="button" type="button"
className="reset-zoom-button" className="reset-zoom-button"
-2
View File
@@ -42,7 +42,6 @@ export const actionCopyAsSvg = register({
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
true,
); );
try { try {
await exportCanvas( await exportCanvas(
@@ -82,7 +81,6 @@ export const actionCopyAsPng = register({
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
true,
); );
try { try {
await exportCanvas( await exportCanvas(
+12 -22
View File
@@ -11,7 +11,6 @@ import { newElementWith } from "../element/mutateElement";
import { getElementsInGroup } from "../groups"; import { getElementsInGroup } from "../groups";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { fixBindingsAfterDeletion } from "../element/binding"; import { fixBindingsAfterDeletion } from "../element/binding";
import { isBoundToContainer } from "../element/typeChecks";
const deleteSelectedElements = ( const deleteSelectedElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@@ -22,12 +21,6 @@ const deleteSelectedElements = (
if (appState.selectedElementIds[el.id]) { if (appState.selectedElementIds[el.id]) {
return newElementWith(el, { isDeleted: true }); return newElementWith(el, { isDeleted: true });
} }
if (
isBoundToContainer(el) &&
appState.selectedElementIds[el.containerId]
) {
return newElementWith(el, { isDeleted: true });
}
return el; return el;
}), }),
appState: { appState: {
@@ -62,7 +55,7 @@ export const actionDeleteSelected = register({
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const { const {
elementId, elementId,
selectedPointsIndices, activePointIndex,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
} = appState.editingLinearElement; } = appState.editingLinearElement;
@@ -72,7 +65,8 @@ export const actionDeleteSelected = register({
} }
if ( if (
// case: no point selected → delete whole element // case: no point selected → delete whole element
selectedPointsIndices == null || activePointIndex == null ||
activePointIndex === -1 ||
// case: deleting last remaining point // case: deleting last remaining point
element.points.length < 2 element.points.length < 2
) { ) {
@@ -92,17 +86,15 @@ export const actionDeleteSelected = register({
// We cannot do this inside `movePoint` because it is also called // We cannot do this inside `movePoint` because it is also called
// when deleting the uncommitted point (which hasn't caused any binding) // when deleting the uncommitted point (which hasn't caused any binding)
const binding = { const binding = {
startBindingElement: selectedPointsIndices?.includes(0) startBindingElement:
? null activePointIndex === 0 ? null : startBindingElement,
: startBindingElement, endBindingElement:
endBindingElement: selectedPointsIndices?.includes( activePointIndex === element.points.length - 1
element.points.length - 1, ? null
) : endBindingElement,
? null
: endBindingElement,
}; };
LinearElementEditor.deletePoints(element, selectedPointsIndices); LinearElementEditor.movePoint(element, activePointIndex, "delete");
return { return {
elements, elements,
@@ -111,15 +103,13 @@ export const actionDeleteSelected = register({
editingLinearElement: { editingLinearElement: {
...appState.editingLinearElement, ...appState.editingLinearElement,
...binding, ...binding,
selectedPointsIndices: activePointIndex: activePointIndex > 0 ? activePointIndex - 1 : 0,
selectedPointsIndices?.[0] > 0
? [selectedPointsIndices[0] - 1]
: [0],
}, },
}, },
commitToHistory: true, commitToHistory: true,
}; };
} }
let { elements: nextElements, appState: nextAppState } = let { elements: nextElements, appState: nextAppState } =
deleteSelectedElements(elements, appState); deleteSelectedElements(elements, appState);
fixBindingsAfterDeletion( fixBindingsAfterDeletion(
+4 -6
View File
@@ -4,13 +4,13 @@ import {
} from "../components/icons"; } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { distributeElements, Distribution } from "../disitrubte"; import { distributeElements, Distribution } from "../disitrubte";
import { getNonDeletedElements } from "../element"; import { getElementMap, 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 } 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 { getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
const enableActionGroup = ( const enableActionGroup = (
@@ -30,11 +30,9 @@ const distributeSelectedElements = (
const updatedElements = distributeElements(selectedElements, distribution); const updatedElements = distributeElements(selectedElements, distribution);
const updatedElementsMap = arrayToMap(updatedElements); const updatedElementsMap = getElementMap(updatedElements);
return elements.map( return elements.map((element) => updatedElementsMap[element.id] || element);
(element) => updatedElementsMap.get(element.id) || element,
);
}; };
export const distributeHorizontally = register({ export const distributeHorizontally = register({
+32 -22
View File
@@ -2,12 +2,13 @@ import { KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { duplicateElement, getNonDeletedElements } from "../element"; import { duplicateElement, getNonDeletedElements } from "../element";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { clone } from "../components/icons"; import { clone } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
import { arrayToMap, getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { mutateElement } from "../element/mutateElement";
import { import {
selectGroupsForSelectedElements, selectGroupsForSelectedElements,
getSelectedGroupForElement, getSelectedGroupForElement,
@@ -17,23 +18,41 @@ import { AppState } from "../types";
import { fixBindingsAfterDuplication } from "../element/binding"; import { fixBindingsAfterDuplication } from "../element/binding";
import { ActionResult } from "./types"; import { ActionResult } from "./types";
import { GRID_SIZE } from "../constants"; import { GRID_SIZE } from "../constants";
import { bindTextToShapeAfterDuplication } from "../element/textElement";
import { isBoundToContainer } from "../element/typeChecks";
export const actionDuplicateSelection = register({ export const actionDuplicateSelection = register({
name: "duplicateSelection", name: "duplicateSelection",
perform: (elements, appState) => { perform: (elements, appState) => {
// duplicate selected point(s) if editing a line // duplicate point if selected while editing multi-point element
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const ret = LinearElementEditor.duplicateSelectedPoints(appState); const { activePointIndex, elementId } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (!ret) { if (!element || activePointIndex === null) {
return false; return false;
} }
const { points } = element;
const selectedPoint = points[activePointIndex];
const nextPoint = points[activePointIndex + 1];
mutateElement(element, {
points: [
...points.slice(0, activePointIndex + 1),
nextPoint
? [
(selectedPoint[0] + nextPoint[0]) / 2,
(selectedPoint[1] + nextPoint[1]) / 2,
]
: [selectedPoint[0] + 30, selectedPoint[1] + 30],
...points.slice(activePointIndex + 1),
],
});
return { return {
appState: {
...appState,
editingLinearElement: {
...appState.editingLinearElement,
activePointIndex: activePointIndex + 1,
},
},
elements, elements,
appState: ret.appState,
commitToHistory: true, commitToHistory: true,
}; };
} }
@@ -87,12 +106,9 @@ const duplicateElements = (
const finalElements: ExcalidrawElement[] = []; const finalElements: ExcalidrawElement[] = [];
let index = 0; let index = 0;
const selectedElementIds = arrayToMap(
getSelectedElements(elements, appState, true),
);
while (index < elements.length) { while (index < elements.length) {
const element = elements[index]; const element = elements[index];
if (selectedElementIds.get(element.id)) { if (appState.selectedElementIds[element.id]) {
if (element.groupIds.length) { if (element.groupIds.length) {
const groupId = getSelectedGroupForElement(appState, element); const groupId = getSelectedGroupForElement(appState, element);
// if group selected, duplicate it atomically // if group selected, duplicate it atomically
@@ -114,11 +130,7 @@ const duplicateElements = (
} }
index++; index++;
} }
bindTextToShapeAfterDuplication(
finalElements,
oldElements,
oldIdToDuplicatedId,
);
fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId); fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId);
return { return {
@@ -128,9 +140,7 @@ const duplicateElements = (
...appState, ...appState,
selectedGroupIds: {}, selectedGroupIds: {},
selectedElementIds: newElements.reduce((acc, element) => { selectedElementIds: newElements.reduce((acc, element) => {
if (!isBoundToContainer(element)) { acc[element.id] = true;
acc[element.id] = true;
}
return acc; return acc;
}, {} as any), }, {} as any),
}, },
+1 -52
View File
@@ -14,60 +14,10 @@ import {
bindOrUnbindLinearElement, bindOrUnbindLinearElement,
} from "../element/binding"; } from "../element/binding";
import { isBindingElement } from "../element/typeChecks"; import { isBindingElement } from "../element/typeChecks";
import { ExcalidrawImageElement } from "../element/types";
import { imageFromImageData } from "../element/image";
export const actionFinalize = register({ export const actionFinalize = register({
name: "finalize", name: "finalize",
perform: ( perform: (elements, appState, _, { canvas, focusContainer }) => {
elements,
appState,
_,
{ canvas, focusContainer, imageCache, addFiles },
) => {
if (appState.editingImageElement) {
const { elementId, imageData } = appState.editingImageElement;
const editingImageElement = elements.find((el) => el.id === elementId) as
| ExcalidrawImageElement
| undefined;
if (editingImageElement?.fileId) {
const cachedImageData = imageCache.get(editingImageElement.fileId);
if (cachedImageData) {
const { image, dataURL } = imageFromImageData(imageData);
imageCache.set(editingImageElement.fileId, {
...cachedImageData,
image,
});
addFiles([
{
id: editingImageElement.fileId,
dataURL,
mimeType: cachedImageData.mimeType,
created: Date.now(),
},
]);
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
}
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const { elementId, startBindingElement, endBindingElement } = const { elementId, startBindingElement, endBindingElement } =
appState.editingLinearElement; appState.editingLinearElement;
@@ -212,7 +162,6 @@ export const actionFinalize = register({
keyTest: (event, appState) => keyTest: (event, appState) =>
(event.key === KEYS.ESCAPE && (event.key === KEYS.ESCAPE &&
(appState.editingLinearElement !== null || (appState.editingLinearElement !== null ||
appState.editingImageElement !== null ||
(!appState.draggingElement && appState.multiElement === null))) || (!appState.draggingElement && appState.multiElement === null))) ||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
appState.multiElement !== null), appState.multiElement !== null),
+7 -9
View File
@@ -1,6 +1,6 @@
import { register } from "./register"; import { register } from "./register";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element"; import { getElementMap, getNonDeletedElements } from "../element";
import { mutateElement } from "../element/mutateElement"; import { mutateElement } from "../element/mutateElement";
import { ExcalidrawElement, NonDeleted } from "../element/types"; import { ExcalidrawElement, NonDeleted } from "../element/types";
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements"; import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
@@ -9,7 +9,6 @@ import { getTransformHandles } from "../element/transformHandles";
import { isFreeDrawElement, isLinearElement } from "../element/typeChecks"; import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
import { updateBoundElements } from "../element/binding"; import { updateBoundElements } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { arrayToMap } from "../utils";
const enableActionFlipHorizontal = ( const enableActionFlipHorizontal = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@@ -84,11 +83,9 @@ const flipSelectedElements = (
flipDirection, flipDirection,
); );
const updatedElementsMap = arrayToMap(updatedElements); const updatedElementsMap = getElementMap(updatedElements);
return elements.map( return elements.map((element) => updatedElementsMap[element.id] || element);
(element) => updatedElementsMap.get(element.id) || element,
);
}; };
const flipElements = ( const flipElements = (
@@ -145,9 +142,10 @@ const flipElement = (
} }
if (isLinearElement(element)) { if (isLinearElement(element)) {
for (let index = 1; index < element.points.length; index++) { for (let i = 1; i < element.points.length; i++) {
LinearElementEditor.movePoints(element, [ LinearElementEditor.movePoint(element, i, [
{ index, point: [-element.points[index][0], element.points[index][1]] }, -element.points[i][0],
element.points[i][1],
]); ]);
} }
LinearElementEditor.normalizePoints(element); LinearElementEditor.normalizePoints(element);
+2 -5
View File
@@ -1,6 +1,6 @@
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { t } from "../i18n"; import { t } from "../i18n";
import { arrayToMap, getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { UngroupIcon, GroupIcon } from "../components/icons"; import { UngroupIcon, GroupIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
@@ -44,7 +44,6 @@ const enableActionGroup = (
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
true,
); );
return ( return (
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements) selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
@@ -57,7 +56,6 @@ export const actionGroup = register({
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
true,
); );
if (selectedElements.length < 2) { if (selectedElements.length < 2) {
// nothing to group // nothing to group
@@ -85,9 +83,8 @@ export const actionGroup = register({
} }
} }
const newGroupId = randomId(); const newGroupId = randomId();
const selectElementIds = arrayToMap(selectedElements);
const updatedElements = elements.map((element) => { const updatedElements = elements.map((element) => {
if (!selectElementIds.get(element.id)) { if (!appState.selectedElementIds[element.id]) {
return element; return element;
} }
return newElementWith(element, { return newElementWith(element, {
+5 -5
View File
@@ -6,9 +6,9 @@ import History, { HistoryEntry } from "../history";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types"; import { AppState } from "../types";
import { isWindows, KEYS } from "../keys"; import { isWindows, KEYS } from "../keys";
import { getElementMap } from "../element";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { fixBindingsAfterDeletion } from "../element/binding"; import { fixBindingsAfterDeletion } from "../element/binding";
import { arrayToMap } from "../utils";
const writeData = ( const writeData = (
prevElements: readonly ExcalidrawElement[], prevElements: readonly ExcalidrawElement[],
@@ -27,17 +27,17 @@ const writeData = (
return { commitToHistory }; return { commitToHistory };
} }
const prevElementMap = arrayToMap(prevElements); const prevElementMap = getElementMap(prevElements);
const nextElements = data.elements; const nextElements = data.elements;
const nextElementMap = arrayToMap(nextElements); const nextElementMap = getElementMap(nextElements);
const deletedElements = prevElements.filter( const deletedElements = prevElements.filter(
(prevElement) => !nextElementMap.has(prevElement.id), (prevElement) => !nextElementMap.hasOwnProperty(prevElement.id),
); );
const elements = nextElements const elements = nextElements
.map((nextElement) => .map((nextElement) =>
newElementWith( newElementWith(
prevElementMap.get(nextElement.id) || nextElement, prevElementMap[nextElement.id] || nextElement,
nextElement, nextElement,
), ),
) )
-75
View File
@@ -1,75 +0,0 @@
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton";
import { backgroundIcon } from "../components/icons";
import { register } from "./register";
import { getNonDeletedElements } from "../element";
import { isInitializedImageElement } from "../element/typeChecks";
import Scene from "../scene/Scene";
export const actionEditImageAlpha = register({
name: "editImageAlpha",
perform: async (elements, appState, _, app) => {
if (appState.editingImageElement) {
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
const selectedElements = getSelectedElements(elements, appState);
const selectedElement = selectedElements[0];
if (
selectedElements.length === 1 &&
isInitializedImageElement(selectedElement)
) {
const imgData = app.imageCache.get(selectedElement.fileId);
if (!imgData) {
return false;
}
const image = await imgData.image;
const { width, height } = image;
const canvas = document.createElement("canvas");
canvas.height = height;
canvas.width = width;
const context = canvas.getContext("2d")!;
context.drawImage(image, 0, 0, width, height);
const imageData = context.getImageData(0, 0, width, height);
Scene.mapElementToScene(selectedElement.id, app.scene);
return {
appState: {
...appState,
editingImageElement: {
editorType: "alpha",
elementId: selectedElement.id,
origImageData: imageData,
imageData,
pointerDownState: { screenX: 0, screenY: 0, sampledPixel: null },
},
},
commitToHistory: false,
};
}
return false;
},
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
type="button"
icon={backgroundIcon}
label="Edit Image Alpha"
className={appState.editingImageElement ? "active" : ""}
title={"Edit image alpha"}
aria-label={"Edit image alpha"}
onClick={() => updateData(null)}
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
/>
),
});
+2 -5
View File
@@ -1,7 +1,7 @@
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups"; import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements, isTextElement } from "../element"; import { getNonDeletedElements } from "../element";
export const actionSelectAll = register({ export const actionSelectAll = register({
name: "selectAll", name: "selectAll",
@@ -15,10 +15,7 @@ export const actionSelectAll = register({
...appState, ...appState,
editingGroupId: null, editingGroupId: null,
selectedElementIds: elements.reduce((map, element) => { selectedElementIds: elements.reduce((map, element) => {
if ( if (!element.isDeleted) {
!element.isDeleted &&
!(isTextElement(element) && element.containerId)
) {
map[element.id] = true; map[element.id] = true;
} }
return map; return map;
-1
View File
@@ -80,4 +80,3 @@ export { actionToggleGridMode } from "./actionToggleGridMode";
export { actionToggleZenMode } from "./actionToggleZenMode"; export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleStats } from "./actionToggleStats"; export { actionToggleStats } from "./actionToggleStats";
export { actionEditImageAlpha } from "./actionImageEditing";
+1 -2
View File
@@ -101,8 +101,7 @@ export type ActionName =
| "flipVertical" | "flipVertical"
| "viewMode" | "viewMode"
| "exportWithDarkMode" | "exportWithDarkMode"
| "toggleTheme" | "toggleTheme";
| "editImageAlpha";
export type PanelComponentProps = { export type PanelComponentProps = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
-2
View File
@@ -41,7 +41,6 @@ export const getDefaultAppState = (): Omit<
editingElement: null, editingElement: null,
editingGroupId: null, editingGroupId: null,
editingLinearElement: null, editingLinearElement: null,
editingImageElement: null,
elementLocked: false, elementLocked: false,
elementType: "selection", elementType: "selection",
errorMessage: null, errorMessage: null,
@@ -126,7 +125,6 @@ const APP_STATE_STORAGE_CONF = (<
editingElement: { browser: false, export: false, server: false }, editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false }, editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false }, editingLinearElement: { browser: false, export: false, server: false },
editingImageElement: { browser: false, export: false, server: false },
elementLocked: { browser: true, export: false, server: false }, elementLocked: { browser: true, export: false, server: false },
elementType: { browser: true, export: false, server: false }, elementType: { browser: true, export: false, server: false },
errorMessage: { browser: false, export: false, server: false }, errorMessage: { browser: false, export: false, server: false },
+1 -2
View File
@@ -58,8 +58,7 @@ export const copyToClipboard = async (
appState: AppState, appState: AppState,
files: BinaryFiles, files: BinaryFiles,
) => { ) => {
// select binded text elements when copying const selectedElements = getSelectedElements(elements, appState);
const selectedElements = getSelectedElements(elements, appState, true);
const contents: ElementsClipboard = { const contents: ElementsClipboard = {
type: EXPORT_DATA_TYPES.excalidrawClipboard, type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements: selectedElements, elements: selectedElements,
-8
View File
@@ -19,7 +19,6 @@ 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 { isImageElement } from "../element/typeChecks";
export const SelectedShapeActions = ({ export const SelectedShapeActions = ({
appState, appState,
@@ -106,13 +105,6 @@ export const SelectedShapeActions = ({
<>{renderAction("changeArrowhead")}</> <>{renderAction("changeArrowhead")}</>
)} )}
<fieldset>
<div className="buttonList">
{targetElements.some((element) => isImageElement(element)) &&
renderAction("editImageAlpha")}
</div>
</fieldset>
{renderAction("changeOpacity")} {renderAction("changeOpacity")}
<fieldset> <fieldset>
+125 -349
View File
@@ -43,7 +43,8 @@ import {
import { import {
APP_NAME, APP_NAME,
CURSOR_TYPE, CURSOR_TYPE,
DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT, DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT_JPG,
DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT_OTHER,
DEFAULT_UI_OPTIONS, DEFAULT_UI_OPTIONS,
DEFAULT_VERTICAL_ALIGN, DEFAULT_VERTICAL_ALIGN,
DRAGGING_THRESHOLD, DRAGGING_THRESHOLD,
@@ -120,7 +121,6 @@ import {
} from "../element/mutateElement"; } from "../element/mutateElement";
import { deepCopyElement, newFreeDrawElement } from "../element/newElement"; import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
import { import {
hasBoundTextElement,
isBindingElement, isBindingElement,
isBindingElementType, isBindingElementType,
isImageElement, isImageElement,
@@ -175,7 +175,7 @@ import {
isSomeElementSelected, isSomeElementSelected,
} from "../scene"; } from "../scene";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { RenderConfig, ScrollBars } from "../scene/types"; import { SceneState, ScrollBars } from "../scene/types";
import { getNewZoom } from "../scene/zoom"; import { getNewZoom } from "../scene/zoom";
import { findShapeByKey } from "../shapes"; import { findShapeByKey } from "../shapes";
import { import {
@@ -195,7 +195,6 @@ import {
import { import {
debounce, debounce,
distance, distance,
getFontString,
getNearestScrollableContainer, getNearestScrollableContainer,
isInputLike, isInputLike,
isToolIcon, isToolIcon,
@@ -224,20 +223,13 @@ import {
} from "../data/blob"; } from "../data/blob";
import { import {
getInitializedImageElements, getInitializedImageElements,
hasTransparentPixels,
loadHTMLImageElement, loadHTMLImageElement,
normalizeSVG, normalizeSVG,
updateImageCache as _updateImageCache, updateImageCache as _updateImageCache,
} from "../element/image"; } from "../element/image";
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import { fileOpen, nativeFileSystemSupported } from "../data/filesystem"; import { fileOpen, nativeFileSystemSupported } from "../data/filesystem";
import {
bindTextToShapeAfterDuplication,
getApproxMinLineHeight,
getApproxMinLineWidth,
getBoundTextElementId,
} from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
import { ImageEditor } from "../element/imageEditor";
const IsMobileContext = React.createContext(false); const IsMobileContext = React.createContext(false);
export const useIsMobile = () => useContext(IsMobileContext); export const useIsMobile = () => useContext(IsMobileContext);
@@ -282,7 +274,7 @@ class App extends React.Component<AppProps, AppState> {
UIOptions: DEFAULT_UI_OPTIONS, UIOptions: DEFAULT_UI_OPTIONS,
}; };
public scene: Scene; private scene: Scene;
private resizeObserver: ResizeObserver | undefined; private resizeObserver: ResizeObserver | undefined;
private nearestScrollableContainer: HTMLElement | Document | undefined; private nearestScrollableContainer: HTMLElement | Document | undefined;
public library: AppClassProperties["library"]; public library: AppClassProperties["library"];
@@ -1032,14 +1024,8 @@ class App extends React.Component<AppProps, AppState> {
); );
if ( if (
(this.state.editingLinearElement && this.state.editingLinearElement &&
!this.state.selectedElementIds[ !this.state.selectedElementIds[this.state.editingLinearElement.elementId]
this.state.editingLinearElement.elementId
]) ||
(this.state.editingImageElement &&
!this.state.selectedElementIds[
this.state.editingImageElement.elementId
])
) { ) {
// defer so that the commitToHistory flag isn't reset via current update // defer so that the commitToHistory flag isn't reset via current update
setTimeout(() => { setTimeout(() => {
@@ -1069,10 +1055,8 @@ class App extends React.Component<AppProps, AppState> {
const cursorButton: { const cursorButton: {
[id: string]: string | undefined; [id: string]: string | undefined;
} = {}; } = {};
const pointerViewportCoords: RenderConfig["remotePointerViewportCoords"] = const pointerViewportCoords: SceneState["remotePointerViewportCoords"] = {};
{}; const remoteSelectedElementIds: SceneState["remoteSelectedElementIds"] = {};
const remoteSelectedElementIds: RenderConfig["remoteSelectedElementIds"] =
{};
const pointerUsernames: { [id: string]: string } = {}; const pointerUsernames: { [id: string]: string } = {};
const pointerUserStates: { [id: string]: string } = {}; const pointerUserStates: { [id: string]: string } = {};
this.state.collaborators.forEach((user, socketId) => { this.state.collaborators.forEach((user, socketId) => {
@@ -1140,9 +1124,10 @@ class App extends React.Component<AppProps, AppState> {
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom, shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
theme: this.state.theme, theme: this.state.theme,
imageCache: this.imageCache, imageCache: this.imageCache,
isExporting: false, },
{
renderOptimizations: true,
renderScrollbars: !this.isMobile, renderScrollbars: !this.isMobile,
editingImageElement: this.state.editingImageElement,
}, },
); );
if (scrollBars) { if (scrollBars) {
@@ -1150,7 +1135,7 @@ class App extends React.Component<AppProps, AppState> {
} }
const scrolledOutside = const scrolledOutside =
// hide when editing text // hide when editing text
isTextElement(this.state.editingElement) this.state.editingElement?.type === "text"
? false ? false
: !atLeastOneVisibleElement && renderingElements.length > 0; : !atLeastOneVisibleElement && renderingElements.length > 0;
if (this.state.scrolledOutside !== scrolledOutside) { if (this.state.scrolledOutside !== scrolledOutside) {
@@ -1392,7 +1377,6 @@ class App extends React.Component<AppProps, AppState> {
oldIdToDuplicatedId.set(element.id, newElement.id); oldIdToDuplicatedId.set(element.id, newElement.id);
return newElement; return newElement;
}); });
bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId);
const nextElements = [ const nextElements = [
...this.scene.getElementsIncludingDeleted(), ...this.scene.getElementsIncludingDeleted(),
...newElements, ...newElements,
@@ -1411,9 +1395,7 @@ class App extends React.Component<AppProps, AppState> {
...this.state, ...this.state,
isLibraryOpen: false, isLibraryOpen: false,
selectedElementIds: newElements.reduce((map, element) => { selectedElementIds: newElements.reduce((map, element) => {
if (isTextElement(element) && !element.containerId) { map[element.id] = true;
map[element.id] = true;
}
return map; return map;
}, {} as any), }, {} as any),
selectedGroupIds: {}, selectedGroupIds: {},
@@ -1729,11 +1711,9 @@ class App extends React.Component<AppProps, AppState> {
!isLinearElement(selectedElements[0]) !isLinearElement(selectedElements[0])
) { ) {
const selectedElement = selectedElements[0]; const selectedElement = selectedElements[0];
this.startTextEditing({ this.startTextEditing({
sceneX: selectedElement.x + selectedElement.width / 2, sceneX: selectedElement.x + selectedElement.width / 2,
sceneY: selectedElement.y + selectedElement.height / 2, sceneY: selectedElement.y + selectedElement.height / 2,
shouldBind: true,
}); });
event.preventDefault(); event.preventDefault();
return; return;
@@ -1888,24 +1868,14 @@ class App extends React.Component<AppProps, AppState> {
isExistingElement?: boolean; isExistingElement?: boolean;
}, },
) { ) {
const updateElement = ( const updateElement = (text: string, isDeleted = false) => {
text: string,
originalText: string,
isDeleted = false,
updateDimensions = false,
) => {
this.scene.replaceAllElements([ this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted().map((_element) => { ...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) { if (_element.id === element.id && isTextElement(_element)) {
return updateTextElement( return updateTextElement(_element, {
_element, text,
{ isDeleted,
text, });
isDeleted,
originalText,
},
updateDimensions,
);
} }
return _element; return _element;
}), }),
@@ -1930,24 +1900,21 @@ class App extends React.Component<AppProps, AppState> {
]; ];
}, },
onChange: withBatchedUpdates((text) => { onChange: withBatchedUpdates((text) => {
updateElement(text, text, false, !element.containerId); updateElement(text);
if (isNonDeletedElement(element)) { if (isNonDeletedElement(element)) {
updateBoundElements(element); updateBoundElements(element);
} }
}), }),
onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => { onSubmit: withBatchedUpdates(({ text, viaKeyboard }) => {
const isDeleted = !text.trim(); const isDeleted = !text.trim();
updateElement(text, originalText, isDeleted, true); updateElement(text, isDeleted);
// select the created text element only if submitting via keyboard // select the created text element only if submitting via keyboard
// (when submitting via click it should act as signal to deselect) // (when submitting via click it should act as signal to deselect)
if (!isDeleted && viaKeyboard) { if (!isDeleted && viaKeyboard) {
const elementIdToSelect = element.containerId
? element.containerId
: element.id;
this.setState((prevState) => ({ this.setState((prevState) => ({
selectedElementIds: { selectedElementIds: {
...prevState.selectedElementIds, ...prevState.selectedElementIds,
[elementIdToSelect]: true, [element.id]: true,
}, },
})); }));
} }
@@ -1976,7 +1943,7 @@ class App extends React.Component<AppProps, AppState> {
// do an initial update to re-initialize element position since we were // do an initial update to re-initialize element position since we were
// modifying element's x/y for sake of editor (case: syncing to remote) // modifying element's x/y for sake of editor (case: syncing to remote)
updateElement(element.text, element.originalText); updateElement(element.text);
} }
private deselectElements() { private deselectElements() {
@@ -1991,9 +1958,7 @@ class App extends React.Component<AppProps, AppState> {
x: number, x: number,
y: number, y: number,
): NonDeleted<ExcalidrawTextElement> | null { ): NonDeleted<ExcalidrawTextElement> | null {
const element = this.getElementAtPosition(x, y, { const element = this.getElementAtPosition(x, y);
includeBoundTextElement: true,
});
if (element && isTextElement(element) && !element.isDeleted) { if (element && isTextElement(element) && !element.isDeleted) {
return element; return element;
@@ -2008,14 +1973,9 @@ class App extends React.Component<AppProps, AppState> {
/** if true, returns the first selected element (with highest z-index) /** if true, returns the first selected element (with highest z-index)
of all hit elements */ of all hit elements */
preferSelected?: boolean; preferSelected?: boolean;
includeBoundTextElement?: boolean;
}, },
): NonDeleted<ExcalidrawElement> | null { ): NonDeleted<ExcalidrawElement> | null {
const allHitElements = this.getElementsAtPosition( const allHitElements = this.getElementsAtPosition(x, y);
x,
y,
opts?.includeBoundTextElement,
);
if (allHitElements.length > 1) { if (allHitElements.length > 1) {
if (opts?.preferSelected) { if (opts?.preferSelected) {
for (let index = allHitElements.length - 1; index > -1; index--) { for (let index = allHitElements.length - 1; index > -1; index--) {
@@ -2046,16 +2006,8 @@ class App extends React.Component<AppProps, AppState> {
private getElementsAtPosition( private getElementsAtPosition(
x: number, x: number,
y: number, y: number,
includeBoundTextElement: boolean = false,
): NonDeleted<ExcalidrawElement>[] { ): NonDeleted<ExcalidrawElement>[] {
const elements = includeBoundTextElement return getElementsAtPosition(this.scene.getElements(), (element) =>
? this.scene.getElements()
: this.scene
.getElements()
.filter(
(element) => !(isTextElement(element) && element.containerId),
);
return getElementsAtPosition(elements, (element) =>
hitTest(element, this.state, x, y), hitTest(element, this.state, x, y),
); );
} }
@@ -2063,17 +2015,17 @@ class App extends React.Component<AppProps, AppState> {
private startTextEditing = ({ private startTextEditing = ({
sceneX, sceneX,
sceneY, sceneY,
shouldBind,
insertAtParentCenter = true, insertAtParentCenter = true,
}: { }: {
/** X position to insert text at */ /** X position to insert text at */
sceneX: number; sceneX: number;
/** Y position to insert text at */ /** Y position to insert text at */
sceneY: number; sceneY: number;
shouldBind: boolean;
/** whether to attempt to insert at element center if applicable */ /** whether to attempt to insert at element center if applicable */
insertAtParentCenter?: boolean; insertAtParentCenter?: boolean;
}) => { }) => {
const existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
const parentCenterPosition = const parentCenterPosition =
insertAtParentCenter && insertAtParentCenter &&
this.getTextWysiwygSnappedToCenterPosition( this.getTextWysiwygSnappedToCenterPosition(
@@ -2084,43 +2036,6 @@ class App extends React.Component<AppProps, AppState> {
window.devicePixelRatio, window.devicePixelRatio,
); );
// bind to container when shouldBind is true or
// clicked on center of container
const container =
shouldBind || parentCenterPosition
? getElementContainingPosition(
this.scene.getElements(),
sceneX,
sceneY,
"text",
)
: null;
let existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
// consider bounded text element if container present
if (container) {
const boundTextElementId = getBoundTextElementId(container);
if (boundTextElementId) {
existingTextElement = this.scene.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
}
}
if (!existingTextElement && container) {
const fontString = {
fontSize: this.state.currentItemFontSize,
fontFamily: this.state.currentItemFontFamily,
};
const minWidth = getApproxMinLineWidth(getFontString(fontString));
const minHeight = getApproxMinLineHeight(getFontString(fontString));
const newHeight = Math.max(container.height, minHeight);
const newWidth = Math.max(container.width, minWidth);
mutateElement(container, { height: newHeight, width: newWidth });
sceneX = container.x + newWidth / 2;
sceneY = container.y + newHeight / 2;
}
const element = existingTextElement const element = existingTextElement
? existingTextElement ? existingTextElement
: newTextElement({ : newTextElement({
@@ -2147,7 +2062,6 @@ class App extends React.Component<AppProps, AppState> {
verticalAlign: parentCenterPosition verticalAlign: parentCenterPosition
? "middle" ? "middle"
: DEFAULT_VERTICAL_ALIGN, : DEFAULT_VERTICAL_ALIGN,
containerId: container?.id ?? undefined,
}); });
this.setState({ editingElement: element }); this.setState({ editingElement: element });
@@ -2218,7 +2132,7 @@ class App extends React.Component<AppProps, AppState> {
resetCursor(this.canvas); resetCursor(this.canvas);
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
event, event,
this.state, this.state,
); );
@@ -2250,22 +2164,9 @@ class App extends React.Component<AppProps, AppState> {
resetCursor(this.canvas); resetCursor(this.canvas);
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) { if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
const selectedElements = getSelectedElements(
this.scene.getElements(),
this.state,
);
if (selectedElements.length === 1) {
const selectedElement = selectedElements[0];
const canBindText = hasBoundTextElement(selectedElement);
if (canBindText) {
sceneX = selectedElement.x + selectedElement.width / 2;
sceneY = selectedElement.y + selectedElement.height / 2;
}
}
this.startTextEditing({ this.startTextEditing({
sceneX, sceneX,
sceneY, sceneY,
shouldBind: false,
insertAtParentCenter: !event.altKey, insertAtParentCenter: !event.altKey,
}); });
} }
@@ -2338,10 +2239,6 @@ class App extends React.Component<AppProps, AppState> {
const scenePointer = viewportCoordsToSceneCoords(event, this.state); const scenePointer = viewportCoordsToSceneCoords(event, this.state);
const { x: scenePointerX, y: scenePointerY } = scenePointer; const { x: scenePointerX, y: scenePointerY } = scenePointer;
if (this.state.editingImageElement) {
return;
}
if ( if (
this.state.editingLinearElement && this.state.editingLinearElement &&
!this.state.editingLinearElement.isDragging !this.state.editingLinearElement.isDragging
@@ -2368,9 +2265,10 @@ class App extends React.Component<AppProps, AppState> {
// and point // and point
const { draggingElement } = this.state; const { draggingElement } = this.state;
if (isBindingElement(draggingElement)) { if (isBindingElement(draggingElement)) {
this.maybeSuggestBindingsForLinearElementAtCoords( this.maybeSuggestBindingForLinearElementAtCursor(
draggingElement, draggingElement,
[scenePointer], "end",
scenePointer,
this.state.startBoundElement, this.state.startBoundElement,
); );
} else { } else {
@@ -2503,21 +2401,6 @@ class App extends React.Component<AppProps, AppState> {
setCursor(this.canvas, CURSOR_TYPE.GRAB); setCursor(this.canvas, CURSOR_TYPE.GRAB);
} else if (isOverScrollBar) { } else if (isOverScrollBar) {
setCursor(this.canvas, CURSOR_TYPE.AUTO); setCursor(this.canvas, CURSOR_TYPE.AUTO);
} else if (this.state.editingLinearElement) {
const element = LinearElementEditor.getElement(
this.state.editingLinearElement.elementId,
);
if (
element &&
isHittingElementNotConsideringBoundingBox(element, this.state, [
scenePointer.x,
scenePointer.y,
])
) {
setCursor(this.canvas, CURSOR_TYPE.MOVE);
} else {
setCursor(this.canvas, CURSOR_TYPE.AUTO);
}
} else if ( } else if (
// if using cmd/ctrl, we're not dragging // if using cmd/ctrl, we're not dragging
!event[KEYS.CTRL_OR_CMD] && !event[KEYS.CTRL_OR_CMD] &&
@@ -2855,7 +2738,6 @@ class App extends React.Component<AppProps, AppState> {
origin, origin,
selectedElements, selectedElements,
), ),
hasHitElementInside: false,
}, },
drag: { drag: {
hasOccurred: false, hasOccurred: false,
@@ -2867,9 +2749,6 @@ class App extends React.Component<AppProps, AppState> {
onKeyUp: null, onKeyUp: null,
onKeyDown: null, onKeyDown: null,
}, },
boxSelection: {
hasOccurred: false,
},
}; };
} }
@@ -2932,14 +2811,6 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
): boolean => { ): boolean => {
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
if (this.state.editingImageElement) {
ImageEditor.handlePointerDown(
this.state.editingImageElement,
pointerDownState.origin,
);
return false;
}
const elements = this.scene.getElements(); const elements = this.scene.getElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(elements, this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) { if (selectedElements.length === 1 && !this.state.editingLinearElement) {
@@ -3019,15 +2890,6 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y, pointerDownState.origin.y,
); );
if (pointerDownState.hit.element) {
pointerDownState.hit.hasHitElementInside =
isHittingElementNotConsideringBoundingBox(
pointerDownState.hit.element,
this.state,
[pointerDownState.origin.x, pointerDownState.origin.y],
);
}
// For overlapped elements one position may hit // For overlapped elements one position may hit
// multiple elements // multiple elements
pointerDownState.hit.allHitElements = this.getElementsAtPosition( pointerDownState.hit.allHitElements = this.getElementsAtPosition(
@@ -3048,14 +2910,8 @@ class App extends React.Component<AppProps, AppState> {
this.clearSelection(hitElement); this.clearSelection(hitElement);
} }
if (this.state.editingLinearElement) { // If we click on something
this.setState({ if (hitElement != null) {
selectedElementIds: {
[this.state.editingLinearElement.elementId]: true,
},
});
// If we click on something
} else if (hitElement != null) {
// on CMD/CTRL, drill down to hit element regardless of groups etc. // on CMD/CTRL, drill down to hit element regardless of groups etc.
if (event[KEYS.CTRL_OR_CMD]) { if (event[KEYS.CTRL_OR_CMD]) {
if (!this.state.selectedElementIds[hitElement.id]) { if (!this.state.selectedElementIds[hitElement.id]) {
@@ -3148,25 +3004,13 @@ class App extends React.Component<AppProps, AppState> {
// if we're currently still editing text, clicking outside // if we're currently still editing text, clicking outside
// should only finalize it, not create another (irrespective // should only finalize it, not create another (irrespective
// of state.elementLocked) // of state.elementLocked)
if (isTextElement(this.state.editingElement)) { if (this.state.editingElement?.type === "text") {
return; return;
} }
let sceneX = pointerDownState.origin.x;
let sceneY = pointerDownState.origin.y;
const element = this.getElementAtPosition(sceneX, sceneY, {
includeBoundTextElement: true,
});
const canBindText = hasBoundTextElement(element);
if (canBindText) {
sceneX = element.x + element.width / 2;
sceneY = element.y + element.height / 2;
}
this.startTextEditing({ this.startTextEditing({
sceneX, sceneX: pointerDownState.origin.x,
sceneY, sceneY: pointerDownState.origin.y,
shouldBind: false,
insertAtParentCenter: !event.altKey, insertAtParentCenter: !event.altKey,
}); });
@@ -3500,32 +3344,17 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
if (this.state.editingImageElement) {
const newImageData = ImageEditor.handlePointerMove(
this.state.editingImageElement,
pointerCoords,
);
if (newImageData) {
this.setState({
editingImageElement: {
...this.state.editingImageElement,
imageData: newImageData,
},
});
}
return;
}
if (this.state.editingLinearElement) { if (this.state.editingLinearElement) {
const didDrag = LinearElementEditor.handlePointDragging( const didDrag = LinearElementEditor.handlePointDragging(
this.state, this.state,
(appState) => this.setState(appState), (appState) => this.setState(appState),
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
(element, pointsSceneCoords) => { (element, startOrEnd) => {
this.maybeSuggestBindingsForLinearElementAtCoords( this.maybeSuggestBindingForLinearElementAtCursor(
element, element,
pointsSceneCoords, startOrEnd,
pointerCoords,
); );
}, },
); );
@@ -3542,16 +3371,8 @@ class App extends React.Component<AppProps, AppState> {
); );
if ( if (
(hasHitASelectedElement || hasHitASelectedElement ||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) && pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
// this allows for box-selecting points when clicking inside the
// line's bounding box
(!this.state.editingLinearElement || !event.shiftKey) &&
// box-selecting without shift when editing line, not clicking on a line
(!this.state.editingLinearElement ||
this.state.editingLinearElement?.elementId !==
pointerDownState.hit.element?.id ||
pointerDownState.hit.hasHitElementInside)
) { ) {
// Marking that click was used for dragging to check // Marking that click was used for dragging to check
// if elements should be deselected on pointerup // if elements should be deselected on pointerup
@@ -3582,6 +3403,7 @@ class App extends React.Component<AppProps, AppState> {
selectedElements, selectedElements,
dragX, dragX,
dragY, dragY,
this.scene,
lockDirection, lockDirection,
dragDistanceX, dragDistanceX,
dragDistanceY, dragDistanceY,
@@ -3601,15 +3423,9 @@ class App extends React.Component<AppProps, AppState> {
const groupIdMap = new Map(); const groupIdMap = new Map();
const oldIdToDuplicatedId = new Map(); const oldIdToDuplicatedId = new Map();
const hitElement = pointerDownState.hit.element; const hitElement = pointerDownState.hit.element;
const elements = this.scene.getElementsIncludingDeleted(); for (const element of this.scene.getElementsIncludingDeleted()) {
const selectedElementIds: Array<ExcalidrawElement["id"]> =
getSelectedElements(elements, this.state, true).map(
(element) => element.id,
);
for (const element of elements) {
if ( if (
selectedElementIds.includes(element.id) || this.state.selectedElementIds[element.id] ||
// case: the state.selectedElementIds might not have been // case: the state.selectedElementIds might not have been
// updated yet by the time this mousemove event is fired // updated yet by the time this mousemove event is fired
(element.id === hitElement?.id && (element.id === hitElement?.id &&
@@ -3637,11 +3453,6 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
const nextSceneElements = [...nextElements, ...elementsToAppend]; const nextSceneElements = [...nextElements, ...elementsToAppend];
bindTextToShapeAfterDuplication(
nextElements,
elementsToAppend,
oldIdToDuplicatedId,
);
fixBindingsAfterDuplication( fixBindingsAfterDuplication(
nextSceneElements, nextSceneElements,
elementsToAppend, elementsToAppend,
@@ -3698,9 +3509,10 @@ class App extends React.Component<AppProps, AppState> {
if (isBindingElement(draggingElement)) { if (isBindingElement(draggingElement)) {
// When creating a linear element by dragging // When creating a linear element by dragging
this.maybeSuggestBindingsForLinearElementAtCoords( this.maybeSuggestBindingForLinearElementAtCursor(
draggingElement, draggingElement,
[pointerCoords], "end",
pointerCoords,
this.state.startBoundElement, this.state.startBoundElement,
); );
} }
@@ -3711,15 +3523,8 @@ class App extends React.Component<AppProps, AppState> {
} }
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
pointerDownState.boxSelection.hasOccurred = true;
const elements = this.scene.getElements(); const elements = this.scene.getElements();
if ( if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
!event.shiftKey &&
// allows for box-selecting points (without shift)
!this.state.editingLinearElement &&
isSomeElementSelected(elements, this.state)
) {
if (pointerDownState.withCmdOrCtrl && pointerDownState.hit.element) { if (pointerDownState.withCmdOrCtrl && pointerDownState.hit.element) {
this.setState((prevState) => this.setState((prevState) =>
selectGroupsForSelectedElements( selectGroupsForSelectedElements(
@@ -3740,43 +3545,33 @@ class App extends React.Component<AppProps, AppState> {
}); });
} }
} }
// box-select line editor points const elementsWithinSelection = getElementsWithinSelection(
if (this.state.editingLinearElement) { elements,
LinearElementEditor.handleBoxSelection( draggingElement,
event, );
this.state, this.setState((prevState) =>
this.setState.bind(this), selectGroupsForSelectedElements(
); {
// regular box-select ...prevState,
} else { selectedElementIds: {
const elementsWithinSelection = getElementsWithinSelection( ...prevState.selectedElementIds,
elements, ...elementsWithinSelection.reduce((map, element) => {
draggingElement, map[element.id] = true;
); return map;
this.setState((prevState) => }, {} as any),
selectGroupsForSelectedElements( ...(pointerDownState.hit.element
{ ? {
...prevState, // if using ctrl/cmd, select the hitElement only if we
selectedElementIds: { // haven't box-selected anything else
...prevState.selectedElementIds, [pointerDownState.hit.element.id]:
...elementsWithinSelection.reduce((map, element) => { !elementsWithinSelection.length,
map[element.id] = true; }
return map; : null),
}, {} as any),
...(pointerDownState.hit.element
? {
// if using ctrl/cmd, select the hitElement only if we
// haven't box-selected anything else
[pointerDownState.hit.element.id]:
!elementsWithinSelection.length,
}
: null),
},
}, },
this.scene.getElements(), },
), this.scene.getElements(),
); ),
} );
} }
}); });
} }
@@ -3838,32 +3633,19 @@ class App extends React.Component<AppProps, AppState> {
this.savePointer(childEvent.clientX, childEvent.clientY, "up"); this.savePointer(childEvent.clientX, childEvent.clientY, "up");
if (this.state.editingImageElement) {
ImageEditor.handlePointerUp(this.state.editingImageElement);
}
// Handle end of dragging a point of a linear element, might close a loop // Handle end of dragging a point of a linear element, might close a loop
// and sets binding element // and sets binding element
if (this.state.editingLinearElement) { if (this.state.editingLinearElement) {
if ( const editingLinearElement = LinearElementEditor.handlePointerUp(
!pointerDownState.boxSelection.hasOccurred && childEvent,
(pointerDownState.hit?.element?.id !== this.state.editingLinearElement,
this.state.editingLinearElement.elementId || this.state,
!pointerDownState.hit.hasHitElementInside) );
) { if (editingLinearElement !== this.state.editingLinearElement) {
this.actionManager.executeAction(actionFinalize); this.setState({
} else { editingLinearElement,
const editingLinearElement = LinearElementEditor.handlePointerUp( suggestedBindings: [],
childEvent, });
this.state.editingLinearElement,
this.state,
);
if (editingLinearElement !== this.state.editingLinearElement) {
this.setState({
editingLinearElement,
suggestedBindings: [],
});
}
} }
} }
@@ -4045,14 +3827,9 @@ class App extends React.Component<AppProps, AppState> {
if ( if (
hitElement && hitElement &&
!pointerDownState.drag.hasOccurred && !pointerDownState.drag.hasOccurred &&
!pointerDownState.hit.wasAddedToSelection && !pointerDownState.hit.wasAddedToSelection
// if we're editing a line, pointerup shouldn't switch selection if
// box selected
(!this.state.editingLinearElement ||
!pointerDownState.boxSelection.hasOccurred)
) { ) {
// when inside line editor, shift selects points instead if (childEvent.shiftKey) {
if (childEvent.shiftKey && !this.state.editingLinearElement) {
if (this.state.selectedElementIds[hitElement.id]) { if (this.state.selectedElementIds[hitElement.id]) {
if (isSelectedViaGroup(this.state, hitElement)) { if (isSelectedViaGroup(this.state, hitElement)) {
// We want to unselect all groups hitElement is part of // We want to unselect all groups hitElement is part of
@@ -4096,7 +3873,6 @@ class App extends React.Component<AppProps, AppState> {
} else { } else {
// add element to selection while // add element to selection while
// keeping prev elements selected // keeping prev elements selected
this.setState((_prevState) => ({ this.setState((_prevState) => ({
selectedElementIds: { selectedElementIds: {
..._prevState.selectedElementIds, ..._prevState.selectedElementIds,
@@ -4227,19 +4003,30 @@ class App extends React.Component<AppProps, AppState> {
const existingFileData = this.files[fileId]; const existingFileData = this.files[fileId];
if (!existingFileData?.dataURL) { if (!existingFileData?.dataURL) {
try { try {
imageFile = await resizeImageFile(imageFile, { if (!(await hasTransparentPixels(imageFile))) {
maxWidthOrHeight: DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT, const _imageFile = await resizeImageFile(imageFile, {
}); maxWidthOrHeight: DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT_JPG,
outputType: MIME_TYPES.jpg,
});
if (_imageFile.size > MAX_ALLOWED_FILE_BYTES) {
imageFile = await resizeImageFile(imageFile, {
maxWidthOrHeight: DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT_OTHER,
outputType: MIME_TYPES.jpg,
});
} else {
imageFile = _imageFile;
}
} else {
imageFile = await resizeImageFile(imageFile, {
maxWidthOrHeight: DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT_OTHER,
});
}
} catch (error: any) { } catch (error: any) {
console.error("error trying to resing image file on insertion", error); console.error("error trying to resing image file on insertion", error);
} }
if (imageFile.size > MAX_ALLOWED_FILE_BYTES) { if (imageFile.size > MAX_ALLOWED_FILE_BYTES) {
throw new Error( throw new Error(t("errors.fileTooBig"));
t("errors.fileTooBig", {
maxSize: `${Math.trunc(MAX_ALLOWED_FILE_BYTES / 1024 / 1024)}MB`,
}),
);
} }
} }
@@ -4578,43 +4365,32 @@ class App extends React.Component<AppProps, AppState> {
}); });
}; };
private maybeSuggestBindingsForLinearElementAtCoords = ( private maybeSuggestBindingForLinearElementAtCursor = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
/** scene coords */ startOrEnd: "start" | "end",
pointerCoords: { pointerCoords: {
x: number; x: number;
y: number; y: number;
}[], },
// During line creation the start binding hasn't been written yet // During line creation the start binding hasn't been written yet
// into `linearElement` // into `linearElement`
oppositeBindingBoundElement?: ExcalidrawBindableElement | null, oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
): void => { ): void => {
if (!pointerCoords.length) { const hoveredBindableElement = getHoveredElementForBinding(
return; pointerCoords,
} this.scene,
const suggestedBindings = pointerCoords.reduce(
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
const hoveredBindableElement = getHoveredElementForBinding(
coords,
this.scene,
);
if (
hoveredBindableElement != null &&
!isLinearElementSimpleAndAlreadyBound(
linearElement,
oppositeBindingBoundElement?.id,
hoveredBindableElement,
)
) {
acc.push(hoveredBindableElement);
}
return acc;
},
[],
); );
this.setState({
this.setState({ suggestedBindings }); suggestedBindings:
hoveredBindableElement != null &&
!isLinearElementSimpleAndAlreadyBound(
linearElement,
oppositeBindingBoundElement?.id,
hoveredBindableElement,
)
? [hoveredBindableElement]
: [],
});
}; };
private maybeSuggestBindingForAll( private maybeSuggestBindingForAll(
+4 -11
View File
@@ -3,22 +3,15 @@ import OpenColor from "open-color";
import "./Card.scss"; import "./Card.scss";
export const Card: React.FC<{ export const Card: React.FC<{
color: keyof OpenColor | "primary"; color: keyof OpenColor;
}> = ({ children, color }) => { }> = ({ children, color }) => {
return ( return (
<div <div
className="Card" className="Card"
style={{ style={{
["--card-color" as any]: ["--card-color" as any]: OpenColor[color][7],
color === "primary" ? "var(--color-primary)" : OpenColor[color][7], ["--card-color-darker" as any]: OpenColor[color][8],
["--card-color-darker" as any]: ["--card-color-darkest" as any]: OpenColor[color][9],
color === "primary"
? "var(--color-primary-darker)"
: OpenColor[color][8],
["--card-color-darkest" as any]:
color === "primary"
? "var(--color-primary-darkest)"
: OpenColor[color][9],
}} }}
> >
{children} {children}
+2 -2
View File
@@ -6,14 +6,14 @@ import "./CheckboxItem.scss";
export const CheckboxItem: React.FC<{ export const CheckboxItem: React.FC<{
checked: boolean; checked: boolean;
onChange: (checked: boolean, event: React.MouseEvent) => void; onChange: (checked: boolean) => void;
className?: string; className?: string;
}> = ({ children, checked, onChange, className }) => { }> = ({ children, checked, onChange, className }) => {
return ( return (
<div <div
className={clsx("Checkbox", className, { "is-checked": checked })} className={clsx("Checkbox", className, { "is-checked": checked })}
onClick={(event) => { onClick={(event) => {
onChange(!checked, event); onChange(!checked);
( (
(event.currentTarget as HTMLDivElement).querySelector( (event.currentTarget as HTMLDivElement).querySelector(
".Checkbox-box", ".Checkbox-box",
+6 -12
View File
@@ -7,7 +7,6 @@ import { AppState } from "../types";
import { import {
isImageElement, isImageElement,
isLinearElement, isLinearElement,
isTextBindableContainer,
isTextElement, isTextElement,
} from "../element/typeChecks"; } from "../element/typeChecks";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
@@ -61,18 +60,13 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
return t("hints.rotate"); return t("hints.rotate");
} }
if (selectedElements.length === 1) { if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
if (isLinearElement(selectedElements[0])) { if (appState.editingLinearElement) {
if (appState.editingLinearElement) { return appState.editingLinearElement.activePointIndex
return appState.editingLinearElement.selectedPointsIndices ? t("hints.lineEditor_pointSelected")
? t("hints.lineEditor_pointSelected") : t("hints.lineEditor_nothingSelected");
: t("hints.lineEditor_nothingSelected");
}
return t("hints.lineEditor_info");
}
if (isTextBindableContainer(selectedElements[0])) {
return t("hints.bindTextToElement");
} }
return t("hints.lineEditor_info");
} }
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) { if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
+1 -1
View File
@@ -22,7 +22,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&:focus-visible { &:focus {
outline: transparent; outline: transparent;
background-color: var(--button-gray-2); background-color: var(--button-gray-2);
& svg { & svg {
+1 -1
View File
@@ -102,7 +102,7 @@ const ImageExportModal = ({
const { exportBackground, viewBackgroundColor } = appState; const { exportBackground, viewBackgroundColor } = appState;
const exportedElements = exportSelected const exportedElements = exportSelected
? getSelectedElements(elements, appState, true) ? getSelectedElements(elements, appState)
: elements; : elements;
useEffect(() => { useEffect(() => {
+1 -1
View File
@@ -3,7 +3,7 @@
--padding: 0; --padding: 0;
background-color: var(--island-bg-color); background-color: var(--island-bg-color);
box-shadow: var(--shadow-island); box-shadow: var(--shadow-island);
border-radius: var(--border-radius-lg); border-radius: 4px;
padding: calc(var(--padding) * var(--space-factor)); padding: calc(var(--padding) * var(--space-factor));
position: relative; position: relative;
transition: box-shadow 0.5s ease-in-out; transition: box-shadow 0.5s ease-in-out;
+4 -13
View File
@@ -19,6 +19,7 @@ import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
import { FixedSideContainer } from "./FixedSideContainer"; import { FixedSideContainer } from "./FixedSideContainer";
import { HintViewer } from "./HintViewer"; import { HintViewer } from "./HintViewer";
import { Island } from "./Island"; import { Island } from "./Island";
import "./LayerUI.scss";
import { LoadingMessage } from "./LoadingMessage"; import { LoadingMessage } from "./LoadingMessage";
import { LockButton } from "./LockButton"; import { LockButton } from "./LockButton";
import { MobileMenu } from "./MobileMenu"; import { MobileMenu } from "./MobileMenu";
@@ -34,9 +35,6 @@ import { LibraryButton } from "./LibraryButton";
import { isImageFileHandle } from "../data/blob"; import { isImageFileHandle } from "../data/blob";
import { LibraryMenu } from "./LibraryMenu"; import { LibraryMenu } from "./LibraryMenu";
import "./LayerUI.scss";
import "./Toolbar.scss";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
appState: AppState; appState: AppState;
@@ -270,7 +268,7 @@ const LayerUI = ({
const libraryMenu = appState.isLibraryOpen ? ( const libraryMenu = appState.isLibraryOpen ? (
<LibraryMenu <LibraryMenu
pendingElements={getSelectedElements(elements, appState, true)} pendingElements={getSelectedElements(elements, appState)}
onClose={closeLibrary} onClose={closeLibrary}
onInsertShape={onInsertElements} onInsertShape={onInsertElements}
onAddToLibrary={deselectItems} onAddToLibrary={deselectItems}
@@ -307,12 +305,7 @@ const LayerUI = ({
<Section heading="shapes"> <Section heading="shapes">
{(heading) => ( {(heading) => (
<Stack.Col gap={4} align="start"> <Stack.Col gap={4} align="start">
<Stack.Row <Stack.Row gap={1}>
gap={1}
className={clsx("App-toolbar-container", {
"zen-mode": zenModeEnabled,
})}
>
<LockButton <LockButton
zenModeEnabled={zenModeEnabled} zenModeEnabled={zenModeEnabled}
checked={appState.elementLocked} checked={appState.elementLocked}
@@ -321,9 +314,7 @@ const LayerUI = ({
/> />
<Island <Island
padding={1} padding={1}
className={clsx("App-toolbar", { className={clsx({ "zen-mode": zenModeEnabled })}
"zen-mode": zenModeEnabled,
})}
> >
<HintViewer <HintViewer
appState={appState} appState={appState}
+4 -4
View File
@@ -16,18 +16,18 @@ const LIBRARY_ICON = (
export const LibraryButton: React.FC<{ export const LibraryButton: React.FC<{
appState: AppState; appState: AppState;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
isMobile?: boolean; }> = ({ appState, setAppState }) => {
}> = ({ appState, setAppState, isMobile }) => {
return ( return (
<label <label
className={clsx( className={clsx(
"ToolIcon ToolIcon_type_floating ToolIcon__library", "ToolIcon ToolIcon_type_floating ToolIcon__library zen-mode-visibility",
`ToolIcon_size_medium`, `ToolIcon_size_medium`,
{ {
"is-mobile": isMobile, "zen-mode-visibility--hidden": appState.zenModeEnabled,
}, },
)} )}
title={`${capitalizeString(t("toolBar.library"))} — 0`} title={`${capitalizeString(t("toolBar.library"))} — 0`}
style={{ marginInlineStart: "var(--space-factor)" }}
> >
<input <input
className="ToolIcon_type_checkbox" className="ToolIcon_type_checkbox"
+3 -42
View File
@@ -18,7 +18,6 @@ import "./LibraryMenu.scss";
import LibraryMenuItems from "./LibraryMenuItems"; import LibraryMenuItems from "./LibraryMenuItems";
import { EVENT } from "../constants"; import { EVENT } from "../constants";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { arrayToMap } from "../utils";
const useOnClickOutside = ( const useOnClickOutside = (
ref: RefObject<HTMLElement>, ref: RefObject<HTMLElement>,
@@ -237,10 +236,6 @@ export const LibraryMenu = ({
], ],
); );
const [lastSelectedItem, setLastSelectedItem] = useState<
LibraryItem["id"] | null
>(null);
return loadingState === "preloading" ? null : ( return loadingState === "preloading" ? null : (
<Island padding={1} ref={ref} className="layer-ui__library"> <Island padding={1} ref={ref} className="layer-ui__library">
{showPublishLibraryDialog && ( {showPublishLibraryDialog && (
@@ -276,44 +271,10 @@ export const LibraryMenu = ({
files={files} files={files}
id={id} id={id}
selectedItems={selectedItems} selectedItems={selectedItems}
onToggle={(id, event) => { onToggle={(id) => {
const shouldSelect = !selectedItems.includes(id); if (!selectedItems.includes(id)) {
setSelectedItems([...selectedItems, id]);
if (shouldSelect) {
if (event.shiftKey && lastSelectedItem) {
const rangeStart = libraryItems.findIndex(
(item) => item.id === lastSelectedItem,
);
const rangeEnd = libraryItems.findIndex(
(item) => item.id === id,
);
if (rangeStart === -1 || rangeEnd === -1) {
setSelectedItems([...selectedItems, id]);
return;
}
const selectedItemsMap = arrayToMap(selectedItems);
const nextSelectedIds = libraryItems.reduce(
(acc: LibraryItem["id"][], item, idx) => {
if (
(idx >= rangeStart && idx <= rangeEnd) ||
selectedItemsMap.has(item.id)
) {
acc.push(item.id);
}
return acc;
},
[],
);
setSelectedItems(nextSelectedIds);
} else {
setSelectedItems([...selectedItems, id]);
}
setLastSelectedItem(id);
} else { } else {
setLastSelectedItem(null);
setSelectedItems(selectedItems.filter((_id) => _id !== id)); setSelectedItems(selectedItems.filter((_id) => _id !== id));
} }
}} }}
+6 -7
View File
@@ -21,7 +21,6 @@ import { ToolButton } from "./ToolButton";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import "./LibraryMenuItems.scss"; import "./LibraryMenuItems.scss";
import { VERSIONS } from "../constants";
const LibraryMenuItems = ({ const LibraryMenuItems = ({
libraryItems, libraryItems,
@@ -52,7 +51,7 @@ const LibraryMenuItems = ({
library: Library; library: Library;
id: string; id: string;
selectedItems: LibraryItem["id"][]; selectedItems: LibraryItem["id"][];
onToggle: (id: LibraryItem["id"], event: React.MouseEvent) => void; onToggle: (id: LibraryItem["id"]) => void;
onPublish: () => void; onPublish: () => void;
resetLibrary: () => void; resetLibrary: () => void;
}) => { }) => {
@@ -213,8 +212,10 @@ const LibraryMenuItems = ({
onClick={params.onClick || (() => {})} onClick={params.onClick || (() => {})}
id={params.item?.id || null} id={params.item?.id || null}
selected={!!params.item?.id && selectedItems.includes(params.item.id)} selected={!!params.item?.id && selectedItems.includes(params.item.id)}
onToggle={(id, event) => { onToggle={() => {
onToggle(id, event); if (params.item?.id) {
onToggle(params.item.id);
}
}} }}
/> />
</Stack.Col> </Stack.Col>
@@ -292,9 +293,7 @@ const LibraryMenuItems = ({
<a <a
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${ href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
window.name || "_blank" window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${ }&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}`}
VERSIONS.excalidrawLibrary
}`}
target="_excalidraw_libraries" target="_excalidraw_libraries"
> >
{t("labels.libraries")} {t("labels.libraries")}
+2 -7
View File
@@ -99,13 +99,8 @@
margin-top: -10px; margin-top: -10px;
pointer-events: none; pointer-events: none;
} }
.library-unit:hover .library-unit__adder { .library-unit--hover .library-unit__adder {
fill: $oc-blue-7; color: $oc-blue-7;
}
.library-unit:active .library-unit__adder {
animation: none;
transform: scale(0.8);
fill: $oc-black;
} }
.library-unit__active { .library-unit__active {
+6 -19
View File
@@ -8,15 +8,12 @@ import { BinaryFiles, LibraryItem } from "../types";
import "./LibraryUnit.scss"; import "./LibraryUnit.scss";
import { CheckboxItem } from "./CheckboxItem"; import { CheckboxItem } from "./CheckboxItem";
// fa-plus
const PLUS_ICON = ( const PLUS_ICON = (
<svg viewBox="0 0 1792 1792"> <svg viewBox="0 0 1792 1792">
<path <path
d="M1600 736v192c0 26.667-9.33 49.333-28 68-18.67 18.67-41.33 28-68 28h-416v416c0 26.67-9.33 49.33-28 68s-41.33 28-68 28H800c-26.667 0-49.333-9.33-68-28s-28-41.33-28-68v-416H288c-26.667 0-49.333-9.33-68-28-18.667-18.667-28-41.333-28-68V736c0-26.667 9.333-49.333 28-68s41.333-28 68-28h416V224c0-26.667 9.333-49.333 28-68s41.333-28 68-28h192c26.67 0 49.33 9.333 68 28s28 41.333 28 68v416h416c26.67 0 49.33 9.333 68 28s28 41.333 28 68Z" fill="currentColor"
style={{ d="M1600 736v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z"
stroke: "#fff",
strokeWidth: 140,
}}
transform="translate(0 64)"
/> />
</svg> </svg>
); );
@@ -36,7 +33,7 @@ export const LibraryUnit = ({
isPending?: boolean; isPending?: boolean;
onClick: () => void; onClick: () => void;
selected: boolean; selected: boolean;
onToggle: (id: string, event: React.MouseEvent) => void; onToggle: (id: string) => void;
}) => { }) => {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
@@ -87,17 +84,7 @@ export const LibraryUnit = ({
})} })}
ref={ref} ref={ref}
draggable={!!elements} draggable={!!elements}
onClick={ onClick={!!elements || !!isPending ? onClick : undefined}
!!elements || !!isPending
? (event) => {
if (id && event.shiftKey) {
onToggle(id, event);
} else {
onClick();
}
}
: undefined
}
onDragStart={(event) => { onDragStart={(event) => {
setIsHovered(false); setIsHovered(false);
event.dataTransfer.setData( event.dataTransfer.setData(
@@ -110,7 +97,7 @@ export const LibraryUnit = ({
{id && elements && (isHovered || isMobile || selected) && ( {id && elements && (isHovered || isMobile || selected) && (
<CheckboxItem <CheckboxItem
checked={selected} checked={selected}
onChange={(checked, event) => onToggle(id, event)} onChange={() => onToggle(id)}
className="library-unit__checkbox" className="library-unit__checkbox"
/> />
)} )}
+2 -3
View File
@@ -10,7 +10,6 @@ type LockIconProps = {
checked: boolean; checked: boolean;
onChange?(): void; onChange?(): void;
zenModeEnabled?: boolean; zenModeEnabled?: boolean;
isMobile?: boolean;
}; };
const DEFAULT_SIZE: ToolButtonSize = "medium"; const DEFAULT_SIZE: ToolButtonSize = "medium";
@@ -43,10 +42,10 @@ export const LockButton = (props: LockIconProps) => {
return ( return (
<label <label
className={clsx( className={clsx(
"ToolIcon ToolIcon__lock ToolIcon_type_floating", "ToolIcon ToolIcon__lock ToolIcon_type_floating zen-mode-visibility",
`ToolIcon_size_${DEFAULT_SIZE}`, `ToolIcon_size_${DEFAULT_SIZE}`,
{ {
"is-mobile": props.isMobile, "zen-mode-visibility--hidden": props.zenModeEnabled,
}, },
)} )}
title={`${props.title} — Q`} title={`${props.title} — Q`}
+3 -8
View File
@@ -64,8 +64,8 @@ export const MobileMenu = ({
<Section heading="shapes"> <Section heading="shapes">
{(heading) => ( {(heading) => (
<Stack.Col gap={4} align="center"> <Stack.Col gap={4} align="center">
<Stack.Row gap={1} className="App-toolbar-container"> <Stack.Row gap={1}>
<Island padding={1} className="App-toolbar"> <Island padding={1}>
{heading} {heading}
<Stack.Row gap={1}> <Stack.Row gap={1}>
<ShapesSwitcher <ShapesSwitcher
@@ -85,13 +85,8 @@ export const MobileMenu = ({
checked={appState.elementLocked} checked={appState.elementLocked}
onChange={onLockToggle} onChange={onLockToggle}
title={t("toolBar.lock")} title={t("toolBar.lock")}
isMobile
/>
<LibraryButton
appState={appState}
setAppState={setAppState}
isMobile
/> />
<LibraryButton appState={appState} setAppState={setAppState} />
</Stack.Row> </Stack.Row>
{libraryMenu} {libraryMenu}
</Stack.Col> </Stack.Col>
+57 -83
View File
@@ -1,5 +1,5 @@
import { ReactNode, useCallback, useEffect, useState } from "react"; import { ReactNode, useCallback, useEffect, useState } from "react";
import OpenColor from "open-color"; import oc from "open-color";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { t } from "../i18n"; import { t } from "../i18n";
@@ -7,19 +7,16 @@ import { t } from "../i18n";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { AppState, LibraryItems, LibraryItem } from "../types"; import { AppState, LibraryItems, LibraryItem } from "../types";
import { exportToCanvas } from "../packages/utils"; import { exportToBlob } from "../packages/utils";
import { import { EXPORT_DATA_TYPES, EXPORT_SOURCE } from "../constants";
EXPORT_DATA_TYPES,
EXPORT_SOURCE,
MIME_TYPES,
VERSIONS,
} from "../constants";
import { ExportedLibraryData } from "../data/types"; import { ExportedLibraryData } from "../data/types";
import "./PublishLibrary.scss"; import "./PublishLibrary.scss";
import { ExcalidrawElement } from "../element/types";
import { newElement } from "../element";
import { mutateElement } from "../element/mutateElement";
import { getCommonBoundingBox } from "../element/bounds";
import SingleLibraryItem from "./SingleLibraryItem"; import SingleLibraryItem from "./SingleLibraryItem";
import { canvasToBlob, resizeImageFile } from "../data/blob";
import { chunk } from "../utils";
interface PublishLibraryDataParams { interface PublishLibraryDataParams {
authorName: string; authorName: string;
@@ -58,75 +55,6 @@ const importPublishLibDataFromStorage = () => {
return null; return null;
}; };
const generatePreviewImage = async (libraryItems: LibraryItems) => {
const MAX_ITEMS_PER_ROW = 6;
const BOX_SIZE = 128;
const BOX_PADDING = Math.round(BOX_SIZE / 16);
const BORDER_WIDTH = Math.max(Math.round(BOX_SIZE / 64), 2);
const rows = chunk(libraryItems, MAX_ITEMS_PER_ROW);
const canvas = document.createElement("canvas");
canvas.width =
rows[0].length * BOX_SIZE +
(rows[0].length + 1) * (BOX_PADDING * 2) -
BOX_PADDING * 2;
canvas.height =
rows.length * BOX_SIZE +
(rows.length + 1) * (BOX_PADDING * 2) -
BOX_PADDING * 2;
const ctx = canvas.getContext("2d")!;
ctx.fillStyle = OpenColor.white;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// draw items
// ---------------------------------------------------------------------------
for (const [index, item] of libraryItems.entries()) {
const itemCanvas = await exportToCanvas({
elements: item.elements,
files: null,
maxWidthOrHeight: BOX_SIZE,
});
const { width, height } = itemCanvas;
// draw item
// -------------------------------------------------------------------------
const rowOffset =
Math.floor(index / MAX_ITEMS_PER_ROW) * (BOX_SIZE + BOX_PADDING * 2);
const colOffset =
(index % MAX_ITEMS_PER_ROW) * (BOX_SIZE + BOX_PADDING * 2);
ctx.drawImage(
itemCanvas,
colOffset + (BOX_SIZE - width) / 2 + BOX_PADDING,
rowOffset + (BOX_SIZE - height) / 2 + BOX_PADDING,
);
// draw item border
// -------------------------------------------------------------------------
ctx.lineWidth = BORDER_WIDTH;
ctx.strokeStyle = OpenColor.gray[4];
ctx.strokeRect(
colOffset + BOX_PADDING / 2,
rowOffset + BOX_PADDING / 2,
BOX_SIZE + BOX_PADDING,
BOX_SIZE + BOX_PADDING,
);
}
return await resizeImageFile(
new File([await canvasToBlob(canvas)], "preview", { type: MIME_TYPES.png }),
{
outputType: MIME_TYPES.jpg,
maxWidthOrHeight: 5000,
},
);
};
const PublishLibrary = ({ const PublishLibrary = ({
onClose, onClose,
libraryItems, libraryItems,
@@ -201,12 +129,59 @@ const PublishLibrary = ({
setIsSubmitting(false); setIsSubmitting(false);
return; return;
} }
const elements: ExcalidrawElement[] = [];
const prevBoundingBox = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
clonedLibItems.forEach((libItem) => {
const boundingBox = getCommonBoundingBox(libItem.elements);
const width = boundingBox.maxX - boundingBox.minX + 30;
const height = boundingBox.maxY - boundingBox.minY + 30;
const offset = {
x: prevBoundingBox.maxX - boundingBox.minX,
y: prevBoundingBox.maxY - boundingBox.minY,
};
const previewImage = await generatePreviewImage(clonedLibItems); const itemsWithUpdatedCoords = libItem.elements.map((element) => {
element = mutateElement(element, {
x: element.x + offset.x + 15,
y: element.y + offset.y + 15,
});
return element;
});
const items = [
...itemsWithUpdatedCoords,
newElement({
type: "rectangle",
width,
height,
x: prevBoundingBox.maxX,
y: prevBoundingBox.maxY,
strokeColor: "#ced4da",
backgroundColor: "transparent",
strokeStyle: "solid",
opacity: 100,
roughness: 0,
strokeSharpness: "sharp",
fillStyle: "solid",
strokeWidth: 1,
}),
];
elements.push(...items);
prevBoundingBox.maxX = prevBoundingBox.maxX + width + 30;
});
const png = await exportToBlob({
elements,
mimeType: "image/png",
appState: {
...appState,
viewBackgroundColor: oc.white,
exportBackground: true,
},
files: null,
});
const libContent: ExportedLibraryData = { const libContent: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary, type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: VERSIONS.excalidrawLibrary, version: 2,
source: EXPORT_SOURCE, source: EXPORT_SOURCE,
libraryItems: clonedLibItems, libraryItems: clonedLibItems,
}; };
@@ -215,8 +190,7 @@ const PublishLibrary = ({
const formData = new FormData(); const formData = new FormData();
formData.append("excalidrawLib", lib); formData.append("excalidrawLib", lib);
formData.append("previewImage", previewImage); formData.append("excalidrawPng", png!);
formData.append("previewImageType", previewImage.type);
formData.append("title", libraryData.name); formData.append("title", libraryData.name);
formData.append("authorName", libraryData.authorName); formData.append("authorName", libraryData.authorName);
formData.append("githubHandle", libraryData.githubHandle); formData.append("githubHandle", libraryData.githubHandle);
+28 -21
View File
@@ -8,7 +8,17 @@
position: relative; position: relative;
cursor: pointer; cursor: pointer;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
border-radius: var(--space-factor);
user-select: none; user-select: none;
background-color: var(--button-gray-1);
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
} }
.ToolIcon--plain { .ToolIcon--plain {
@@ -19,20 +29,6 @@
} }
} }
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
& + .ToolIcon__icon {
background-color: var(--button-gray-1);
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
}
}
.ToolIcon__icon { .ToolIcon__icon {
width: 2.5rem; width: 2.5rem;
height: 2.5rem; height: 2.5rem;
@@ -42,11 +38,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: var(--border-radius-lg); border-radius: var(--space-factor);
& + .ToolIcon__label {
margin-inline-start: 0;
}
svg { svg {
position: relative; position: relative;
@@ -54,6 +46,10 @@
fill: var(--icon-fill-color); fill: var(--icon-fill-color);
color: var(--icon-fill-color); color: var(--icon-fill-color);
} }
& + .ToolIcon__label {
margin-inline-start: 0;
}
} }
.ToolIcon__label { .ToolIcon__label {
@@ -83,7 +79,7 @@
margin: 0; margin: 0;
font-size: inherit; font-size: inherit;
&:focus-visible { &:focus {
box-shadow: 0 0 0 2px var(--focus-highlight-color); box-shadow: 0 0 0 2px var(--focus-highlight-color);
} }
@@ -125,7 +121,7 @@
} }
} }
&:focus-visible + .ToolIcon__icon { &:focus + .ToolIcon__icon {
box-shadow: 0 0 0 2px var(--focus-highlight-color); box-shadow: 0 0 0 2px var(--focus-highlight-color);
} }
@@ -145,6 +141,10 @@
background-color: transparent; background-color: transparent;
} }
&:focus {
box-shadow: none;
}
.ToolIcon__icon { .ToolIcon__icon {
background-color: var(--button-gray-1); background-color: var(--button-gray-1);
&:hover { &:hover {
@@ -159,6 +159,13 @@
} }
} }
.ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}
}
.ToolIcon__keybinding { .ToolIcon__keybinding {
position: absolute; position: absolute;
bottom: 2px; bottom: 2px;
-112
View File
@@ -1,112 +0,0 @@
@import "open-color/open-color.scss";
@mixin toolbarButtonColorStates {
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
& + .ToolIcon__icon:active {
background: var(--color-primary-light);
}
&:checked + .ToolIcon__icon {
background: var(--color-primary);
--icon-fill-color: #{$oc-white};
--keybinding-color: #{$oc-white};
}
&:checked + .ToolIcon__icon:active {
background: var(--color-primary-darker);
}
}
.ToolIcon__keybinding {
bottom: 4px;
right: 4px;
}
}
.excalidraw {
.App-toolbar-container {
.ToolIcon_type_floating {
@include toolbarButtonColorStates;
&:not(.is-mobile) {
.ToolIcon__icon {
padding: 1px;
background-color: var(--island-bg-color);
box-shadow: 1px 3px 4px 0px rgb(0 0 0 / 15%);
border-radius: 50%;
transition: box-shadow 0.5s ease, transform 0.5s ease;
}
}
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
&:focus-within + .ToolIcon__icon {
// override for custom floating button shadow
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
}
}
.ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}
}
.ToolIcon__library {
margin-inline-start: var(--space-factor);
}
&.zen-mode {
.ToolIcon_type_floating {
.ToolIcon__icon {
box-shadow: none;
transform: scale(0.9);
}
.ToolIcon_type_checkbox:not(:checked):not(:hover):not(:active) {
& + .ToolIcon__icon {
svg {
fill: $oc-gray-5;
color: $oc-gray-5;
}
}
}
}
}
}
.App-toolbar {
border-radius: var(--border-radius-lg);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.01), 1px 1px 5px rgb(0 0 0 / 15%);
.ToolIcon {
&:hover {
--icon-fill-color: var(--color-primary-chubb);
--keybinding-color: var(--color-primary-chubb);
}
&:active {
--icon-fill-color: #{$oc-gray-9};
--keybinding-color: #{$oc-gray-9};
}
.ToolIcon__icon {
background: transparent;
border-radius: var(--border-radius-lg);
}
@include toolbarButtonColorStates;
}
&.zen-mode {
.ToolIcon__keybinding,
.HintViewer {
display: none;
}
}
}
&.theme--dark .App-toolbar .ToolIcon:active {
--icon-fill-color: #{$oc-gray-3};
--keybinding-color: #{$oc-gray-3};
}
}
+1
View File
@@ -29,6 +29,7 @@
// wraps the element we want to apply the tooltip to // wraps the element we want to apply the tooltip to
.excalidraw-tooltip-wrapper { .excalidraw-tooltip-wrapper {
display: flex; display: flex;
height: 100%;
} }
.excalidraw-tooltip-icon { .excalidraw-tooltip-icon {
+1 -8
View File
@@ -62,15 +62,9 @@ type TooltipProps = {
children: React.ReactNode; children: React.ReactNode;
label: string; label: string;
long?: boolean; long?: boolean;
style?: React.CSSProperties;
}; };
export const Tooltip = ({ export const Tooltip = ({ children, label, long = false }: TooltipProps) => {
children,
label,
long = false,
style,
}: TooltipProps) => {
useEffect(() => { useEffect(() => {
return () => return () =>
getTooltipDiv().classList.remove("excalidraw-tooltip--visible"); getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
@@ -90,7 +84,6 @@ export const Tooltip = ({
onPointerLeave={() => onPointerLeave={() =>
getTooltipDiv().classList.remove("excalidraw-tooltip--visible") getTooltipDiv().classList.remove("excalidraw-tooltip--visible")
} }
style={style}
> >
{children} {children}
</div> </div>
-4
View File
@@ -7,10 +7,6 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end; justify-content: flex-end;
&:empty {
display: none;
}
} }
.UserList > * { .UserList > * {
+2 -11
View File
@@ -15,9 +15,8 @@ import { THEME } from "../constants";
const activeElementColor = (theme: Theme) => const activeElementColor = (theme: Theme) =>
theme === THEME.LIGHT ? oc.orange[4] : oc.orange[9]; theme === THEME.LIGHT ? oc.orange[4] : oc.orange[9];
const iconFillColor = (theme: Theme) =>
const iconFillColor = (theme: Theme) => "var(--icon-fill-color)"; theme === THEME.LIGHT ? oc.black : oc.gray[4];
const handlerColor = (theme: Theme) => const handlerColor = (theme: Theme) =>
theme === THEME.LIGHT ? oc.white : "#1e1e1e"; theme === THEME.LIGHT ? oc.white : "#1e1e1e";
@@ -89,14 +88,6 @@ export const trash = createIcon(
{ width: 448, height: 512 }, { width: 448, height: 512 },
); );
export const backgroundIcon = createIcon(
<path
fill="currentColor"
d="M512 320s-64 92.65-64 128c0 35.35 28.66 64 64 64s64-28.65 64-64-64-128-64-128zm-9.37-102.94L294.94 9.37C288.69 3.12 280.5 0 272.31 0s-16.38 3.12-22.62 9.37l-81.58 81.58L81.93 4.76c-6.25-6.25-16.38-6.25-22.62 0L36.69 27.38c-6.24 6.25-6.24 16.38 0 22.62l86.19 86.18-94.76 94.76c-37.49 37.48-37.49 98.26 0 135.75l117.19 117.19c18.74 18.74 43.31 28.12 67.87 28.12 24.57 0 49.13-9.37 67.87-28.12l221.57-221.57c12.5-12.5 12.5-32.75.01-45.25zm-116.22 70.97H65.93c1.36-3.84 3.57-7.98 7.43-11.83l13.15-13.15 81.61-81.61 58.6 58.6c12.49 12.49 32.75 12.49 45.24 0s12.49-32.75 0-45.24l-58.6-58.6 58.95-58.95 162.44 162.44-48.34 48.34z"
></path>,
{ width: 576, height: 512 },
);
export const palette = createIcon( export const palette = createIcon(
"M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z", "M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z",
+2 -8
View File
@@ -162,7 +162,8 @@ export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
export const EXPORT_SCALES = [1, 2, 3]; export const EXPORT_SCALES = [1, 2, 3];
export const DEFAULT_EXPORT_PADDING = 10; // px export const DEFAULT_EXPORT_PADDING = 10; // px
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440; export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT_JPG = 10000;
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT_OTHER = 1440;
export const ALLOWED_IMAGE_MIME_TYPES = [ export const ALLOWED_IMAGE_MIME_TYPES = [
MIME_TYPES.png, MIME_TYPES.png,
@@ -176,10 +177,3 @@ export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
export const SVG_NS = "http://www.w3.org/2000/svg"; export const SVG_NS = "http://www.w3.org/2000/svg";
export const ENCRYPTION_KEY_BITS = 128; export const ENCRYPTION_KEY_BITS = 128;
export const VERSIONS = {
excalidraw: 2,
excalidrawLibrary: 2,
} as const;
export const PADDING = 30;
+8 -10
View File
@@ -180,7 +180,7 @@
} }
.buttonList label:focus-within, .buttonList label:focus-within,
input:focus-visible { input:focus {
outline: transparent; outline: transparent;
box-shadow: 0 0 0 2px var(--focus-highlight-color); box-shadow: 0 0 0 2px var(--focus-highlight-color);
} }
@@ -190,14 +190,14 @@
user-select: none; user-select: none;
background-color: var(--button-gray-1); background-color: var(--button-gray-1);
border: 0; border: 0;
border-radius: var(--border-radius-md); border-radius: 4px;
margin: 0.125rem 0; margin: 0.125rem 0;
padding: 0.25rem; padding: 0.25rem;
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
&:focus-visible { &:focus {
outline: transparent; outline: transparent;
box-shadow: 0 0 0 2px var(--focus-highlight-color); box-shadow: 0 0 0 2px var(--focus-highlight-color);
} }
@@ -217,16 +217,14 @@
.active, .active,
.buttonList label.active { .buttonList label.active {
background-color: var(--color-primary); background-color: var(--button-gray-2);
--icon-fill-color: #{$oc-white};
&:hover { &:hover {
background-color: var(--color-primary-darker); background-color: var(--button-gray-2);
} }
&:active { &:active {
background-color: var(--color-primary-darkest); background-color: var(--button-gray-3);
} }
} }
@@ -236,7 +234,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
svg { svg {
width: 35px; width: 36px;
height: 14px; height: 14px;
padding: 2px; padding: 2px;
opacity: 0.6; opacity: 0.6;
@@ -313,7 +311,7 @@
} }
.App-menu_top { .App-menu_top {
grid-template-columns: auto max-content auto; grid-template-columns: 1fr auto 1fr;
grid-gap: 4px; grid-gap: 4px;
align-items: flex-start; align-items: flex-start;
cursor: default; cursor: default;
+3 -19
View File
@@ -12,7 +12,7 @@
--dialog-border-color: #{$oc-gray-6}; --dialog-border-color: #{$oc-gray-6};
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>'); --dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
--focus-highlight-color: #{$oc-blue-2}; --focus-highlight-color: #{$oc-blue-2};
--icon-fill-color: #{$oc-gray-9}; --icon-fill-color: #{$oc-black};
--icon-green-fill-color: #{$oc-green-9}; --icon-green-fill-color: #{$oc-green-9};
--default-bg-color: #{$oc-white}; --default-bg-color: #{$oc-white};
--input-bg-color: #{$oc-white}; --input-bg-color: #{$oc-white};
@@ -32,20 +32,10 @@
--sar: env(safe-area-inset-right); --sar: env(safe-area-inset-right);
--sat: env(safe-area-inset-top); --sat: env(safe-area-inset-top);
--select-highlight-color: #{$oc-blue-5}; --select-highlight-color: #{$oc-blue-5};
--shadow-island: 0 0 0 1px rgba(0, 0, 0, 0.01), 1px 1px 5px rgb(0 0 0 / 12%); --shadow-island: 0 1px 5px #{transparentize($oc-black, 0.85)};
--space-factor: 0.25rem; --space-factor: 0.25rem;
--text-primary-color: #{$oc-gray-8}; --text-primary-color: #{$oc-gray-8};
--color-primary: #6965db;
--color-primary-chubb: #625ee0; // to offset Chubb illusion
--color-primary-darker: #5b57d1;
--color-primary-darkest: #4a47b1;
--color-primary-light: #e2e1fc;
--border-radius-md: 0.375rem;
--border-radius-lg: 0.5rem;
&.theme--dark { &.theme--dark {
background: $oc-black; background: $oc-black;
@@ -81,13 +71,7 @@
--popup-text-color: #{$oc-gray-4}; --popup-text-color: #{$oc-gray-4};
--popup-text-inverted-color: #2c2c2c; --popup-text-inverted-color: #2c2c2c;
--select-highlight-color: #{$oc-blue-4}; --select-highlight-color: #{$oc-blue-4};
--shadow-island: 1px 1px 5px #{transparentize($oc-black, 0.7)}; --shadow-island: 0 1px 5px #{transparentize($oc-black, 0.7)};
--text-primary-color: #{$oc-gray-4}; --text-primary-color: #{$oc-gray-4};
--color-primary: #5650f0;
--color-primary-chubb: #726dff; // to offset Chubb illusion
--color-primary-darker: #4b46d8;
--color-primary-darkest: #3e39be;
--color-primary-light: #3f3d64;
} }
} }
+3 -1
View File
@@ -271,6 +271,8 @@ export const resizeImageFile = async (
}; };
} }
const fileType = file.type;
if (!isSupportedImageFile(file)) { if (!isSupportedImageFile(file)) {
throw new Error(t("errors.unsupportedFileType")); throw new Error(t("errors.unsupportedFileType"));
} }
@@ -279,7 +281,7 @@ export const resizeImageFile = async (
[await reduce.toBlob(file, { max: opts.maxWidthOrHeight })], [await reduce.toBlob(file, { max: opts.maxWidthOrHeight })],
file.name, file.name,
{ {
type: opts.outputType || file.type, type: fileType,
}, },
); );
}; };
+1 -13
View File
@@ -234,19 +234,7 @@ const splitBuffers = (concatenatedBuffer: Uint8Array) => {
let cursor = 0; let cursor = 0;
// first chunk is the version // first chunk is the version (ignored for now)
const version = dataView(
concatenatedBuffer,
NEXT_CHUNK_SIZE_DATAVIEW_BYTES,
cursor,
);
// If version is outside of the supported versions, throw an error.
// This usually means the buffer wasn't encoded using this API, so we'd only
// waste compute.
if (version > CONCAT_BUFFERS_VERSION) {
throw new Error(`invalid version ${version}`);
}
cursor += VERSION_DATAVIEW_BYTES; cursor += VERSION_DATAVIEW_BYTES;
while (true) { while (true) {
+32 -3
View File
@@ -1,8 +1,11 @@
import decodePng from "png-chunks-extract"; import extractPngChunks from "png-chunks-extract";
import tEXt from "png-chunk-text"; import tEXt from "png-chunk-text";
import encodePng from "png-chunks-encode"; import encodePng from "png-chunks-encode";
import { stringToBase64, encode, decode, base64ToString } from "./encode"; import { stringToBase64, encode, decode, base64ToString } from "./encode";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants"; import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import { PngChunk } from "../types";
export { extractPngChunks };
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// PNG // PNG
@@ -28,7 +31,9 @@ const blobToArrayBuffer = (blob: Blob): Promise<ArrayBuffer> => {
export const getTEXtChunk = async ( export const getTEXtChunk = async (
blob: Blob, blob: Blob,
): Promise<{ keyword: string; text: string } | null> => { ): Promise<{ keyword: string; text: string } | null> => {
const chunks = decodePng(new Uint8Array(await blobToArrayBuffer(blob))); const chunks = extractPngChunks(
new Uint8Array(await blobToArrayBuffer(blob)),
);
const metadataChunk = chunks.find((chunk) => chunk.name === "tEXt"); const metadataChunk = chunks.find((chunk) => chunk.name === "tEXt");
if (metadataChunk) { if (metadataChunk) {
return tEXt.decode(metadataChunk.data); return tEXt.decode(metadataChunk.data);
@@ -36,6 +41,28 @@ export const getTEXtChunk = async (
return null; return null;
}; };
export const findPngChunk = (
chunks: PngChunk[],
name: PngChunk["name"],
/** this makes the search stop before IDAT chunk (before which most
* metadata chunks reside). This is a perf optim. */
breakBeforeIDAT = true,
) => {
let i = 0;
const len = chunks.length;
while (i <= len) {
const chunk = chunks[i];
if (chunk.name === name) {
return chunk;
}
if (breakBeforeIDAT && chunk.name === "IDAT") {
return null;
}
i++;
}
return null;
};
export const encodePngMetadata = async ({ export const encodePngMetadata = async ({
blob, blob,
metadata, metadata,
@@ -43,7 +70,9 @@ export const encodePngMetadata = async ({
blob: Blob; blob: Blob;
metadata: string; metadata: string;
}) => { }) => {
const chunks = decodePng(new Uint8Array(await blobToArrayBuffer(blob))); const chunks = extractPngChunks(
new Uint8Array(await blobToArrayBuffer(blob)),
);
const metadataChunk = tEXt.encode( const metadataChunk = tEXt.encode(
MIME_TYPES.excalidraw, MIME_TYPES.excalidraw,
+3 -8
View File
@@ -1,11 +1,6 @@
import { fileOpen, fileSave } from "./filesystem"; import { fileOpen, fileSave } from "./filesystem";
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState"; import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
import { import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants";
EXPORT_DATA_TYPES,
EXPORT_SOURCE,
MIME_TYPES,
VERSIONS,
} from "../constants";
import { clearElementsForDatabase, clearElementsForExport } from "../element"; import { clearElementsForDatabase, clearElementsForExport } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState, BinaryFiles, LibraryItems } from "../types"; import { AppState, BinaryFiles, LibraryItems } from "../types";
@@ -47,7 +42,7 @@ export const serializeAsJSON = (
): string => { ): string => {
const data: ExportedDataState = { const data: ExportedDataState = {
type: EXPORT_DATA_TYPES.excalidraw, type: EXPORT_DATA_TYPES.excalidraw,
version: VERSIONS.excalidraw, version: 2,
source: EXPORT_SOURCE, source: EXPORT_SOURCE,
elements: elements:
type === "local" type === "local"
@@ -126,7 +121,7 @@ export const isValidLibrary = (json: any) => {
export const saveLibraryAsJSON = async (libraryItems: LibraryItems) => { export const saveLibraryAsJSON = async (libraryItems: LibraryItems) => {
const data: ExportedLibraryData = { const data: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary, type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: VERSIONS.excalidrawLibrary, version: 2,
source: EXPORT_SOURCE, source: EXPORT_SOURCE,
libraryItems, libraryItems,
}; };
+9 -15
View File
@@ -10,7 +10,11 @@ import {
NormalizedZoomValue, NormalizedZoomValue,
} from "../types"; } from "../types";
import { ImportedDataState } from "./types"; import { ImportedDataState } from "./types";
import { getNormalizedDimensions, isInvisiblySmallElement } from "../element"; import {
getElementMap,
getNormalizedDimensions,
isInvisiblySmallElement,
} from "../element";
import { isLinearElementType } from "../element/typeChecks"; import { isLinearElementType } from "../element/typeChecks";
import { randomId } from "../random"; import { randomId } from "../random";
import { import {
@@ -22,8 +26,6 @@ import {
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { bumpVersion } from "../element/mutateElement"; import { bumpVersion } from "../element/mutateElement";
import { getUpdatedTimestamp } from "../utils";
import { arrayToMap } from "../utils";
type RestoredAppState = Omit< type RestoredAppState = Omit<
AppState, AppState,
@@ -64,10 +66,7 @@ const restoreElementWithProperties = <
T extends ExcalidrawElement, T extends ExcalidrawElement,
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>, K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
>( >(
element: Required<T> & { element: Required<T>,
/** @deprecated */
boundElementIds?: readonly ExcalidrawElement["id"][];
},
extra: Pick< extra: Pick<
T, T,
// This extra Pick<T, keyof K> ensure no excess properties are passed. // This extra Pick<T, keyof K> ensure no excess properties are passed.
@@ -101,10 +100,7 @@ const restoreElementWithProperties = <
strokeSharpness: strokeSharpness:
element.strokeSharpness ?? element.strokeSharpness ??
(isLinearElementType(element.type) ? "round" : "sharp"), (isLinearElementType(element.type) ? "round" : "sharp"),
boundElements: element.boundElementIds boundElementIds: element.boundElementIds ?? [],
? element.boundElementIds.map((id) => ({ type: "arrow", id }))
: element.boundElements ?? [],
updated: element.updated ?? getUpdatedTimestamp(),
}; };
return { return {
@@ -135,8 +131,6 @@ const restoreElement = (
baseline: element.baseline, baseline: element.baseline,
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN, textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN, verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
containerId: element.containerId ?? null,
originalText: element.originalText ?? "",
}); });
case "freedraw": { case "freedraw": {
return restoreElementWithProperties(element, { return restoreElementWithProperties(element, {
@@ -210,14 +204,14 @@ export const restoreElements = (
/** NOTE doesn't serve for reconciliation */ /** NOTE doesn't serve for reconciliation */
localElements: readonly ExcalidrawElement[] | null | undefined, localElements: readonly ExcalidrawElement[] | null | undefined,
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
const localElementsMap = localElements ? arrayToMap(localElements) : null; const localElementsMap = localElements ? getElementMap(localElements) : null;
return (elements || []).reduce((elements, element) => { return (elements || []).reduce((elements, element) => {
// filtering out selection, which is legacy, no longer kept in elements, // filtering out selection, which is legacy, no longer kept in elements,
// and causing issues if retained // and causing issues if retained
if (element.type !== "selection" && !isInvisiblySmallElement(element)) { if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
let migratedElement: ExcalidrawElement | null = restoreElement(element); let migratedElement: ExcalidrawElement | null = restoreElement(element);
if (migratedElement) { if (migratedElement) {
const localElement = localElementsMap?.get(element.id); const localElement = localElementsMap?.[element.id];
if (localElement && localElement.version > migratedElement.version) { if (localElement && localElement.version > migratedElement.version) {
migratedElement = bumpVersion(migratedElement, localElement.version); migratedElement = bumpVersion(migratedElement, localElement.version);
} }
+1 -2
View File
@@ -1,7 +1,6 @@
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState, BinaryFiles, LibraryItems, LibraryItems_v1 } from "../types"; import { AppState, BinaryFiles, LibraryItems, LibraryItems_v1 } from "../types";
import type { cleanAppStateForExport } from "../appState"; import type { cleanAppStateForExport } from "../appState";
import { VERSIONS } from "../constants";
export interface ExportedDataState { export interface ExportedDataState {
type: string; type: string;
@@ -25,7 +24,7 @@ export interface ImportedDataState {
export interface ExportedLibraryData { export interface ExportedLibraryData {
type: string; type: string;
version: typeof VERSIONS.excalidrawLibrary; version: 2;
source: string; source: string;
libraryItems: LibraryItems; libraryItems: LibraryItems;
} }
+65 -90
View File
@@ -8,11 +8,7 @@ import {
} from "./types"; } from "./types";
import { getElementAtPosition } from "../scene"; import { getElementAtPosition } from "../scene";
import { AppState } from "../types"; import { AppState } from "../types";
import { import { isBindableElement, isBindingElement } from "./typeChecks";
isBindableElement,
isBindingElement,
isLinearElement,
} from "./typeChecks";
import { import {
bindingBorderTest, bindingBorderTest,
distanceToBindableElement, distanceToBindableElement,
@@ -24,7 +20,7 @@ import {
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { arrayToMap, tupleToCoors } from "../utils"; import { tupleToCoors } from "../utils";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
export type SuggestedBinding = export type SuggestedBinding =
@@ -78,9 +74,8 @@ export const bindOrUnbindLinearElement = (
.getNonDeletedElements(onlyUnbound) .getNonDeletedElements(onlyUnbound)
.forEach((element) => { .forEach((element) => {
mutateElement(element, { mutateElement(element, {
boundElements: element.boundElements?.filter( boundElementIds: element.boundElementIds?.filter(
(element) => (id) => id !== linearElement.id,
element.type !== "arrow" || element.id !== linearElement.id,
), ),
}); });
}); });
@@ -185,16 +180,11 @@ const bindLinearElement = (
...calculateFocusAndGap(linearElement, hoveredElement, startOrEnd), ...calculateFocusAndGap(linearElement, hoveredElement, startOrEnd),
} as PointBinding, } as PointBinding,
}); });
mutateElement(hoveredElement, {
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []); boundElementIds: Array.from(
if (!boundElementsMap.has(linearElement.id)) { new Set([...(hoveredElement.boundElementIds ?? []), linearElement.id]),
mutateElement(hoveredElement, { ),
boundElements: (hoveredElement.boundElements || []).concat({ });
id: linearElement.id,
type: "arrow",
}),
});
}
}; };
// Don't bind both ends of a simple segment // Don't bind both ends of a simple segment
@@ -294,56 +284,52 @@ export const updateBoundElements = (
newSize?: { width: number; height: number }; newSize?: { width: number; height: number };
}, },
) => { ) => {
const boundLinearElements = (changedElement.boundElements ?? []).filter( const boundElementIds = changedElement.boundElementIds ?? [];
(el) => el.type === "arrow", if (boundElementIds.length === 0) {
);
if (boundLinearElements.length === 0) {
return; return;
} }
const { newSize, simultaneouslyUpdated } = options ?? {}; const { newSize, simultaneouslyUpdated } = options ?? {};
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
simultaneouslyUpdated, simultaneouslyUpdated,
); );
Scene.getScene(changedElement)! (
.getNonDeletedElements(boundLinearElements.map((el) => el.id)) Scene.getScene(changedElement)!.getNonDeletedElements(
.forEach((element) => { boundElementIds,
if (!isLinearElement(element)) { ) as NonDeleted<ExcalidrawLinearElement>[]
return; ).forEach((linearElement) => {
} const bindableElement = changedElement as ExcalidrawBindableElement;
// In case the boundElementIds are stale
const bindableElement = changedElement as ExcalidrawBindableElement; if (!doesNeedUpdate(linearElement, bindableElement)) {
// In case the boundElements are stale return;
if (!doesNeedUpdate(element, bindableElement)) { }
return; const startBinding = maybeCalculateNewGapWhenScaling(
} bindableElement,
const startBinding = maybeCalculateNewGapWhenScaling( linearElement.startBinding,
bindableElement, newSize,
element.startBinding, );
newSize, const endBinding = maybeCalculateNewGapWhenScaling(
); bindableElement,
const endBinding = maybeCalculateNewGapWhenScaling( linearElement.endBinding,
bindableElement, newSize,
element.endBinding, );
newSize, // `linearElement` is being moved/scaled already, just update the binding
); if (simultaneouslyUpdatedElementIds.has(linearElement.id)) {
// `linearElement` is being moved/scaled already, just update the binding mutateElement(linearElement, { startBinding, endBinding });
if (simultaneouslyUpdatedElementIds.has(element.id)) { return;
mutateElement(element, { startBinding, endBinding }); }
return; updateBoundPoint(
} linearElement,
updateBoundPoint( "start",
element, startBinding,
"start", changedElement as ExcalidrawBindableElement,
startBinding, );
changedElement as ExcalidrawBindableElement, updateBoundPoint(
); linearElement,
updateBoundPoint( "end",
element, endBinding,
"end", changedElement as ExcalidrawBindableElement,
endBinding, );
changedElement as ExcalidrawBindableElement, });
);
});
}; };
const doesNeedUpdate = ( const doesNeedUpdate = (
@@ -415,17 +401,10 @@ const updateBoundPoint = (
newEdgePoint = intersections[0]; newEdgePoint = intersections[0];
} }
} }
LinearElementEditor.movePoints( LinearElementEditor.movePoint(
linearElement, linearElement,
[ edgePointIndex,
{ LinearElementEditor.pointFromAbsoluteCoords(linearElement, newEdgePoint),
index: edgePointIndex,
point: LinearElementEditor.pointFromAbsoluteCoords(
linearElement,
newEdgePoint,
),
},
],
{ [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding }, { [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding },
); );
}; };
@@ -573,11 +552,11 @@ export const fixBindingsAfterDuplication = (
const allBindableElementIds: Set<ExcalidrawElement["id"]> = new Set(); const allBindableElementIds: Set<ExcalidrawElement["id"]> = new Set();
const shouldReverseRoles = duplicatesServeAsOld === "duplicatesServeAsOld"; const shouldReverseRoles = duplicatesServeAsOld === "duplicatesServeAsOld";
oldElements.forEach((oldElement) => { oldElements.forEach((oldElement) => {
const { boundElements } = oldElement; const { boundElementIds } = oldElement;
if (boundElements != null && boundElements.length > 0) { if (boundElementIds != null && boundElementIds.length > 0) {
boundElements.forEach((boundElement) => { boundElementIds.forEach((boundElementId) => {
if (shouldReverseRoles && !oldIdToDuplicatedId.has(boundElement.id)) { if (shouldReverseRoles && !oldIdToDuplicatedId.has(boundElementId)) {
allBoundElementIds.add(boundElement.id); allBoundElementIds.add(boundElementId);
} }
}); });
allBindableElementIds.add(oldIdToDuplicatedId.get(oldElement.id)!); allBindableElementIds.add(oldIdToDuplicatedId.get(oldElement.id)!);
@@ -621,16 +600,12 @@ export const fixBindingsAfterDuplication = (
sceneElements sceneElements
.filter(({ id }) => allBindableElementIds.has(id)) .filter(({ id }) => allBindableElementIds.has(id))
.forEach((bindableElement) => { .forEach((bindableElement) => {
const { boundElements } = bindableElement; const { boundElementIds } = bindableElement;
if (boundElements != null && boundElements.length > 0) { if (boundElementIds != null && boundElementIds.length > 0) {
mutateElement(bindableElement, { mutateElement(bindableElement, {
boundElements: boundElements.map((boundElement) => boundElementIds: boundElementIds.map(
oldIdToDuplicatedId.has(boundElement.id) (boundElementId) =>
? { oldIdToDuplicatedId.get(boundElementId) ?? boundElementId,
id: oldIdToDuplicatedId.get(boundElement.id)!,
type: boundElement.type,
}
: boundElement,
), ),
}); });
} }
@@ -663,9 +638,9 @@ export const fixBindingsAfterDeletion = (
const boundElementIds: Set<ExcalidrawElement["id"]> = new Set(); const boundElementIds: Set<ExcalidrawElement["id"]> = new Set();
deletedElements.forEach((deletedElement) => { deletedElements.forEach((deletedElement) => {
if (isBindableElement(deletedElement)) { if (isBindableElement(deletedElement)) {
deletedElement.boundElements?.forEach((element) => { deletedElement.boundElementIds?.forEach((id) => {
if (!deletedElementIds.has(element.id)) { if (!deletedElementIds.has(id)) {
boundElementIds.add(element.id); boundElementIds.add(id);
} }
}); });
} }
+11 -12
View File
@@ -31,9 +31,7 @@ import { Point } from "../types";
import { Drawable } from "roughjs/bin/core"; import { Drawable } from "roughjs/bin/core";
import { AppState } from "../types"; import { AppState } from "../types";
import { getShapeForElement } from "../renderer/renderElement"; import { getShapeForElement } from "../renderer/renderElement";
import { hasBoundTextElement, isImageElement } from "./typeChecks"; import { isImageElement } from "./typeChecks";
import { isTextElement } from ".";
import { isTransparent } from "../utils";
const isElementDraggableFromInside = ( const isElementDraggableFromInside = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
@@ -45,9 +43,9 @@ const isElementDraggableFromInside = (
if (element.type === "freedraw") { if (element.type === "freedraw") {
return true; return true;
} }
const isDraggableFromInside =
!isTransparent(element.backgroundColor) || const isDraggableFromInside = element.backgroundColor !== "transparent";
(isTransparent(element.backgroundColor) && hasBoundTextElement(element));
if (element.type === "line") { if (element.type === "line") {
return isDraggableFromInside && isPathALoop(element.points); return isDraggableFromInside && isPathALoop(element.points);
} }
@@ -85,18 +83,19 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
); );
}; };
export const isHittingElementNotConsideringBoundingBox = ( const isHittingElementNotConsideringBoundingBox = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
point: Point, point: Point,
): boolean => { ): boolean => {
const threshold = 10 / appState.zoom.value; const threshold = 10 / appState.zoom.value;
const check = isTextElement(element) const check =
? isStrictlyInside element.type === "text"
: isElementDraggableFromInside(element) ? isStrictlyInside
? isInsideCheck : isElementDraggableFromInside(element)
: isNearCheck; ? isInsideCheck
: isNearCheck;
return hitTestPointAgainstElement({ element, point, threshold, check }); return hitTestPointAgainstElement({ element, point, threshold, check });
}; };
+18 -49
View File
@@ -6,13 +6,13 @@ import { getPerfectElementSize } from "./sizeHelpers";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { NonDeletedExcalidrawElement } from "./types"; import { NonDeletedExcalidrawElement } from "./types";
import { PointerDownState } from "../types"; import { PointerDownState } from "../types";
import { getBoundTextElementId } from "./textElement";
export const dragSelectedElements = ( export const dragSelectedElements = (
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
scene: Scene,
lockDirection: boolean = false, lockDirection: boolean = false,
distanceX: number = 0, distanceX: number = 0,
distanceY: number = 0, distanceY: number = 0,
@@ -20,61 +20,30 @@ export const dragSelectedElements = (
const [x1, y1] = getCommonBounds(selectedElements); const [x1, y1] = getCommonBounds(selectedElements);
const offset = { x: pointerX - x1, y: pointerY - y1 }; const offset = { x: pointerX - x1, y: pointerY - y1 };
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
updateElementCoords( let x: number;
lockDirection, let y: number;
distanceX, if (lockDirection) {
distanceY, const lockX = lockDirection && distanceX < distanceY;
pointerDownState, const lockY = lockDirection && distanceX > distanceY;
element, const original = pointerDownState.originalElements.get(element.id);
offset, x = lockX && original ? original.x : element.x + offset.x;
); y = lockY && original ? original.y : element.y + offset.y;
if (!element.groupIds.length) { } else {
const boundTextElementId = getBoundTextElementId(element); x = element.x + offset.x;
if (boundTextElementId) { y = element.y + offset.y;
const textElement =
Scene.getScene(element)!.getElement(boundTextElementId);
updateElementCoords(
lockDirection,
distanceX,
distanceY,
pointerDownState,
textElement!,
offset,
);
}
} }
mutateElement(element, {
x,
y,
});
updateBoundElements(element, { updateBoundElements(element, {
simultaneouslyUpdated: selectedElements, simultaneouslyUpdated: selectedElements,
}); });
}); });
}; };
const updateElementCoords = (
lockDirection: boolean,
distanceX: number,
distanceY: number,
pointerDownState: PointerDownState,
element: NonDeletedExcalidrawElement,
offset: { x: number; y: number },
) => {
let x: number;
let y: number;
if (lockDirection) {
const lockX = lockDirection && distanceX < distanceY;
const lockY = lockDirection && distanceX > distanceY;
const original = pointerDownState.originalElements.get(element.id);
x = lockX && original ? original.x : element.x + offset.x;
y = lockY && original ? original.y : element.y + offset.y;
} else {
x = element.x + offset.x;
y = element.y + offset.y;
}
mutateElement(element, {
x,
y,
});
};
export const getDragOffsetXY = ( export const getDragOffsetXY = (
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],
x: number, x: number,
+76 -10
View File
@@ -3,6 +3,7 @@
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
import { MIME_TYPES, SVG_NS } from "../constants"; import { MIME_TYPES, SVG_NS } from "../constants";
import { getDataURL } from "../data/blob";
import { t } from "../i18n"; import { t } from "../i18n";
import { AppClassProperties, DataURL, BinaryFiles } from "../types"; import { AppClassProperties, DataURL, BinaryFiles } from "../types";
import { isInitializedImageElement } from "./typeChecks"; import { isInitializedImageElement } from "./typeChecks";
@@ -110,15 +111,80 @@ export const normalizeSVG = async (SVGString: string) => {
} }
}; };
export const imageFromImageData = (imagedata: ImageData) => { /**
const canvas = document.createElement("canvas"); * To improve perf, uses `createImageBitmap` is available. But there are
const ctx = canvas.getContext("2d")!; * quality issues across browsers, so don't use this API where quality matters.
canvas.width = imagedata.width; */
canvas.height = imagedata.height; export const speedyImageToCanvas = async (imageFile: Blob | File) => {
ctx.putImageData(imagedata, 0, 0); let imageSrc: HTMLImageElement | ImageBitmap;
if (
typeof ImageBitmap !== "undefined" &&
ImageBitmap.prototype &&
ImageBitmap.prototype.close &&
window.createImageBitmap
) {
imageSrc = await window.createImageBitmap(imageFile);
} else {
imageSrc = await loadHTMLImageElement(await getDataURL(imageFile));
}
const { width, height } = imageSrc;
const image = new Image(); const canvas = document.createElement("canvas");
const dataURL = canvas.toDataURL() as DataURL; canvas.height = height;
image.src = dataURL; canvas.width = width;
return { image, dataURL }; const context = canvas.getContext("2d")!;
context.drawImage(imageSrc, 0, 0, width, height);
if (typeof ImageBitmap !== "undefined" && imageSrc instanceof ImageBitmap) {
imageSrc.close();
}
return { canvas, context, width, height };
};
/**
* Does its best at figuring out if an image (PNG) has any (semi)transparent
* pixels. If not PNG, always returns false.
*/
export const hasTransparentPixels = async (imageFile: Blob | File) => {
if (imageFile.type !== MIME_TYPES.png) {
return false;
}
const { findPngChunk, extractPngChunks } = await import("../data/image");
const buffer = await imageFile.arrayBuffer();
const chunks = extractPngChunks(new Uint8Array(buffer));
// early exit if tRNS not found and IHDR states no support for alpha
// -----------------------------------------------------------------------
const IHDR = findPngChunk(chunks, "IHDR");
if (
IHDR &&
IHDR.data[9] !== 4 &&
IHDR.data[9] !== 6 &&
!findPngChunk(chunks, "tRNS")
) {
return false;
}
// otherwise loop through pixels to check if there's any actually
// (semi)transparent pixel
// -----------------------------------------------------------------------
const { width, height, context } = await speedyImageToCanvas(imageFile);
{
const { data } = context.getImageData(0, 0, width, height);
const len = data.byteLength;
let i = 3;
while (i <= len) {
if (data[i] !== 255) {
return true;
}
i += 4;
}
}
return false;
}; };
-112
View File
@@ -1,112 +0,0 @@
import { distance2d } from "../math";
import Scene from "../scene/Scene";
import {
ExcalidrawImageElement,
InitializedExcalidrawImageElement,
} from "./types";
export type EditingImageElement = {
editorType: "alpha";
elementId: ExcalidrawImageElement["id"];
origImageData: Readonly<ImageData>;
imageData: ImageData;
pointerDownState: {
screenX: number;
screenY: number;
sampledPixel: readonly [number, number, number, number] | null;
};
};
const getElement = (id: EditingImageElement["elementId"]) => {
const element = Scene.getScene(id)?.getNonDeletedElement(id);
if (element) {
return element as InitializedExcalidrawImageElement;
}
return null;
};
export class ImageEditor {
static handlePointerDown(
editingElement: EditingImageElement,
scenePointer: { x: number; y: number },
) {
const imageElement = getElement(editingElement.elementId);
if (imageElement) {
if (
scenePointer.x >= imageElement.x &&
scenePointer.x <= imageElement.x + imageElement.width &&
scenePointer.y >= imageElement.y &&
scenePointer.y <= imageElement.y + imageElement.height
) {
editingElement.pointerDownState.screenX = scenePointer.x;
editingElement.pointerDownState.screenY = scenePointer.y;
const { width, height, data } = editingElement.origImageData;
const imageOffsetX = Math.round(
(scenePointer.x - imageElement.x) * (width / imageElement.width),
);
const imageOffsetY = Math.round(
(scenePointer.y - imageElement.y) * (height / imageElement.height),
);
const sampledPixel = [
data[(imageOffsetY * width + imageOffsetX) * 4 + 0],
data[(imageOffsetY * width + imageOffsetX) * 4 + 1],
data[(imageOffsetY * width + imageOffsetX) * 4 + 2],
data[(imageOffsetY * width + imageOffsetX) * 4 + 3],
] as const;
editingElement.pointerDownState.sampledPixel = sampledPixel;
}
}
}
static handlePointerMove(
editingElement: EditingImageElement,
scenePointer: { x: number; y: number },
) {
const { sampledPixel } = editingElement.pointerDownState;
if (sampledPixel) {
const { screenX, screenY } = editingElement.pointerDownState;
const distance = distance2d(
scenePointer.x,
scenePointer.y,
screenX,
screenY,
);
const { width, height, data } = editingElement.origImageData;
const newImageData = new ImageData(width, height);
for (let x = 0; x < width; ++x) {
for (let y = 0; y < height; ++y) {
if (
Math.abs(sampledPixel[0] - data[(y * width + x) * 4 + 0]) +
Math.abs(sampledPixel[1] - data[(y * width + x) * 4 + 1]) +
Math.abs(sampledPixel[2] - data[(y * width + x) * 4 + 2]) <
distance
) {
newImageData.data[(y * width + x) * 4 + 0] = 0;
newImageData.data[(y * width + x) * 4 + 1] = 255;
newImageData.data[(y * width + x) * 4 + 2] = 0;
newImageData.data[(y * width + x) * 4 + 3] = 0;
} else {
for (let p = 0; p < 4; ++p) {
newImageData.data[(y * width + x) * 4 + p] =
data[(y * width + x) * 4 + p];
}
}
}
}
return newImageData;
}
}
static handlePointerUp(editingElement: EditingImageElement) {
editingElement.pointerDownState.sampledPixel = null;
editingElement.origImageData = editingElement.imageData;
}
}
+9
View File
@@ -59,6 +59,15 @@ export {
} from "./sizeHelpers"; } from "./sizeHelpers";
export { showSelectedShapeActions } from "./showSelectedShapeActions"; export { showSelectedShapeActions } from "./showSelectedShapeActions";
export const getElementMap = (elements: readonly ExcalidrawElement[]) =>
elements.reduce(
(acc: { [key: string]: ExcalidrawElement }, element: ExcalidrawElement) => {
acc[element.id] = element;
return acc;
},
{},
);
export const getSceneVersion = (elements: readonly ExcalidrawElement[]) => export const getSceneVersion = (elements: readonly ExcalidrawElement[]) =>
elements.reduce((acc, el) => acc + el.version, 0); elements.reduce((acc, el) => acc + el.version, 0);
+100 -415
View File
@@ -25,19 +25,11 @@ export class LinearElementEditor {
public elementId: ExcalidrawElement["id"] & { public elementId: ExcalidrawElement["id"] & {
_brand: "excalidrawLinearElementId"; _brand: "excalidrawLinearElementId";
}; };
/** indices */ public activePointIndex: number | null;
public selectedPointsIndices: readonly number[] | null;
public pointerDownState: Readonly<{
prevSelectedPointsIndices: readonly number[] | null;
/** index */
lastClickedPoint: number;
}>;
/** whether you're dragging a point */ /** whether you're dragging a point */
public isDragging: boolean; public isDragging: boolean;
public lastUncommittedPoint: Point | null; public lastUncommittedPoint: Point | null;
public pointerOffset: Readonly<{ x: number; y: number }>; public pointerOffset: { x: number; y: number };
public startBindingElement: ExcalidrawBindableElement | null | "keep"; public startBindingElement: ExcalidrawBindableElement | null | "keep";
public endBindingElement: ExcalidrawBindableElement | null | "keep"; public endBindingElement: ExcalidrawBindableElement | null | "keep";
@@ -48,16 +40,12 @@ export class LinearElementEditor {
Scene.mapElementToScene(this.elementId, scene); Scene.mapElementToScene(this.elementId, scene);
LinearElementEditor.normalizePoints(element); LinearElementEditor.normalizePoints(element);
this.selectedPointsIndices = null; this.activePointIndex = null;
this.lastUncommittedPoint = null; this.lastUncommittedPoint = null;
this.isDragging = false; this.isDragging = false;
this.pointerOffset = { x: 0, y: 0 }; this.pointerOffset = { x: 0, y: 0 };
this.startBindingElement = "keep"; this.startBindingElement = "keep";
this.endBindingElement = "keep"; this.endBindingElement = "keep";
this.pointerDownState = {
prevSelectedPointsIndices: null,
lastClickedPoint: -1,
};
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -78,58 +66,6 @@ export class LinearElementEditor {
return null; return null;
} }
static handleBoxSelection(
event: PointerEvent,
appState: AppState,
setState: React.Component<any, AppState>["setState"],
) {
if (
!appState.editingLinearElement ||
appState.draggingElement?.type !== "selection"
) {
return false;
}
const { editingLinearElement } = appState;
const { selectedPointsIndices, elementId } = editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return false;
}
const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(appState.draggingElement);
const pointsSceneCoords =
LinearElementEditor.getPointsGlobalCoordinates(element);
const nextSelectedPoints = pointsSceneCoords.reduce(
(acc: number[], point, index) => {
if (
(point[0] >= selectionX1 &&
point[0] <= selectionX2 &&
point[1] >= selectionY1 &&
point[1] <= selectionY2) ||
(event.shiftKey && selectedPointsIndices?.includes(index))
) {
acc.push(index);
}
return acc;
},
[],
);
setState({
editingLinearElement: {
...editingLinearElement,
selectedPointsIndices: nextSelectedPoints.length
? nextSelectedPoints
: null,
},
});
}
/** @returns whether point was dragged */ /** @returns whether point was dragged */
static handlePointDragging( static handlePointDragging(
appState: AppState, appState: AppState,
@@ -138,27 +74,21 @@ export class LinearElementEditor {
scenePointerY: number, scenePointerY: number,
maybeSuggestBinding: ( maybeSuggestBinding: (
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
pointSceneCoords: { x: number; y: number }[], startOrEnd: "start" | "end",
) => void, ) => void,
): boolean { ): boolean {
if (!appState.editingLinearElement) { if (!appState.editingLinearElement) {
return false; return false;
} }
const { editingLinearElement } = appState; const { editingLinearElement } = appState;
const { selectedPointsIndices, elementId, isDragging } = const { activePointIndex, elementId, isDragging } = editingLinearElement;
editingLinearElement;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId);
if (!element) { if (!element) {
return false; return false;
} }
// point that's being dragged (out of all selected points) if (activePointIndex != null && activePointIndex > -1) {
const draggingPoint = element.points[
editingLinearElement.pointerDownState.lastClickedPoint
] as [number, number] | undefined;
if (selectedPointsIndices && draggingPoint) {
if (isDragging === false) { if (isDragging === false) {
setState({ setState({
editingLinearElement: { editingLinearElement: {
@@ -168,79 +98,18 @@ export class LinearElementEditor {
}); });
} }
const newDraggingPointPosition = LinearElementEditor.createPointAt( const newPoint = LinearElementEditor.createPointAt(
element, element,
scenePointerX - editingLinearElement.pointerOffset.x, scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y, scenePointerY - editingLinearElement.pointerOffset.y,
appState.gridSize, appState.gridSize,
); );
LinearElementEditor.movePoint(element, activePointIndex, newPoint);
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
LinearElementEditor.movePoints(
element,
selectedPointsIndices.map((pointIndex) => {
const newPointPosition =
pointIndex ===
editingLinearElement.pointerDownState.lastClickedPoint
? LinearElementEditor.createPointAt(
element,
scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y,
appState.gridSize,
)
: ([
element.points[pointIndex][0] + deltaX,
element.points[pointIndex][1] + deltaY,
] as const);
return {
index: pointIndex,
point: newPointPosition,
isDragging:
pointIndex ===
editingLinearElement.pointerDownState.lastClickedPoint,
};
}),
);
// suggest bindings for first and last point if selected
if (isBindingElement(element)) { if (isBindingElement(element)) {
const coords: { x: number; y: number }[] = []; maybeSuggestBinding(element, activePointIndex === 0 ? "start" : "end");
const firstSelectedIndex = selectedPointsIndices[0];
if (firstSelectedIndex === 0) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[0],
),
),
);
}
const lastSelectedIndex =
selectedPointsIndices[selectedPointsIndices.length - 1];
if (lastSelectedIndex === element.points.length - 1) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[lastSelectedIndex],
),
),
);
}
if (coords.length) {
maybeSuggestBinding(element, coords);
}
} }
return true; return true;
} }
return false; return false;
} }
@@ -249,79 +118,45 @@ export class LinearElementEditor {
editingLinearElement: LinearElementEditor, editingLinearElement: LinearElementEditor,
appState: AppState, appState: AppState,
): LinearElementEditor { ): LinearElementEditor {
const { elementId, selectedPointsIndices, isDragging, pointerDownState } = const { elementId, activePointIndex, isDragging } = editingLinearElement;
editingLinearElement;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId);
if (!element) { if (!element) {
return editingLinearElement; return editingLinearElement;
} }
const bindings: Partial< let binding = {};
Pick< if (
InstanceType<typeof LinearElementEditor>, isDragging &&
"startBindingElement" | "endBindingElement" (activePointIndex === 0 || activePointIndex === element.points.length - 1)
> ) {
> = {}; if (isPathALoop(element.points, appState.zoom.value)) {
LinearElementEditor.movePoint(
if (isDragging && selectedPointsIndices) { element,
for (const selectedPoint of selectedPointsIndices) { activePointIndex,
if ( activePointIndex === 0
selectedPoint === 0 || ? element.points[element.points.length - 1]
selectedPoint === element.points.length - 1 : element.points[0],
) { );
if (isPathALoop(element.points, appState.zoom.value)) {
LinearElementEditor.movePoints(element, [
{
index: selectedPoint,
point:
selectedPoint === 0
? element.points[element.points.length - 1]
: element.points[0],
},
]);
}
const bindingElement = isBindingEnabled(appState)
? getHoveredElementForBinding(
tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
selectedPoint!,
),
),
Scene.getScene(element)!,
)
: null;
bindings[
selectedPoint === 0 ? "startBindingElement" : "endBindingElement"
] = bindingElement;
}
} }
const bindingElement = isBindingEnabled(appState)
? getHoveredElementForBinding(
tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
activePointIndex!,
),
),
Scene.getScene(element)!,
)
: null;
binding = {
[activePointIndex === 0 ? "startBindingElement" : "endBindingElement"]:
bindingElement,
};
} }
return { return {
...editingLinearElement, ...editingLinearElement,
...bindings, ...binding,
// if clicking without previously dragging a point(s), and not holding
// shift, deselect all points except the one clicked. If holding shift,
// toggle the point.
selectedPointsIndices:
isDragging || event.shiftKey
? !isDragging &&
event.shiftKey &&
pointerDownState.prevSelectedPointsIndices?.includes(
pointerDownState.lastClickedPoint,
)
? selectedPointsIndices &&
selectedPointsIndices.filter(
(pointIndex) =>
pointIndex !== pointerDownState.lastClickedPoint,
)
: selectedPointsIndices
: selectedPointsIndices?.includes(pointerDownState.lastClickedPoint)
? [pointerDownState.lastClickedPoint]
: selectedPointsIndices,
isDragging: false, isDragging: false,
pointerOffset: { x: 0, y: 0 }, pointerOffset: { x: 0, y: 0 },
}; };
@@ -371,12 +206,7 @@ export class LinearElementEditor {
setState({ setState({
editingLinearElement: { editingLinearElement: {
...appState.editingLinearElement, ...appState.editingLinearElement,
pointerDownState: { activePointIndex: element.points.length - 1,
prevSelectedPointsIndices:
appState.editingLinearElement.selectedPointsIndices,
lastClickedPoint: -1,
},
selectedPointsIndices: [element.points.length - 1],
lastUncommittedPoint: null, lastUncommittedPoint: null,
endBindingElement: getHoveredElementForBinding( endBindingElement: getHoveredElementForBinding(
scenePointer, scenePointer,
@@ -429,28 +259,10 @@ export class LinearElementEditor {
element.angle, element.angle,
); );
const nextSelectedPointsIndices =
clickedPointIndex > -1 || event.shiftKey
? event.shiftKey ||
appState.editingLinearElement.selectedPointsIndices?.includes(
clickedPointIndex,
)
? normalizeSelectedPoints([
...(appState.editingLinearElement.selectedPointsIndices || []),
clickedPointIndex,
])
: [clickedPointIndex]
: null;
setState({ setState({
editingLinearElement: { editingLinearElement: {
...appState.editingLinearElement, ...appState.editingLinearElement,
pointerDownState: { activePointIndex: clickedPointIndex > -1 ? clickedPointIndex : null,
prevSelectedPointsIndices:
appState.editingLinearElement.selectedPointsIndices,
lastClickedPoint: clickedPointIndex,
},
selectedPointsIndices: nextSelectedPointsIndices,
pointerOffset: targetPoint pointerOffset: targetPoint
? { ? {
x: scenePointer.x - targetPoint[0], x: scenePointer.x - targetPoint[0],
@@ -480,7 +292,7 @@ export class LinearElementEditor {
if (!event.altKey) { if (!event.altKey) {
if (lastPoint === lastUncommittedPoint) { if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.deletePoints(element, [points.length - 1]); LinearElementEditor.movePoint(element, points.length - 1, "delete");
} }
return { ...editingLinearElement, lastUncommittedPoint: null }; return { ...editingLinearElement, lastUncommittedPoint: null };
} }
@@ -493,14 +305,13 @@ export class LinearElementEditor {
); );
if (lastPoint === lastUncommittedPoint) { if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.movePoints(element, [ LinearElementEditor.movePoint(
{ element,
index: element.points.length - 1, element.points.length - 1,
point: newPoint, newPoint,
}, );
]);
} else { } else {
LinearElementEditor.addPoints(element, [{ point: newPoint }]); LinearElementEditor.movePoint(element, "new", newPoint);
} }
return { return {
@@ -509,21 +320,6 @@ export class LinearElementEditor {
}; };
} }
/** scene coords */
static getPointGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>,
point: Point,
) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
let { x, y } = element;
[x, y] = rotate(x + point[0], y + point[1], cx, cy, element.angle);
return [x, y] as const;
}
/** scene coords */
static getPointsGlobalCoordinates( static getPointsGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
) { ) {
@@ -643,122 +439,22 @@ export class LinearElementEditor {
mutateElement(element, LinearElementEditor.getNormalizedPoints(element)); mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
} }
static duplicateSelectedPoints(appState: AppState) { static movePointByOffset(
if (!appState.editingLinearElement) {
return false;
}
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (!element || selectedPointsIndices === null) {
return false;
}
const { points } = element;
const nextSelectedIndices: number[] = [];
let pointAddedToEnd = false;
let indexCursor = -1;
const nextPoints = points.reduce((acc: Point[], point, index) => {
++indexCursor;
acc.push(point);
const isSelected = selectedPointsIndices.includes(index);
if (isSelected) {
const nextPoint = points[index + 1];
if (!nextPoint) {
pointAddedToEnd = true;
}
acc.push(
nextPoint
? [(point[0] + nextPoint[0]) / 2, (point[1] + nextPoint[1]) / 2]
: [point[0], point[1]],
);
nextSelectedIndices.push(indexCursor + 1);
++indexCursor;
}
return acc;
}, []);
mutateElement(element, { points: nextPoints });
// temp hack to ensure the line doesn't move when adding point to the end,
// potentially expanding the bounding box
if (pointAddedToEnd) {
const lastPoint = element.points[element.points.length - 1];
LinearElementEditor.movePoints(element, [
{
index: element.points.length - 1,
point: [lastPoint[0] + 30, lastPoint[1] + 30],
},
]);
}
return {
appState: {
...appState,
editingLinearElement: {
...appState.editingLinearElement,
selectedPointsIndices: nextSelectedIndices,
},
},
};
}
static deletePoints(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
pointIndices: readonly number[], pointIndex: number,
offset: { x: number; y: number },
) { ) {
let offsetX = 0; const [x, y] = element.points[pointIndex];
let offsetY = 0; LinearElementEditor.movePoint(element, pointIndex, [
x + offset.x,
const isDeletingOriginPoint = pointIndices.includes(0); y + offset.y,
]);
// if deleting first point, make the next to be [0,0] and recalculate
// positions of the rest with respect to it
if (isDeletingOriginPoint) {
const firstNonDeletedPoint = element.points.find((point, idx) => {
return !pointIndices.includes(idx);
});
if (firstNonDeletedPoint) {
offsetX = firstNonDeletedPoint[0];
offsetY = firstNonDeletedPoint[1];
}
}
const nextPoints = element.points.reduce((acc: Point[], point, idx) => {
if (!pointIndices.includes(idx)) {
acc.push(
!acc.length ? [0, 0] : [point[0] - offsetX, point[1] - offsetY],
);
}
return acc;
}, []);
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
} }
static addPoints( static movePoint(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
targetPoints: { point: Point }[], pointIndex: number | "new",
) { targetPosition: Point | "delete",
const offsetX = 0;
const offsetY = 0;
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
}
static movePoints(
element: NonDeleted<ExcalidrawLinearElement>,
targetPoints: { index: number; point: Point; isDragging?: boolean }[],
otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding }, otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
) { ) {
const { points } = element; const { points } = element;
@@ -771,50 +467,49 @@ export class LinearElementEditor {
let offsetX = 0; let offsetX = 0;
let offsetY = 0; let offsetY = 0;
const selectedOriginPoint = targetPoints.find(({ index }) => index === 0); let nextPoints: (readonly [number, number])[];
if (targetPosition === "delete") {
// remove point
if (pointIndex === "new") {
throw new Error("invalid args in movePoint");
}
nextPoints = points.slice();
nextPoints.splice(pointIndex, 1);
if (pointIndex === 0) {
// if deleting first point, make the next to be [0,0] and recalculate
// positions of the rest with respect to it
offsetX = nextPoints[0][0];
offsetY = nextPoints[0][1];
nextPoints = nextPoints.map((point, idx) => {
if (idx === 0) {
return [0, 0];
}
return [point[0] - offsetX, point[1] - offsetY];
});
}
} else if (pointIndex === "new") {
nextPoints = [...points, targetPosition];
} else {
const deltaX = targetPosition[0] - points[pointIndex][0];
const deltaY = targetPosition[1] - points[pointIndex][1];
nextPoints = points.map((point, idx) => {
if (idx === pointIndex) {
if (idx === 0) {
offsetX = deltaX;
offsetY = deltaY;
return point;
}
offsetX = 0;
offsetY = 0;
if (selectedOriginPoint) { return [point[0] + deltaX, point[1] + deltaY] as const;
offsetX = }
selectedOriginPoint.point[0] - points[selectedOriginPoint.index][0]; return offsetX || offsetY
offsetY = ? ([point[0] - offsetX, point[1] - offsetY] as const)
selectedOriginPoint.point[1] - points[selectedOriginPoint.index][1]; : point;
});
} }
const nextPoints = points.map((point, idx) => {
const selectedPointData = targetPoints.find((p) => p.index === idx);
if (selectedPointData) {
if (selectedOriginPoint) {
return point;
}
const deltaX =
selectedPointData.point[0] - points[selectedPointData.index][0];
const deltaY =
selectedPointData.point[1] - points[selectedPointData.index][1];
return [point[0] + deltaX, point[1] + deltaY] as const;
}
return offsetX || offsetY
? ([point[0] - offsetX, point[1] - offsetY] as const)
: point;
});
LinearElementEditor._updatePoints(
element,
nextPoints,
offsetX,
offsetY,
otherUpdates,
);
}
private static _updatePoints(
element: NonDeleted<ExcalidrawLinearElement>,
nextPoints: readonly Point[],
offsetX: number,
offsetY: number,
otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
) {
const nextCoords = getElementPointsCoords( const nextCoords = getElementPointsCoords(
element, element,
nextPoints, nextPoints,
@@ -822,7 +517,7 @@ export class LinearElementEditor {
); );
const prevCoords = getElementPointsCoords( const prevCoords = getElementPointsCoords(
element, element,
element.points, points,
element.strokeSharpness || "round", element.strokeSharpness || "round",
); );
const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2; const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2;
@@ -841,13 +536,3 @@ export class LinearElementEditor {
}); });
} }
} }
const normalizeSelectedPoints = (
points: (number | null)[],
): number[] | null => {
let nextPoints = [
...new Set(points.filter((p) => p !== null && p !== -1)),
] as number[];
nextPoints = nextPoints.sort((a, b) => a - b);
return nextPoints.length ? nextPoints : null;
};
+1 -5
View File
@@ -4,7 +4,6 @@ import Scene from "../scene/Scene";
import { getSizeFromPoints } from "../points"; import { getSizeFromPoints } from "../points";
import { randomInteger } from "../random"; import { randomInteger } from "../random";
import { Point } from "../types"; import { Point } from "../types";
import { getUpdatedTimestamp } from "../utils";
type ElementUpdate<TElement extends ExcalidrawElement> = Omit< type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
Partial<TElement>, Partial<TElement>,
@@ -93,7 +92,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
element.version++; element.version++;
element.versionNonce = randomInteger(); element.versionNonce = randomInteger();
element.updated = getUpdatedTimestamp();
if (informMutation) { if (informMutation) {
Scene.getScene(element)?.informMutation(); Scene.getScene(element)?.informMutation();
@@ -128,14 +126,13 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
return { return {
...element, ...element,
...updates, ...updates,
updated: getUpdatedTimestamp(),
version: element.version + 1, version: element.version + 1,
versionNonce: randomInteger(), versionNonce: randomInteger(),
}; };
}; };
/** /**
* Mutates element, bumping `version`, `versionNonce`, and `updated`. * Mutates element and updates `version` & `versionNonce`.
* *
* NOTE: does not trigger re-render. * NOTE: does not trigger re-render.
*/ */
@@ -145,6 +142,5 @@ export const bumpVersion = (
) => { ) => {
element.version = (version ?? element.version) + 1; element.version = (version ?? element.version) + 1;
element.versionNonce = randomInteger(); element.versionNonce = randomInteger();
element.updated = getUpdatedTimestamp();
return element; return element;
}; };
+35 -81
View File
@@ -11,28 +11,23 @@ import {
Arrowhead, Arrowhead,
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
FontFamilyValues, FontFamilyValues,
ExcalidrawRectangleElement,
} from "../element/types"; } from "../element/types";
import { getFontString, getUpdatedTimestamp } from "../utils"; import { measureText, getFontString } from "../utils";
import { randomInteger, randomId } from "../random"; import { randomInteger, randomId } from "../random";
import { mutateElement, newElementWith } from "./mutateElement"; import { newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups"; import { getNewGroupIdsForDuplication } from "../groups";
import { AppState } from "../types"; import { AppState } from "../types";
import { getElementAbsoluteCoords } from "."; import { getElementAbsoluteCoords } from ".";
import { adjustXYWithRotation } from "../math"; import { adjustXYWithRotation } from "../math";
import { getResizedElementAbsoluteCoords } from "./bounds"; import { getResizedElementAbsoluteCoords } from "./bounds";
import { measureText } from "./textElement";
import { isBoundToContainer } from "./typeChecks";
import Scene from "../scene/Scene";
import { PADDING } from "../constants";
type ElementConstructorOpts = MarkOptional< type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">, Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted">,
| "width" | "width"
| "height" | "height"
| "angle" | "angle"
| "groupIds" | "groupIds"
| "boundElements" | "boundElementIds"
| "seed" | "seed"
| "version" | "version"
| "versionNonce" | "versionNonce"
@@ -55,36 +50,32 @@ const _newElementBase = <T extends ExcalidrawElement>(
angle = 0, angle = 0,
groupIds = [], groupIds = [],
strokeSharpness, strokeSharpness,
boundElements = null, boundElementIds = null,
...rest ...rest
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">, }: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
) => { ) => ({
const element = { id: rest.id || randomId(),
id: rest.id || randomId(), type,
type, x,
x, y,
y, width,
width, height,
height, angle,
angle, strokeColor,
strokeColor, backgroundColor,
backgroundColor, fillStyle,
fillStyle, strokeWidth,
strokeWidth, strokeStyle,
strokeStyle, roughness,
roughness, opacity,
opacity, groupIds,
groupIds, strokeSharpness,
strokeSharpness, seed: rest.seed ?? randomInteger(),
seed: rest.seed ?? randomInteger(), version: rest.version || 1,
version: rest.version || 1, versionNonce: rest.versionNonce ?? 0,
versionNonce: rest.versionNonce ?? 0, isDeleted: false as false,
isDeleted: false as false, boundElementIds,
boundElements, });
updated: getUpdatedTimestamp(),
};
return element;
};
export const newElement = ( export const newElement = (
opts: { opts: {
@@ -122,7 +113,6 @@ export const newTextElement = (
fontFamily: FontFamilyValues; fontFamily: FontFamilyValues;
textAlign: TextAlign; textAlign: TextAlign;
verticalAlign: VerticalAlign; verticalAlign: VerticalAlign;
containerId?: ExcalidrawRectangleElement["id"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawTextElement> => { ): NonDeleted<ExcalidrawTextElement> => {
const metrics = measureText(opts.text, getFontString(opts)); const metrics = measureText(opts.text, getFontString(opts));
@@ -140,8 +130,6 @@ export const newTextElement = (
width: metrics.width, width: metrics.width,
height: metrics.height, height: metrics.height,
baseline: metrics.baseline, baseline: metrics.baseline,
containerId: opts.containerId || null,
originalText: opts.text,
}, },
{}, {},
); );
@@ -158,25 +146,18 @@ const getAdjustedDimensions = (
height: number; height: number;
baseline: number; baseline: number;
} => { } => {
const maxWidth = element.containerId ? element.width : null;
const { const {
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
baseline: nextBaseline, baseline: nextBaseline,
} = measureText(nextText, getFontString(element), maxWidth); } = measureText(nextText, getFontString(element));
const { textAlign, verticalAlign } = element; const { textAlign, verticalAlign } = element;
let x: number; let x: number;
let y: number; let y: number;
if (
textAlign === "center" && if (textAlign === "center" && verticalAlign === "middle") {
verticalAlign === "middle" && const prevMetrics = measureText(element.text, getFontString(element));
!element.containerId
) {
const prevMetrics = measureText(
element.text,
getFontString(element),
maxWidth,
);
const offsets = getTextElementPositionOffsets(element, { const offsets = getTextElementPositionOffsets(element, {
width: nextWidth - prevMetrics.width, width: nextWidth - prevMetrics.width,
height: nextHeight - prevMetrics.height, height: nextHeight - prevMetrics.height,
@@ -213,22 +194,6 @@ const getAdjustedDimensions = (
); );
} }
// make sure container dimensions are set properly when
// text editor overflows beyond viewport dimensions
if (isBoundToContainer(element)) {
const container = Scene.getScene(element)!.getElement(element.containerId)!;
let height = container.height;
let width = container.width;
if (nextHeight > height - PADDING * 2) {
height = nextHeight + PADDING * 2;
}
if (nextWidth > width - PADDING * 2) {
width = nextWidth + PADDING * 2;
}
if (height !== container.height || width !== container.width) {
mutateElement(container, { height, width });
}
}
return { return {
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
@@ -240,22 +205,12 @@ const getAdjustedDimensions = (
export const updateTextElement = ( export const updateTextElement = (
element: ExcalidrawTextElement, element: ExcalidrawTextElement,
{ { text, isDeleted }: { text: string; isDeleted?: boolean },
text,
isDeleted,
originalText,
}: { text: string; isDeleted?: boolean; originalText: string },
updateDimensions: boolean,
): ExcalidrawTextElement => { ): ExcalidrawTextElement => {
const dimensions = updateDimensions
? getAdjustedDimensions(element, text)
: undefined;
return newElementWith(element, { return newElementWith(element, {
text, text,
originalText,
isDeleted: isDeleted ?? element.isDeleted, isDeleted: isDeleted ?? element.isDeleted,
...dimensions, ...getAdjustedDimensions(element, text),
}); });
}; };
@@ -382,7 +337,6 @@ export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
} else { } else {
copy.id = randomId(); copy.id = randomId();
} }
copy.updated = getUpdatedTimestamp();
copy.seed = randomInteger(); copy.seed = randomInteger();
copy.groupIds = getNewGroupIdsForDuplication( copy.groupIds = getNewGroupIdsForDuplication(
copy.groupIds, copy.groupIds,
+3 -43
View File
@@ -25,7 +25,7 @@ import {
} from "./typeChecks"; } from "./typeChecks";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers"; import { getPerfectElementSize } from "./sizeHelpers";
import { getFontString } from "../utils"; import { measureText, getFontString } from "../utils";
import { updateBoundElements } from "./binding"; import { updateBoundElements } from "./binding";
import { import {
TransformHandleType, TransformHandleType,
@@ -33,13 +33,6 @@ import {
TransformHandleDirection, TransformHandleDirection,
} from "./transformHandles"; } from "./transformHandles";
import { Point, PointerDownState } from "../types"; import { Point, PointerDownState } from "../types";
import Scene from "../scene/Scene";
import {
getApproxMinLineWidth,
getBoundTextElementId,
handleBindTextResize,
measureText,
} from "./textElement";
export const normalizeAngle = (angle: number): number => { export const normalizeAngle = (angle: number): number => {
if (angle >= 2 * Math.PI) { if (angle >= 2 * Math.PI) {
@@ -139,7 +132,6 @@ export const transformElements = (
pointerX, pointerX,
pointerY, pointerY,
); );
handleBindTextResize(selectedElements, transformHandleType);
return true; return true;
} }
} }
@@ -162,11 +154,6 @@ const rotateSingleElement = (
} }
angle = normalizeAngle(angle); angle = normalizeAngle(angle);
mutateElement(element, { angle }); mutateElement(element, { angle });
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement = Scene.getScene(element)!.getElement(boundTextElementId);
mutateElement(textElement!, { angle });
}
}; };
// used in DEV only // used in DEV only
@@ -285,7 +272,6 @@ const measureFontSizeFromWH = (
const metrics = measureText( const metrics = measureText(
element.text, element.text,
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }), getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
element.containerId ? element.width : null,
); );
return { return {
size: nextFontSize, size: nextFontSize,
@@ -427,9 +413,6 @@ export const resizeSingleElement = (
element.width, element.width,
element.height, element.height,
); );
const boundTextElementId = getBoundTextElementId(element);
const boundsCurrentWidth = esx2 - esx1; const boundsCurrentWidth = esx2 - esx1;
const boundsCurrentHeight = esy2 - esy1; const boundsCurrentHeight = esy2 - esy1;
@@ -490,11 +473,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)) {
@@ -587,16 +565,9 @@ export const resizeSingleElement = (
], ],
}); });
} }
let minWidth = 0;
if (boundTextElementId) {
const boundTextElement = Scene.getScene(element)!.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
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)
@@ -605,7 +576,6 @@ 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);
} }
}; };
@@ -677,7 +647,7 @@ const resizeMultipleElements = (
const width = element.width * scale; const width = element.width * scale;
const height = element.height * scale; const height = element.height * scale;
let font: { fontSize?: number; baseline?: number } = {}; let font: { fontSize?: number; baseline?: number } = {};
if (isTextElement(element)) { if (element.type === "text") {
const nextFont = measureFontSizeFromWH(element, width, height); const nextFont = measureFontSizeFromWH(element, width, height);
if (nextFont === null) { if (nextFont === null) {
return null; return null;
@@ -758,16 +728,6 @@ const rotateMultipleElements = (
y: element.y + (rotatedCY - cy), y: element.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + origAngle), angle: normalizeAngle(centerAngle + origAngle),
}); });
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement =
Scene.getScene(element)!.getElement(boundTextElementId)!;
mutateElement(textElement, {
x: textElement.x + (rotatedCX - cx),
y: textElement.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + origAngle),
});
}
}); });
}; };
+3 -380
View File
@@ -1,389 +1,12 @@
import { getFontString, arrayToMap } from "../utils"; import { measureText, getFontString } from "../utils";
import { import { ExcalidrawTextElement } from "./types";
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawTextElement,
FontString,
NonDeletedExcalidrawElement,
} from "./types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { PADDING } from "../constants";
import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene";
export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => { export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => {
let maxWidth; const metrics = measureText(element.text, getFontString(element));
if (element.containerId) {
maxWidth = element.width;
}
const metrics = measureText(
element.originalText,
getFontString(element),
maxWidth,
);
mutateElement(element, { mutateElement(element, {
width: metrics.width, width: metrics.width,
height: metrics.height, height: metrics.height,
baseline: metrics.baseline, baseline: metrics.baseline,
}); });
}; };
export const bindTextToShapeAfterDuplication = (
sceneElements: ExcalidrawElement[],
oldElements: ExcalidrawElement[],
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
): void => {
const sceneElementMap = arrayToMap(sceneElements) as Map<
ExcalidrawElement["id"],
ExcalidrawElement
>;
oldElements.forEach((element) => {
const newElementId = oldIdToDuplicatedId.get(element.id) as string;
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId)!;
mutateElement(
sceneElementMap.get(newElementId) as ExcalidrawBindableElement,
{
boundElements: element.boundElements?.concat({
type: "text",
id: newTextElementId,
}),
},
);
mutateElement(
sceneElementMap.get(newTextElementId) as ExcalidrawTextElement,
{
containerId: newElementId,
},
);
}
});
};
export const handleBindTextResize = (
elements: readonly NonDeletedExcalidrawElement[],
transformHandleType: MaybeTransformHandleType,
) => {
elements.forEach((element) => {
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement = Scene.getScene(element)!.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
if (textElement && textElement.text) {
if (!element) {
return;
}
let text = textElement.text;
let nextHeight = textElement.height;
let containerHeight = element.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
let minCharWidthTillNow = 0;
if (text) {
minCharWidthTillNow = getMinCharWidth(getFontString(textElement));
// check if the diff has exceeded min char width needed
const diff = Math.abs(
element.width - textElement.width + PADDING * 2,
);
if (diff >= minCharWidthTillNow) {
text = wrapText(
textElement.originalText,
getFontString(textElement),
element.width,
);
}
}
const dimensions = measureText(
text,
getFontString(textElement),
element.width,
);
nextHeight = dimensions.height;
nextBaseLine = dimensions.baseline;
}
// increase height in case text element height exceeds
if (nextHeight > element.height - PADDING * 2) {
containerHeight = nextHeight + 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;
mutateElement(textElement, {
text,
// preserve padding and set width correctly
width: element.width - PADDING * 2,
height: nextHeight,
x: element.x + PADDING,
y: updatedY,
baseline: nextBaseLine,
});
}
}
});
};
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
export const measureText = (
text: string,
font: FontString,
maxWidth?: number | null,
) => {
text = text
.split("\n")
// replace empty lines with single space because leading/trailing empty
// lines would be stripped from computation
.map((x) => x || " ")
.join("\n");
const container = document.createElement("div");
container.style.position = "absolute";
container.style.whiteSpace = "pre";
container.style.font = font;
container.style.minHeight = "1em";
if (maxWidth) {
const lineHeight = getApproxLineHeight(font);
container.style.width = `${String(maxWidth)}px`;
container.style.maxWidth = `${String(maxWidth)}px`;
container.style.overflow = "hidden";
container.style.wordBreak = "break-word";
container.style.lineHeight = `${String(lineHeight)}px`;
container.style.whiteSpace = "pre-wrap";
}
document.body.appendChild(container);
container.innerText = text;
const span = document.createElement("span");
span.style.display = "inline-block";
span.style.overflow = "hidden";
span.style.width = "1px";
span.style.height = "1px";
container.appendChild(span);
// Baseline is important for positioning text on canvas
const baseline = span.offsetTop + span.offsetHeight;
const width = container.offsetWidth;
const height = container.offsetHeight;
document.body.removeChild(container);
return { width, height, baseline };
};
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
export const getApproxLineHeight = (font: FontString) => {
return measureText(DUMMY_TEXT, font, null).height;
};
let canvas: HTMLCanvasElement | undefined;
const getTextWidth = (text: string, font: FontString) => {
if (!canvas) {
canvas = document.createElement("canvas");
}
const canvas2dContext = canvas.getContext("2d")!;
canvas2dContext.font = font;
const metrics = canvas2dContext.measureText(text);
return metrics.width;
};
export const wrapText = (
text: string,
font: FontString,
containerWidth: number,
) => {
const maxWidth = containerWidth - PADDING * 2;
const lines: Array<string> = [];
const originalLines = text.split("\n");
const spaceWidth = getTextWidth(" ", font);
originalLines.forEach((originalLine) => {
const words = originalLine.split(" ");
// This means its newline so push it
if (words.length === 1 && words[0] === "") {
lines.push(words[0]);
} else {
let currentLine = "";
let currentLineWidthTillNow = 0;
let index = 0;
while (index < words.length) {
const currentWordWidth = getTextWidth(words[index], font);
// Start breaking longer words exceeding max width
if (currentWordWidth > maxWidth) {
// push current line since the current word exceeds the max width
// so will be appended in next line
if (currentLine) {
lines.push(currentLine);
}
currentLine = "";
currentLineWidthTillNow = 0;
while (words[index].length > 0) {
const currentChar = words[index][0];
const width = charWidth.calculate(currentChar, font);
currentLineWidthTillNow += width;
words[index] = words[index].slice(1);
if (currentLineWidthTillNow >= maxWidth) {
// only remove last trailing space which we have added when joining words
if (currentLine.slice(-1) === " ") {
currentLine = currentLine.slice(0, -1);
}
lines.push(currentLine);
currentLine = currentChar;
currentLineWidthTillNow = width;
if (currentLineWidthTillNow === maxWidth) {
currentLine = "";
currentLineWidthTillNow = 0;
}
} else {
currentLine += currentChar;
}
}
// push current line if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth > maxWidth) {
lines.push(currentLine);
currentLine = "";
currentLineWidthTillNow = 0;
} else {
// space needs to be appended before next word
// as currentLine contains chars which couldn't be appended
// to previous line
currentLine += " ";
currentLineWidthTillNow += spaceWidth;
}
index++;
} else {
// Start appending words in a line till max width reached
while (currentLineWidthTillNow < maxWidth && index < words.length) {
const word = words[index];
currentLineWidthTillNow = getTextWidth(currentLine + word, font);
if (currentLineWidthTillNow >= maxWidth) {
lines.push(currentLine);
currentLineWidthTillNow = 0;
currentLine = "";
break;
}
index++;
currentLine += `${word} `;
}
if (currentLineWidthTillNow === maxWidth) {
currentLine = "";
currentLineWidthTillNow = 0;
}
}
}
if (currentLine) {
// only remove last trailing space which we have added when joining words
if (currentLine.slice(-1) === " ") {
currentLine = currentLine.slice(0, -1);
}
lines.push(currentLine);
}
}
});
return lines.join("\n");
};
export const charWidth = (() => {
const cachedCharWidth: { [key: FontString]: Array<number> } = {};
const calculate = (char: string, font: FontString) => {
const ascii = char.charCodeAt(0);
if (!cachedCharWidth[font]) {
cachedCharWidth[font] = [];
}
if (!cachedCharWidth[font][ascii]) {
const width = getTextWidth(char, font);
cachedCharWidth[font][ascii] = width;
}
return cachedCharWidth[font][ascii];
};
const updateCache = (char: string, font: FontString) => {
const ascii = char.charCodeAt(0);
if (!cachedCharWidth[font][ascii]) {
cachedCharWidth[font][ascii] = calculate(char, font);
}
};
const clearCacheforFont = (font: FontString) => {
cachedCharWidth[font] = [];
};
const getCache = (font: FontString) => {
return cachedCharWidth[font];
};
return {
calculate,
updateCache,
clearCacheforFont,
getCache,
};
})();
export const getApproxMinLineWidth = (font: FontString) => {
return measureText(DUMMY_TEXT.split("").join("\n"), font).width + PADDING * 2;
};
export const getApproxMinLineHeight = (font: FontString) => {
return getApproxLineHeight(font) + PADDING * 2;
};
export const getMinCharWidth = (font: FontString) => {
const cache = charWidth.getCache(font);
if (!cache) {
return 0;
}
const cacheWithOutEmpty = cache.filter((val) => val !== undefined);
return Math.min(...cacheWithOutEmpty);
};
export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
// Generally lower case is used so converting to lower case
const dummyText = DUMMY_TEXT.toLocaleLowerCase();
const batchLength = 6;
let index = 0;
let widthTillNow = 0;
let str = "";
while (widthTillNow <= width) {
const batch = dummyText.substr(index, index + batchLength);
str += batch;
widthTillNow += getTextWidth(str, font);
if (index === dummyText.length - 1) {
index = 0;
}
index = index + batchLength;
}
while (widthTillNow > width) {
str = str.substr(0, str.length - 1);
widthTillNow = getTextWidth(str, font);
}
return str.length;
};
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
return container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id;
};
+43 -247
View File
@@ -1,25 +1,10 @@
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { import { isWritableElement, getFontString } from "../utils";
isWritableElement,
getFontString,
viewportCoordsToSceneCoords,
getFontFamilyString,
} from "../utils";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { isBoundToContainer, isTextElement } from "./typeChecks"; import { isTextElement } from "./typeChecks";
import { CLASSES, PADDING } from "../constants"; import { CLASSES } from "../constants";
import { import { ExcalidrawElement } from "./types";
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawTextElement,
} from "./types";
import { AppState } from "../types"; import { AppState } from "../types";
import { mutateElement } from "./mutateElement";
import {
getApproxLineHeight,
getBoundTextElementId,
wrapText,
} from "./textElement";
const normalizeText = (text: string) => { const normalizeText = (text: string) => {
return ( return (
@@ -63,154 +48,55 @@ export const textWysiwyg = ({
id: ExcalidrawElement["id"]; id: ExcalidrawElement["id"];
appState: AppState; appState: AppState;
onChange?: (text: string) => void; onChange?: (text: string) => void;
onSubmit: (data: { onSubmit: (data: { text: string; viaKeyboard: boolean }) => void;
text: string;
viaKeyboard: boolean;
originalText: string;
}) => void;
getViewportCoords: (x: number, y: number) => [number, number]; getViewportCoords: (x: number, y: number) => [number, number];
element: ExcalidrawElement; element: ExcalidrawElement;
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
excalidrawContainer: HTMLDivElement | null; excalidrawContainer: HTMLDivElement | null;
}) => { }) => {
const textPropertiesUpdated = (
updatedElement: ExcalidrawTextElement,
editable: HTMLTextAreaElement,
) => {
const currentFont = editable.style.fontFamily.replaceAll('"', "");
if (
getFontFamilyString({ fontFamily: updatedElement.fontFamily }) !==
currentFont
) {
return true;
}
if (`${updatedElement.fontSize}px` !== editable.style.fontSize) {
return true;
}
return false;
};
let originalContainerHeight: number;
let approxLineHeight = isTextElement(element)
? getApproxLineHeight(getFontString(element))
: 0;
const updateWysiwygStyle = () => { const updateWysiwygStyle = () => {
const updatedElement = Scene.getScene(element)?.getElement(id); const updatedElement = Scene.getScene(element)?.getElement(id);
if (updatedElement && isTextElement(updatedElement)) { if (updatedElement && isTextElement(updatedElement)) {
let coordX = updatedElement.x; const [viewportX, viewportY] = getViewportCoords(
let coordY = updatedElement.y; updatedElement.x,
let container = updatedElement?.containerId updatedElement.y,
? Scene.getScene(updatedElement)!.getElement(updatedElement.containerId) );
: null;
let maxWidth = updatedElement.width;
let maxHeight = updatedElement.height;
let width = updatedElement.width;
let height = updatedElement.height;
if (container && updatedElement.containerId) {
const propertiesUpdated = textPropertiesUpdated(
updatedElement,
editable,
);
if (propertiesUpdated) {
const currentContainer = Scene.getScene(updatedElement)?.getElement(
updatedElement.containerId,
) as ExcalidrawBindableElement;
approxLineHeight = isTextElement(updatedElement)
? getApproxLineHeight(getFontString(updatedElement))
: 0;
if (updatedElement.height > currentContainer.height - PADDING * 2) {
const nextHeight = updatedElement.height + PADDING * 2;
originalContainerHeight = nextHeight;
mutateElement(container, { height: nextHeight });
container = { ...container, height: nextHeight };
}
editable.style.height = `${updatedElement.height}px`;
}
if (!originalContainerHeight) {
originalContainerHeight = container.height;
}
maxWidth = container.width - PADDING * 2;
maxHeight = container.height - PADDING * 2;
width = maxWidth;
height = Math.min(height, maxHeight);
// The coordinates of text box set a distance of
// 30px to preserve padding
coordX = container.x + PADDING;
// autogrow container height if text exceeds
if (editable.clientHeight > maxHeight) {
const diff = Math.min(
editable.clientHeight - maxHeight,
approxLineHeight,
);
mutateElement(container, { height: container.height + diff });
return;
} else if (
// autoshrink container height until original container height
// is reached when text is removed
container.height > originalContainerHeight &&
editable.clientHeight < maxHeight
) {
const diff = Math.min(
maxHeight - editable.clientHeight,
approxLineHeight,
);
mutateElement(container, { height: container.height - diff });
}
// Start pushing text upward until a diff of 30px (padding)
// is reached
else {
const lines = editable.clientHeight / approxLineHeight;
// For some reason the scrollHeight gets set to twice the lineHeight
// when you start typing for first time and thus line count is 2
// hence this check
if (lines > 2 || propertiesUpdated) {
// vertically center align the text
coordY =
container.y + container.height / 2 - editable.clientHeight / 2;
}
}
}
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
const { textAlign, angle } = updatedElement; const { textAlign, angle } = updatedElement;
editable.value = updatedElement.originalText || updatedElement.text; editable.value = updatedElement.text;
const lines = updatedElement.originalText.split("\n");
const lineHeight = updatedElement.containerId const lines = updatedElement.text.replace(/\r\n?/g, "\n").split("\n");
? approxLineHeight const lineHeight = updatedElement.height / lines.length;
: updatedElement.height / lines.length; const maxWidth =
if (!container) { (appState.offsetLeft + appState.width - viewportX - 8) /
maxWidth = appState.zoom.value -
(appState.offsetLeft + appState.width - viewportX - 8) / // margin-right of parent if any
appState.zoom.value - Number(
// margin-right of parent if any getComputedStyle(
Number( excalidrawContainer?.parentNode as Element,
getComputedStyle( ).marginRight.slice(0, -2),
excalidrawContainer?.parentNode as Element, );
).marginRight.slice(0, -2),
);
}
// Make sure text editor height doesn't go beyond viewport
const editorMaxHeight =
(appState.offsetTop + appState.height - viewportY) /
appState.zoom.value;
Object.assign(editable.style, { Object.assign(editable.style, {
font: getFontString(updatedElement), font: getFontString(updatedElement),
// must be defined *after* font ¯\_(ツ)_/¯ // must be defined *after* font ¯\_(ツ)_/¯
lineHeight: `${lineHeight}px`, lineHeight: `${lineHeight}px`,
width: `${width}px`, width: `${updatedElement.width}px`,
height: `${Math.max(editable.clientHeight, updatedElement.height)}px`, height: `${updatedElement.height}px`,
left: `${viewportX}px`, left: `${viewportX}px`,
top: `${viewportY}px`, top: `${viewportY}px`,
transform: getTransform(width, height, angle, appState, maxWidth), transform: getTransform(
updatedElement.width,
updatedElement.height,
angle,
appState,
maxWidth,
),
textAlign, textAlign,
color: updatedElement.strokeColor, color: updatedElement.strokeColor,
opacity: updatedElement.opacity / 100, opacity: updatedElement.opacity / 100,
filter: "var(--theme-filter)", filter: "var(--theme-filter)",
maxWidth: `${maxWidth}px`, maxWidth: `${maxWidth}px`,
maxHeight: `${editorMaxHeight}px`,
}); });
} }
}; };
@@ -224,13 +110,6 @@ export const textWysiwyg = ({
editable.wrap = "off"; editable.wrap = "off";
editable.classList.add("excalidraw-wysiwyg"); editable.classList.add("excalidraw-wysiwyg");
let whiteSpace = "pre";
let wordBreak = "normal";
if (isBoundToContainer(element)) {
whiteSpace = "pre-wrap";
wordBreak = "break-word";
}
Object.assign(editable.style, { Object.assign(editable.style, {
position: "absolute", position: "absolute",
display: "inline-block", display: "inline-block",
@@ -243,21 +122,16 @@ export const textWysiwyg = ({
resize: "none", resize: "none",
background: "transparent", background: "transparent",
overflow: "hidden", overflow: "hidden",
// prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
whiteSpace: "pre",
// must be specified because in dark mode canvas creates a stacking context // must be specified because in dark mode canvas creates a stacking context
zIndex: "var(--zIndex-wysiwyg)", zIndex: "var(--zIndex-wysiwyg)",
wordBreak,
// prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
whiteSpace,
overflowWrap: "break-word",
}); });
updateWysiwygStyle(); updateWysiwygStyle();
if (onChange) { if (onChange) {
editable.oninput = () => { editable.oninput = () => {
if (isBoundToContainer(element)) {
editable.style.height = "auto";
editable.style.height = `${editable.scrollHeight}px`;
}
onChange(normalizeText(editable.value)); onChange(normalizeText(editable.value));
}; };
} }
@@ -300,7 +174,7 @@ export const textWysiwyg = ({
const linesStartIndices = getSelectedLinesStartIndices(); const linesStartIndices = getSelectedLinesStartIndices();
let value = editable.value; let value = editable.value;
linesStartIndices.forEach((startIndex: number) => { linesStartIndices.forEach((startIndex) => {
const startValue = value.slice(0, startIndex); const startValue = value.slice(0, startIndex);
const endValue = value.slice(startIndex); const endValue = value.slice(startIndex);
@@ -400,63 +274,9 @@ export const textWysiwyg = ({
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the // it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
// wysiwyg on update // wysiwyg on update
cleanup(); cleanup();
const updateElement = Scene.getScene(element)?.getElement(element.id);
if (!updateElement) {
return;
}
let wrappedText = "";
if (isTextElement(updateElement) && updateElement?.containerId) {
const container = Scene.getScene(updateElement)!.getElement(
updateElement.containerId,
) as ExcalidrawBindableElement;
if (container) {
wrappedText = wrapText(
editable.value,
getFontString(updateElement),
container.width,
);
const { x, y } = viewportCoordsToSceneCoords(
{
clientX: Number(editable.style.left.slice(0, -2)),
clientY: Number(editable.style.top.slice(0, -2)),
},
appState,
);
if (isTextElement(updateElement) && updateElement.containerId) {
if (editable.value) {
mutateElement(updateElement, {
y: y + appState.offsetTop,
height: Number(editable.style.height.slice(0, -2)),
width: Number(editable.style.width.slice(0, -2)),
x: x + appState.offsetLeft,
});
const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId || boundTextElementId !== element.id) {
mutateElement(container, {
boundElements: (container.boundElements || []).concat({
type: "text",
id: element.id,
}),
});
}
} else {
mutateElement(container, {
boundElements: container.boundElements?.filter(
(ele) => ele.type !== "text",
),
});
}
}
}
} else {
wrappedText = editable.value;
}
onSubmit({ onSubmit({
text: normalizeText(wrappedText), text: normalizeText(editable.value),
viaKeyboard: submittedViaKeyboard, viaKeyboard: submittedViaKeyboard,
originalText: editable.value,
}); });
}; };
@@ -485,45 +305,26 @@ export const textWysiwyg = ({
editable.remove(); editable.remove();
}; };
const bindBlurEvent = (event?: MouseEvent) => { const bindBlurEvent = () => {
window.removeEventListener("pointerup", bindBlurEvent); window.removeEventListener("pointerup", bindBlurEvent);
// Deferred so that the pointerdown that initiates the wysiwyg doesn't // Deferred so that the pointerdown that initiates the wysiwyg doesn't
// trigger the blur on ensuing pointerup. // trigger the blur on ensuing pointerup.
// Also to handle cases such as picking a color which would trigger a blur // Also to handle cases such as picking a color which would trigger a blur
// in that same tick. // in that same tick.
const target = event?.target;
const isTargetColorPicker =
target instanceof HTMLInputElement &&
target.closest(".color-picker-input") &&
isWritableElement(target);
setTimeout(() => { setTimeout(() => {
editable.onblur = handleSubmit; editable.onblur = handleSubmit;
if (target && isTargetColorPicker) {
target.onblur = () => {
editable.focus();
};
}
// case: clicking on the same property → no change → no update → no focus // case: clicking on the same property → no change → no update → no focus
if (!isTargetColorPicker) { editable.focus();
editable.focus();
}
}); });
}; };
// prevent blur when changing properties from the menu // prevent blur when changing properties from the menu
const onPointerDown = (event: MouseEvent) => { const onPointerDown = (event: MouseEvent) => {
const isTargetColorPicker =
event.target instanceof HTMLInputElement &&
event.target.closest(".color-picker-input") &&
isWritableElement(event.target);
if ( if (
((event.target instanceof HTMLElement || (event.target instanceof HTMLElement ||
event.target instanceof SVGElement) && event.target instanceof SVGElement) &&
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) && event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
!isWritableElement(event.target)) || !isWritableElement(event.target)
isTargetColorPicker
) { ) {
editable.onblur = null; editable.onblur = null;
window.addEventListener("pointerup", bindBlurEvent); window.addEventListener("pointerup", bindBlurEvent);
@@ -536,12 +337,7 @@ export const textWysiwyg = ({
// handle updates of textElement properties of editing element // handle updates of textElement properties of editing element
const unbindUpdate = Scene.getScene(element)!.addCallback(() => { const unbindUpdate = Scene.getScene(element)!.addCallback(() => {
updateWysiwygStyle(); updateWysiwygStyle();
const isColorPickerActive = !!document.activeElement?.closest( editable.focus();
".color-picker-input",
);
if (!isColorPickerActive) {
editable.focus();
}
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+1 -2
View File
@@ -3,7 +3,6 @@ import { ExcalidrawElement, PointerType } from "./types";
import { getElementAbsoluteCoords, Bounds } from "./bounds"; import { getElementAbsoluteCoords, Bounds } from "./bounds";
import { rotate } from "../math"; import { rotate } from "../math";
import { Zoom } from "../types"; import { Zoom } from "../types";
import { isTextElement } from ".";
export type TransformHandleDirection = export type TransformHandleDirection =
| "n" | "n"
@@ -243,7 +242,7 @@ export const getTransformHandles = (
omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH; omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
} }
} }
} else if (isTextElement(element)) { } else if (element.type === "text") {
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT; omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
} }
+1 -30
View File
@@ -7,7 +7,6 @@ import {
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
InitializedExcalidrawImageElement, InitializedExcalidrawImageElement,
ExcalidrawImageElement, ExcalidrawImageElement,
ExcalidrawTextElementWithContainer,
} from "./types"; } from "./types";
export const isGenericElement = ( export const isGenericElement = (
@@ -86,18 +85,7 @@ export const isBindableElement = (
(element.type === "rectangle" || (element.type === "rectangle" ||
element.type === "diamond" || element.type === "diamond" ||
element.type === "ellipse" || element.type === "ellipse" ||
element.type === "image" || element.type === "text")
(element.type === "text" && !element.containerId))
);
};
export const isTextBindableContainer = (element: ExcalidrawElement | null) => {
return (
element != null &&
(element.type === "rectangle" ||
element.type === "diamond" ||
element.type === "ellipse" ||
element.type === "image")
); );
}; };
@@ -112,20 +100,3 @@ export const isExcalidrawElement = (element: any): boolean => {
element?.type === "line" element?.type === "line"
); );
}; };
export const hasBoundTextElement = (
element: ExcalidrawElement | null,
): element is ExcalidrawBindableElement => {
return (
isBindableElement(element) &&
!!element.boundElements?.some(({ type }) => type === "text")
);
};
export const isBoundToContainer = (
element: ExcalidrawElement | null,
): element is ExcalidrawTextElementWithContainer => {
return (
element !== null && isTextElement(element) && element.containerId !== null
);
};
+2 -15
View File
@@ -43,15 +43,8 @@ type _ExcalidrawElementBase = Readonly<{
/** List of groups the element belongs to. /** List of groups the element belongs to.
Ordered from deepest to shallowest. */ Ordered from deepest to shallowest. */
groupIds: readonly GroupId[]; groupIds: readonly GroupId[];
/** other elements that are bound to this element */ /** Ids of (linear) elements that are bound to this element. */
boundElements: boundElementIds: readonly ExcalidrawLinearElement["id"][] | null;
| readonly Readonly<{
id: ExcalidrawLinearElement["id"];
type: "arrow" | "text";
}>[]
| null;
/** epoch (ms) timestamp of last element update */
updated: number;
}>; }>;
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & { export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
@@ -121,8 +114,6 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
baseline: number; baseline: number;
textAlign: TextAlign; textAlign: TextAlign;
verticalAlign: VerticalAlign; verticalAlign: VerticalAlign;
containerId: ExcalidrawGenericElement["id"] | null;
originalText: string;
}>; }>;
export type ExcalidrawBindableElement = export type ExcalidrawBindableElement =
@@ -132,10 +123,6 @@ export type ExcalidrawBindableElement =
| ExcalidrawTextElement | ExcalidrawTextElement
| ExcalidrawImageElement; | ExcalidrawImageElement;
export type ExcalidrawTextElementWithContainer = {
containerId: ExcalidrawGenericElement["id"];
} & ExcalidrawTextElement;
export type PointBinding = { export type PointBinding = {
elementId: ExcalidrawBindableElement["id"]; elementId: ExcalidrawBindableElement["id"];
focus: number; focus: number;
@@ -78,7 +78,7 @@ export const ExportToExcalidrawPlus: React.FC<{
onError: (error: Error) => void; onError: (error: Error) => void;
}> = ({ elements, appState, files, onError }) => { }> = ({ elements, appState, files, onError }) => {
return ( return (
<Card color="primary"> <Card color="indigo">
<div className="Card-icon">{excalidrawPlusIcon}</div> <div className="Card-icon">{excalidrawPlusIcon}</div>
<h2>Excalidraw+</h2> <h2>Excalidraw+</h2>
<div className="Card-details"> <div className="Card-details">
+1 -5
View File
@@ -199,11 +199,7 @@ export const encodeFilesForUpload = async ({
}); });
if (buffer.byteLength > maxBytes) { if (buffer.byteLength > maxBytes) {
throw new Error( throw new Error(t("errors.fileTooBig"));
t("errors.fileTooBig", {
maxSize: `${Math.trunc(maxBytes / 1024 / 1024)}MB`,
}),
);
} }
processedFiles.push({ processedFiles.push({
+1 -9
View File
@@ -11,15 +11,7 @@ import { MIME_TYPES } from "../../constants";
// private // private
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
let FIREBASE_CONFIG: Record<string, any>; const FIREBASE_CONFIG = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
try {
FIREBASE_CONFIG = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
} catch (error: any) {
console.warn(
`Error JSON parsing firebase config. Supplied value: ${process.env.REACT_APP_FIREBASE_CONFIG}`,
);
FIREBASE_CONFIG = {};
}
let firebasePromise: Promise<typeof import("firebase/app").default> | null = let firebasePromise: Promise<typeof import("firebase/app").default> | null =
null; null;
+37 -65
View File
@@ -1,6 +1,6 @@
import { compressData, decompressData } from "../../data/encode";
import { import {
decryptData, decryptData,
encryptData,
generateEncryptionKey, generateEncryptionKey,
IV_LENGTH_BYTES, IV_LENGTH_BYTES,
} from "../../data/encryption"; } from "../../data/encryption";
@@ -109,45 +109,9 @@ export const getCollaborationLink = (data: {
return `${window.location.origin}${window.location.pathname}#room=${data.roomId},${data.roomKey}`; return `${window.location.origin}${window.location.pathname}#room=${data.roomId},${data.roomKey}`;
}; };
/**
* Decodes shareLink data using the legacy buffer format.
* @deprecated
*/
const legacy_decodeFromBackend = async ({
buffer,
decryptionKey,
}: {
buffer: ArrayBuffer;
decryptionKey: string;
}) => {
let decrypted: ArrayBuffer;
try {
// Buffer should contain both the IV (fixed length) and encrypted data
const iv = buffer.slice(0, IV_LENGTH_BYTES);
const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
decrypted = await decryptData(new Uint8Array(iv), encrypted, decryptionKey);
} catch (error: any) {
// Fixed IV (old format, backward compatibility)
const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
decrypted = await decryptData(fixedIv, buffer, decryptionKey);
}
// We need to convert the decrypted array buffer to a string
const string = new window.TextDecoder("utf-8").decode(
new Uint8Array(decrypted),
);
const data: ImportedDataState = JSON.parse(string);
return {
elements: data.elements || null,
appState: data.appState || null,
};
};
const importFromBackend = async ( const importFromBackend = async (
id: string, id: string,
decryptionKey: string, privateKey: string,
): Promise<ImportedDataState> => { ): Promise<ImportedDataState> => {
try { try {
const response = await fetch(`${BACKEND_V2_GET}${id}`); const response = await fetch(`${BACKEND_V2_GET}${id}`);
@@ -158,28 +122,28 @@ const importFromBackend = async (
} }
const buffer = await response.arrayBuffer(); const buffer = await response.arrayBuffer();
let decrypted: ArrayBuffer;
try { try {
const { data: decodedBuffer } = await decompressData( // Buffer should contain both the IV (fixed length) and encrypted data
new Uint8Array(buffer), const iv = buffer.slice(0, IV_LENGTH_BYTES);
{ const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
decryptionKey, decrypted = await decryptData(new Uint8Array(iv), encrypted, privateKey);
},
);
const data: ImportedDataState = JSON.parse(
new TextDecoder().decode(decodedBuffer),
);
return {
elements: data.elements || null,
appState: data.appState || null,
};
} catch (error: any) { } catch (error: any) {
console.warn( // Fixed IV (old format, backward compatibility)
"error when decoding shareLink data using the new format:", const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
error, decrypted = await decryptData(fixedIv, buffer, privateKey);
);
return legacy_decodeFromBackend({ buffer, decryptionKey });
} }
// We need to convert the decrypted array buffer to a string
const string = new window.TextDecoder("utf-8").decode(
new Uint8Array(decrypted),
);
const data: ImportedDataState = JSON.parse(string);
return {
elements: data.elements || null,
appState: data.appState || null,
};
} catch (error: any) { } catch (error: any) {
window.alert(t("alerts.importBackendFailed")); window.alert(t("alerts.importBackendFailed"));
console.error(error); console.error(error);
@@ -224,14 +188,20 @@ export const exportToBackend = async (
appState: AppState, appState: AppState,
files: BinaryFiles, files: BinaryFiles,
) => { ) => {
const encryptionKey = await generateEncryptionKey("string"); const json = serializeAsJSON(elements, appState, files, "database");
const encoded = new TextEncoder().encode(json);
const payload = await compressData( const cryptoKey = await generateEncryptionKey("cryptoKey");
new TextEncoder().encode(
serializeAsJSON(elements, appState, files, "database"), const { encryptedBuffer, iv } = await encryptData(cryptoKey, encoded);
),
{ encryptionKey }, // Concatenate IV with encrypted data (IV does not have to be secret).
); const payloadBlob = new Blob([iv.buffer, encryptedBuffer]);
const payload = await new Response(payloadBlob).arrayBuffer();
// We use jwk encoding to be able to extract just the base64 encoded key.
// We will hardcode the rest of the attributes when importing back the key.
const exportedKey = await window.crypto.subtle.exportKey("jwk", cryptoKey);
try { try {
const filesMap = new Map<FileId, BinaryFileData>(); const filesMap = new Map<FileId, BinaryFileData>();
@@ -241,6 +211,8 @@ export const exportToBackend = async (
} }
} }
const encryptionKey = exportedKey.k!;
const filesToUpload = await encodeFilesForUpload({ const filesToUpload = await encodeFilesForUpload({
files: filesMap, files: filesMap,
encryptionKey, encryptionKey,
@@ -249,7 +221,7 @@ export const exportToBackend = async (
const response = await fetch(BACKEND_V2_POST, { const response = await fetch(BACKEND_V2_POST, {
method: "POST", method: "POST",
body: payload.buffer, body: payload,
}); });
const json = await response.json(); const json = await response.json();
if (json.id) { if (json.id) {
+1 -1
View File
@@ -9,7 +9,7 @@
.encrypted-icon { .encrypted-icon {
border-radius: var(--space-factor); border-radius: var(--space-factor);
color: var(--color-primary); color: var(--icon-green-fill-color);
margin-top: auto; margin-top: auto;
margin-bottom: auto; margin-bottom: auto;
margin-inline-start: auto; margin-inline-start: auto;
+4 -4
View File
@@ -1,3 +1,5 @@
// import type {PngChunk} from "./types";
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Document { interface Document {
fonts?: { fonts?: {
@@ -54,8 +56,6 @@ type NonOptional<T> = Exclude<T, undefined>;
// PNG encoding/decoding // PNG encoding/decoding
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
declare module "png-chunk-text" { declare module "png-chunk-text" {
function encode( function encode(
name: string, name: string,
@@ -64,11 +64,11 @@ declare module "png-chunk-text" {
function decode(data: Uint8Array): { keyword: string; text: string }; function decode(data: Uint8Array): { keyword: string; text: string };
} }
declare module "png-chunks-encode" { declare module "png-chunks-encode" {
function encode(chunks: TEXtChunk[]): Uint8Array; function encode(chunks: import("./types").PngChunk[]): Uint8Array;
export = encode; export = encode;
} }
declare module "png-chunks-extract" { declare module "png-chunks-extract" {
function extract(buffer: Uint8Array): TEXtChunk[]; function extract(buffer: Uint8Array): import("./types").PngChunk[];
export = extract; export = extract;
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "مشاركة", "share": "مشاركة",
"showStroke": "إظهار منتقي لون الخط", "showStroke": "إظهار منتقي لون الخط",
"showBackground": "إظهار منتقي لون الخلفية", "showBackground": "إظهار منتقي لون الخلفية",
"toggleTheme": "غير النمط", "toggleTheme": "غير النمط"
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "إعادة تعيين اللوحة", "clearReset": "إعادة تعيين اللوحة",
@@ -137,11 +135,7 @@
"zenMode": "وضع التأمل", "zenMode": "وضع التأمل",
"exitZenMode": "إلغاء الوضع الليلى", "exitZenMode": "إلغاء الوضع الليلى",
"cancel": "إلغاء", "cancel": "إلغاء",
"clear": "مسح", "clear": "مسح"
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "هذا سيُزيل كامل اللوحة. هل أنت متأكد؟", "clearReset": "هذا سيُزيل كامل اللوحة. هل أنت متأكد؟",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة", "cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.", "invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟", "resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل." "invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "انقر نقراً مزدوجاً أو اضغط Enter لتعديل النقاط", "lineEditor_info": "انقر نقراً مزدوجاً أو اضغط Enter لتعديل النقاط",
"lineEditor_pointSelected": "اضغط على حذف لإزالة النقطة، Ctrl Or Cmd+D للتكرار، أو اسحب للانتقال", "lineEditor_pointSelected": "اضغط على حذف لإزالة النقطة، Ctrl Or Cmd+D للتكرار، أو اسحب للانتقال",
"lineEditor_nothingSelected": "حدد نقطة لتحريك أو إزالتها، أو اضغط Alt ثم انقر لإضافة نقاط جديدة", "lineEditor_nothingSelected": "حدد نقطة لتحريك أو إزالتها، أو اضغط Alt ثم انقر لإضافة نقاط جديدة",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "تعذر عرض المعاينة", "cannotShowPreview": "تعذر عرض المعاينة",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.", "tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.",
"link": "مشاركة المدونة في التشفير من النهاية إلى النهاية في Excalidraw" "link": "مشاركة المدونة في التشفير من النهاية إلى النهاية في Excalidraw"
@@ -345,7 +289,6 @@
"width": "العرض" "width": "العرض"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "نسخت الانماط.", "copyStyles": "نسخت الانماط.",
"copyToClipboard": "نسخ إلى الحافظة.", "copyToClipboard": "نسخ إلى الحافظة.",
"copyToClipboardAsPng": "تم نسخ {{exportSelection}} إلى الحافظة بصيغة PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "تم نسخ {{exportSelection}} إلى الحافظة بصيغة PNG\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": ""
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Нулиране на платно", "clearReset": "Нулиране на платно",
@@ -137,11 +135,7 @@
"zenMode": "Режим Zen", "zenMode": "Режим Zen",
"exitZenMode": "Спиране на Zen режим", "exitZenMode": "Спиране на Zen режим",
"cancel": "", "cancel": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "Това ще изчисти цялото платно. Сигурни ли сте?", "clearReset": "Това ще изчисти цялото платно. Сигурни ли сте?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл", "cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Кликнете два пъти или натиснете Enter за да промените точките", "lineEditor_info": "Кликнете два пъти или натиснете Enter за да промените точките",
"lineEditor_pointSelected": "Натиснете Delete за да изтриете точка, CtrlOrCmd+D за дуплициране, или извлачете за да преместите", "lineEditor_pointSelected": "Натиснете Delete за да изтриете точка, CtrlOrCmd+D за дуплициране, или извлачете за да преместите",
"lineEditor_nothingSelected": "Изберете точка за местене или изтриване, или пък задръжте Alt и натиснете за да добавите нови точки", "lineEditor_nothingSelected": "Изберете точка за местене или изтриване, или пък задръжте Alt и натиснете за да добавите нови точки",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Невъзможност за показване на preview", "cannotShowPreview": "Невъзможност за показване на preview",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат.", "tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат.",
"link": "" "link": ""
@@ -345,7 +289,6 @@
"width": "Широчина" "width": "Широчина"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Копирани стилове.", "copyStyles": "Копирани стилове.",
"copyToClipboard": "Копирано в клипборда.", "copyToClipboard": "Копирано в клипборда.",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": ""
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "", "clearReset": "",
@@ -137,11 +135,7 @@
"zenMode": "", "zenMode": "",
"exitZenMode": "", "exitZenMode": "",
"cancel": "", "cancel": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "", "clearReset": "",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "", "cannotRestoreFromImage": "",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "", "lineEditor_info": "",
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "", "cannotShowPreview": "",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "", "tooltip": "",
"link": "" "link": ""
@@ -345,7 +289,6 @@
"width": "" "width": ""
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "", "copyStyles": "",
"copyToClipboard": "", "copyToClipboard": "",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Comparteix", "share": "Comparteix",
"showStroke": "Mostra el selector de color del traç", "showStroke": "Mostra el selector de color del traç",
"showBackground": "Mostra el selector de color de fons", "showBackground": "Mostra el selector de color de fons",
"toggleTheme": "Activa o desactiva el tema", "toggleTheme": "Activa o desactiva el tema"
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Neteja el llenç", "clearReset": "Neteja el llenç",
@@ -137,11 +135,7 @@
"zenMode": "Mode zen", "zenMode": "Mode zen",
"exitZenMode": "Surt de mode zen", "exitZenMode": "Surt de mode zen",
"cancel": "", "cancel": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "S'esborrarà tot el llenç. N'esteu segur?", "clearReset": "S'esborrarà tot el llenç. N'esteu segur?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Lescena no sha pogut restaurar des daquest fitxer dimatge", "cannotRestoreFromImage": "Lescena no sha pogut restaurar des daquest fitxer dimatge",
"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?",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Fes doble clic o premi Enter per editar punts", "lineEditor_info": "Fes doble clic o premi Enter per editar punts",
"lineEditor_pointSelected": "Premeu Suprimir per a eliminar el punt, CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l", "lineEditor_pointSelected": "Premeu Suprimir per a eliminar el punt, CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l",
"lineEditor_nothingSelected": "Selecciona un punt per moure o eliminar, o manté premut Alt i fes clic per afegir punts nous", "lineEditor_nothingSelected": "Selecciona un punt per moure o eliminar, o manté premut Alt i fes clic per afegir punts nous",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "No es pot mostrar la previsualització", "cannotShowPreview": "No es pot mostrar la previsualització",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors dExcalidraw no els veuran mai.", "tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors dExcalidraw no els veuran mai.",
"link": "Article del blog sobre encriptació d'extrem a extrem a Excalidraw" "link": "Article del blog sobre encriptació d'extrem a extrem a Excalidraw"
@@ -345,7 +289,6 @@
"width": "Amplada" "width": "Amplada"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "S'han copiat els estils.", "copyStyles": "S'han copiat els estils.",
"copyToClipboard": "S'ha copiat al porta-retalls.", "copyToClipboard": "S'ha copiat al porta-retalls.",
"copyToClipboardAsPng": "S'ha copiat {{exportSelection}} al porta-retalls en format PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "S'ha copiat {{exportSelection}} al porta-retalls en format PNG\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Sdílet", "share": "Sdílet",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "Přepnout tmavý řežim", "toggleTheme": "Přepnout tmavý řežim"
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "", "clearReset": "",
@@ -137,11 +135,7 @@
"zenMode": "Zen mód", "zenMode": "Zen mód",
"exitZenMode": "Opustit zen mód", "exitZenMode": "Opustit zen mód",
"cancel": "", "cancel": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "", "clearReset": "",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "", "cannotRestoreFromImage": "",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "", "lineEditor_info": "",
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "", "cannotShowPreview": "",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "", "tooltip": "",
"link": "" "link": ""
@@ -345,7 +289,6 @@
"width": "" "width": ""
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "", "copyStyles": "",
"copyToClipboard": "", "copyToClipboard": "",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Del", "share": "Del",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": ""
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "", "clearReset": "",
@@ -137,11 +135,7 @@
"zenMode": "", "zenMode": "",
"exitZenMode": "", "exitZenMode": "",
"cancel": "", "cancel": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "", "clearReset": "",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "", "cannotRestoreFromImage": "",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "", "lineEditor_info": "",
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "", "cannotShowPreview": "",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "", "tooltip": "",
"link": "" "link": ""
@@ -345,7 +289,6 @@
"width": "Bredde" "width": "Bredde"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Kopieret stilarter.", "copyStyles": "Kopieret stilarter.",
"copyToClipboard": "Kopieret til klippebord.", "copyToClipboard": "Kopieret til klippebord.",
"copyToClipboardAsPng": "Kopieret {{exportSelection}} til klippebord som PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Kopieret {{exportSelection}} til klippebord som PNG\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Teilen", "share": "Teilen",
"showStroke": "Auswahl für Strichfarbe anzeigen", "showStroke": "Auswahl für Strichfarbe anzeigen",
"showBackground": "Hintergrundfarbe auswählen", "showBackground": "Hintergrundfarbe auswählen",
"toggleTheme": "Design umschalten", "toggleTheme": "Design umschalten"
"personalLib": "Persönliche Bibliothek",
"excalidrawLib": "Excalidraw-Bibliothek"
}, },
"buttons": { "buttons": {
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen", "clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
@@ -137,11 +135,7 @@
"zenMode": "Zen-Modus", "zenMode": "Zen-Modus",
"exitZenMode": "Zen-Modus verlassen", "exitZenMode": "Zen-Modus verlassen",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"clear": "Löschen", "clear": "Löschen"
"remove": "Entfernen",
"publishLibrary": "Veröffentlichen",
"submit": "Absenden",
"confirm": "Bestätigen"
}, },
"alerts": { "alerts": {
"clearReset": "Dies wird die ganze Zeichenfläche löschen. Bist du dir sicher?", "clearReset": "Dies wird die ganze Zeichenfläche löschen. Bist du dir sicher?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Die Zeichnung konnte aus dieser Bilddatei nicht wiederhergestellt werden", "cannotRestoreFromImage": "Die Zeichnung konnte aus dieser Bilddatei nicht wiederhergestellt werden",
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.", "invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?", "resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?",
"removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?",
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert." "invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Doppelklicken oder Eingabetaste drücken, um Punkte zu bearbeiten", "lineEditor_info": "Doppelklicken oder Eingabetaste drücken, um Punkte zu bearbeiten",
"lineEditor_pointSelected": "Drücke Löschen, um Punkt zu entfernen, Strg+D oder Cmd+D zum Duplizieren oder ziehe zum Verschieben", "lineEditor_pointSelected": "Drücke Löschen, um Punkt zu entfernen, Strg+D oder Cmd+D zum Duplizieren oder ziehe zum Verschieben",
"lineEditor_nothingSelected": "Wähle einen Punkt zum Verschieben oder Löschen oder halte die Alt-Taste gedrückt und klicke, um neue Punkte hinzuzufügen", "lineEditor_nothingSelected": "Wähle einen Punkt zum Verschieben oder Löschen oder halte die Alt-Taste gedrückt und klicke, um neue Punkte hinzuzufügen",
"placeImage": "Klicken, um das Bild zu platzieren oder klicken und ziehen um seine Größe manuell zu setzen", "placeImage": "Klicken, um das Bild zu platzieren oder klicken und ziehen um seine Größe manuell zu setzen"
"publishLibrary": "Veröffentliche deine eigene Bibliothek"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Vorschau kann nicht angezeigt werden", "cannotShowPreview": "Vorschau kann nicht angezeigt werden",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Zeichenfläche löschen" "title": "Zeichenfläche löschen"
}, },
"publishDialog": {
"title": "Bibliothek veröffentlichen",
"itemName": "Elementname",
"authorName": "Name des Autors",
"githubUsername": "GitHub-Benutzername",
"twitterUsername": "Twitter-Benutzername",
"libraryName": "Name der Bibliothek",
"libraryDesc": "Beschreibung der Bibliothek",
"website": "Webseite",
"placeholder": {
"authorName": "Dein Name oder Benutzername",
"libraryName": "Name deiner Bibliothek",
"libraryDesc": "Beschreibung deiner Bibliothek, um anderen Nutzern bei der Verwendung zu helfen",
"githubHandle": "GitHub-Handle (optional), damit du die Bibliothek bearbeiten kannst, wenn sie zur Überprüfung eingereicht wurde",
"twitterHandle": "Twitter-Benutzername (optional), damit wir wissen, wen wir bei Werbung über Twitter nennen können",
"website": "Link zu deiner persönlichen Webseite oder zu anderer Seite (optional)"
},
"errors": {
"required": "Erforderlich",
"website": "Gültige URL eingeben"
},
"noteDescription": {
"pre": "Sende deine Bibliothek ein, um in die ",
"link": "öffentliche Bibliotheks-Repository aufgenommen zu werden",
"post": "damit andere Nutzer sie in ihren Zeichnungen verwenden können."
},
"noteGuidelines": {
"pre": "Die Bibliothek muss zuerst manuell freigegeben werden. Bitte lies die ",
"link": "Richtlinien",
"post": " vor dem Absenden. Du benötigst ein GitHub-Konto, um zu kommunizieren und Änderungen vorzunehmen, falls erforderlich, aber es ist nicht unbedingt erforderlich."
},
"noteLicense": {
"pre": "Mit dem Absenden stimmst du zu, dass die Bibliothek unter der ",
"link": "MIT-Lizenz, ",
"post": "die zusammengefasst beinhaltet, dass jeder sie ohne Einschränkungen nutzen kann."
},
"noteItems": "Jedes Bibliothekselement muss einen eigenen Namen haben, damit es gefiltert werden kann. Die folgenden Bibliothekselemente werden hinzugefügt:",
"atleastOneLibItem": "Bitte wähle mindestens ein Bibliothekselement aus, um zu beginnen"
},
"publishSuccessDialog": {
"title": "Bibliothek übermittelt",
"content": "Vielen Dank {{authorName}}. Deine Bibliothek wurde zur Überprüfung eingereicht. Du kannst den Status verfolgen",
"link": "hier"
},
"confirmDialog": {
"resetLibrary": "Bibliothek zurücksetzen",
"removeItemsFromLib": "Ausgewählte Elemente aus der Bibliothek entfernen"
},
"encrypted": { "encrypted": {
"tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals.", "tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals.",
"link": "Blogbeitrag über Ende-zu-Ende-Verschlüsselung in Excalidraw" "link": "Blogbeitrag über Ende-zu-Ende-Verschlüsselung in Excalidraw"
@@ -345,7 +289,6 @@
"width": "Breite" "width": "Breite"
}, },
"toast": { "toast": {
"addedToLibrary": "Zur Bibliothek hinzugefügt",
"copyStyles": "Formatierungen kopiert.", "copyStyles": "Formatierungen kopiert.",
"copyToClipboard": "In die Zwischenablage kopiert.", "copyToClipboard": "In die Zwischenablage kopiert.",
"copyToClipboardAsPng": "{{exportSelection}} als PNG in die Zwischenablage kopiert\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} als PNG in die Zwischenablage kopiert\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Κοινοποίηση", "share": "Κοινοποίηση",
"showStroke": "Εμφάνιση επιλογέα χρωμάτων πινελιάς", "showStroke": "Εμφάνιση επιλογέα χρωμάτων πινελιάς",
"showBackground": "Εμφάνιση επιλογέα χρώματος φόντου", "showBackground": "Εμφάνιση επιλογέα χρώματος φόντου",
"toggleTheme": "Εναλλαγή θέματος", "toggleTheme": "Εναλλαγή θέματος"
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Επαναφορά του καμβά", "clearReset": "Επαναφορά του καμβά",
@@ -137,11 +135,7 @@
"zenMode": "Λειτουργία Zεν", "zenMode": "Λειτουργία Zεν",
"exitZenMode": "Έξοδος από την λειτουργία Zen", "exitZenMode": "Έξοδος από την λειτουργία Zen",
"cancel": "Ακύρωση", "cancel": "Ακύρωση",
"clear": "Καθαρισμός", "clear": "Καθαρισμός"
"remove": "Κατάργηση",
"publishLibrary": "Δημοσίευση",
"submit": "Υποβολή",
"confirm": "Επιβεβαίωση"
}, },
"alerts": { "alerts": {
"clearReset": "Αυτό θα σβήσει ολόκληρο τον καμβά. Είσαι σίγουρος;", "clearReset": "Αυτό θα σβήσει ολόκληρο τον καμβά. Είσαι σίγουρος;",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας", "cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;", "resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη." "invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Διπλό-κλικ ή πιέστε Enter για να επεξεργαστείτε τα σημεία", "lineEditor_info": "Διπλό-κλικ ή πιέστε Enter για να επεξεργαστείτε τα σημεία",
"lineEditor_pointSelected": "Πιέστε Διαγραφή για να αφαιρέσετε το σημείου, CtrlOrCmd+D για να το αντιγράψετε ή σύρτε το για να το μετακινήσετε", "lineEditor_pointSelected": "Πιέστε Διαγραφή για να αφαιρέσετε το σημείου, CtrlOrCmd+D για να το αντιγράψετε ή σύρτε το για να το μετακινήσετε",
"lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για μετακίνηση ή αφαίρεση, ή κρατήστε παρατεταμένα το Alt και κάντε κλικ για να προσθέσετε νέα σημεία", "lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για μετακίνηση ή αφαίρεση, ή κρατήστε παρατεταμένα το Alt και κάντε κλικ για να προσθέσετε νέα σημεία",
"placeImage": "", "placeImage": ""
"publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης", "cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Καθαρισμός καμβά" "title": "Καθαρισμός καμβά"
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "Όνομα δημιουργού",
"githubUsername": "GitHub username",
"twitterUsername": "Twitter username",
"libraryName": "Όνομα βιβλιοθήκης",
"libraryDesc": "",
"website": "Ιστοσελίδα",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": "Εισάγετε μια έγκυρη διεύθυνση URL"
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw.", "tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw.",
"link": "Blog post στην κρυπτογράφηση end-to-end στο Excalidraw" "link": "Blog post στην κρυπτογράφηση end-to-end στο Excalidraw"
@@ -345,7 +289,6 @@
"width": "Πλάτος" "width": "Πλάτος"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Αντιγράφηκαν στυλ.", "copyStyles": "Αντιγράφηκαν στυλ.",
"copyToClipboard": "Αντιγράφηκε στο πρόχειρο.", "copyToClipboard": "Αντιγράφηκε στο πρόχειρο.",
"copyToClipboardAsPng": "Αντιγράφηκε {{exportSelection}} στο πρόχειρο ως PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Αντιγράφηκε {{exportSelection}} στο πρόχειρο ως PNG\n({{exportColorScheme}})",
+4 -5
View File
@@ -169,7 +169,7 @@
"errors": { "errors": {
"unsupportedFileType": "Unsupported file type.", "unsupportedFileType": "Unsupported file type.",
"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.",
"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."
}, },
@@ -204,11 +204,10 @@
"resizeImage": "You can resize freely by holding SHIFT,\nhold ALT to resize from the center", "resizeImage": "You can resize freely by holding SHIFT,\nhold ALT to resize from the center",
"rotate": "You can constrain angles by holding SHIFT while rotating", "rotate": "You can constrain angles by holding SHIFT while rotating",
"lineEditor_info": "Double-click or press Enter to edit points", "lineEditor_info": "Double-click or press Enter to edit points",
"lineEditor_pointSelected": "Press Delete to remove point(s),\nCtrlOrCmd+D to duplicate, or drag to move", "lineEditor_pointSelected": "Press Delete to remove point, CtrlOrCmd+D to duplicate, or drag to move",
"lineEditor_nothingSelected": "Select a point to edit (hold SHIFT to select multiple),\nor hold Alt and click to add new points", "lineEditor_nothingSelected": "Select a point to move or remove, or hold Alt and click to add new points",
"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"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Cannot show preview", "cannotShowPreview": "Cannot show preview",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Compartir", "share": "Compartir",
"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": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Limpiar lienzo y reiniciar el color de fondo", "clearReset": "Limpiar lienzo y reiniciar el color de fondo",
@@ -137,11 +135,7 @@
"zenMode": "Modo Zen", "zenMode": "Modo Zen",
"exitZenMode": "Salir del modo Zen", "exitZenMode": "Salir del modo Zen",
"cancel": "Cancelar", "cancel": "Cancelar",
"clear": "Borrar", "clear": "Borrar"
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "Esto limpiará todo el lienzo. Estás seguro?", "clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
@@ -163,7 +157,6 @@
"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": "",
"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": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Doble clic o pulse Enter para editar puntos", "lineEditor_info": "Doble clic o pulse Enter para editar puntos",
"lineEditor_pointSelected": "Presione Suprimir para eliminar el punto, CtrlOrCmd+D para duplicarlo, o arrástrelo para moverlo", "lineEditor_pointSelected": "Presione Suprimir para eliminar el punto, CtrlOrCmd+D para duplicarlo, o arrástrelo para moverlo",
"lineEditor_nothingSelected": "Selecciona un punto sea para mover o eliminar, o mantén pulsado Alt y haz clic para añadir nuevos puntos", "lineEditor_nothingSelected": "Selecciona un punto sea para mover o eliminar, o mantén pulsado Alt y haz 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": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "No se puede mostrar la vista previa", "cannotShowPreview": "No se puede mostrar la vista previa",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Borrar lienzo" "title": "Borrar lienzo"
}, },
"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": { "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.",
"link": "Entrada en el blog sobre cifrado de extremo a extremo" "link": "Entrada en el blog sobre cifrado de extremo a extremo"
@@ -345,7 +289,6 @@
"width": "Ancho" "width": "Ancho"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"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}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "اشتراک‌گذاری", "share": "اشتراک‌گذاری",
"showStroke": "نمایش انتخاب کننده رنگ حاشیه", "showStroke": "نمایش انتخاب کننده رنگ حاشیه",
"showBackground": "نمایش انتخاب کننده رنگ پس زمینه", "showBackground": "نمایش انتخاب کننده رنگ پس زمینه",
"toggleTheme": "تغییر تم", "toggleTheme": "تغییر تم"
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "پاکسازی بوم نقاشی", "clearReset": "پاکسازی بوم نقاشی",
@@ -137,11 +135,7 @@
"zenMode": "حالت ذن", "zenMode": "حالت ذن",
"exitZenMode": "خروج از حالت تمرکز", "exitZenMode": "خروج از حالت تمرکز",
"cancel": "", "cancel": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟", "clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد", "cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید", "lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
"lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید.", "lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید.",
"lineEditor_nothingSelected": "یک نقطه را برای جابجایی یا حذف انتخاب کنید یا کلید Alt بگیرید و کلیک کنید تا بتوانید یک نقطه جدید اضافه کنید", "lineEditor_nothingSelected": "یک نقطه را برای جابجایی یا حذف انتخاب کنید یا کلید Alt بگیرید و کلیک کنید تا بتوانید یک نقطه جدید اضافه کنید",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "پیش نمایش نشان داده نمی شود", "cannotShowPreview": "پیش نمایش نشان داده نمی شود",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.", "tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
"link": "" "link": ""
@@ -345,7 +289,6 @@
"width": "عرض" "width": "عرض"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "کپی سبک.", "copyStyles": "کپی سبک.",
"copyToClipboard": "در کلیپ‌بورد کپی شد.", "copyToClipboard": "در کلیپ‌بورد کپی شد.",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Jaa", "share": "Jaa",
"showStroke": "Näytä viivan värin valitsin", "showStroke": "Näytä viivan värin valitsin",
"showBackground": "Näytä taustavärin valitsin", "showBackground": "Näytä taustavärin valitsin",
"toggleTheme": "Vaihda teema", "toggleTheme": "Vaihda teema"
"personalLib": "Oma kirjasto",
"excalidrawLib": "Excalidraw kirjasto"
}, },
"buttons": { "buttons": {
"clearReset": "Tyhjennä piirtoalue", "clearReset": "Tyhjennä piirtoalue",
@@ -137,11 +135,7 @@
"zenMode": "Zen-tila", "zenMode": "Zen-tila",
"exitZenMode": "Poistu zen-tilasta", "exitZenMode": "Poistu zen-tilasta",
"cancel": "Peruuta", "cancel": "Peruuta",
"clear": "Pyyhi", "clear": "Pyyhi"
"remove": "Poista",
"publishLibrary": "Julkaise",
"submit": "Lähetä",
"confirm": "Vahvista"
}, },
"alerts": { "alerts": {
"clearReset": "Tämä tyhjentää koko piirtoalueen. Jatketaanko?", "clearReset": "Tämä tyhjentää koko piirtoalueen. Jatketaanko?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Teosta ei voitu palauttaa tästä kuvatiedostosta", "cannotRestoreFromImage": "Teosta ei voitu palauttaa tästä kuvatiedostosta",
"invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.", "invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.",
"resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?", "resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?",
"removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?",
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä." "invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Kaksoisnapauta tai paina Enter muokataksesi pisteitä", "lineEditor_info": "Kaksoisnapauta tai paina Enter muokataksesi pisteitä",
"lineEditor_pointSelected": "Paina Delete poistaaksesi pisteen, Ctrl tai Cmd+D monistaaksesi, tai raahaa liikuttaaksesi", "lineEditor_pointSelected": "Paina Delete poistaaksesi pisteen, Ctrl tai Cmd+D monistaaksesi, tai raahaa liikuttaaksesi",
"lineEditor_nothingSelected": "Valitse liikutettava tai poistettava piste, tai pidä ALT-näppäintä alaspainettuna ja napsauta lisätäksesi uusia pisteitä", "lineEditor_nothingSelected": "Valitse liikutettava tai poistettava piste, tai pidä ALT-näppäintä alaspainettuna ja napsauta lisätäksesi uusia pisteitä",
"placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti", "placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti"
"publishLibrary": "Julkaise oma kirjasto"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Esikatselua ei voitu näyttää", "cannotShowPreview": "Esikatselua ei voitu näyttää",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Pyyhi piirtoalue" "title": "Pyyhi piirtoalue"
}, },
"publishDialog": {
"title": "Julkaise kirjasto",
"itemName": "Kohteen nimi",
"authorName": "Tekijän nimi",
"githubUsername": "GitHub-käyttäjätunnus",
"twitterUsername": "Twitter-käyttäjätunnus",
"libraryName": "Kirjaston nimi",
"libraryDesc": "Kirjaston kuvaus",
"website": "Verkkosivu",
"placeholder": {
"authorName": "Nimesi tai käyttäjänimesi",
"libraryName": "Kirjastosi nimi",
"libraryDesc": "Kirjaston kuvaus, joka auttaa ihmisiä ymmärtämään sen käyttötarkoitukset",
"githubHandle": "GitHub-tunnuksesi (valinnainen), jotta voit muokata kirjastoa sen jälkeen kun se on lähetetty tarkastettavaksi",
"twitterHandle": "Twitter-tunnus (valinnainen), jotta tiedämme ketä kiittää kun viestimme Twitterissä",
"website": "Linkki henkilökohtaiselle verkkosivustollesi tai muualle (valinnainen)"
},
"errors": {
"required": "Pakollinen",
"website": "Syötä oikeamuotoinen URL-osoite"
},
"noteDescription": {
"pre": "Lähetä kirjastosi, jotta se voidaan sisällyttää ",
"link": "julkisessa kirjastolistauksessa",
"post": "muiden käyttöön omissa piirrustuksissaan."
},
"noteGuidelines": {
"pre": "Kirjasto on ensin hyväksyttävä manuaalisesti. Ole hyvä ja lue ",
"link": "ohjeet",
"post": " ennen lähettämistä. Tarvitset GitHub-tilin, jotta voit viestiä ja tehdä muutoksia pyydettäessä, mutta se ei ole ehdottoman välttämätöntä."
},
"noteLicense": {
"pre": "Lähettämällä hyväksyt että kirjasto julkaistaan ",
"link": "MIT-lisenssin ",
"post": "alla, mikä lyhyesti antaa muiden käyttää sitä ilman rajoituksia."
},
"noteItems": "Jokaisella kirjaston kohteella on oltava oma nimensä suodatusta varten. Seuraavat kirjaston kohteet sisältyvät:",
"atleastOneLibItem": "Valitse vähintään yksi kirjaston kohde aloittaaksesi"
},
"publishSuccessDialog": {
"title": "Kirjasto lähetetty",
"content": "Kiitos {{authorName}}. Kirjastosi on lähetetty tarkistettavaksi. Voit seurata sen tilaa",
"link": "täällä"
},
"confirmDialog": {
"resetLibrary": "Tyhjennä kirjasto",
"removeItemsFromLib": "Poista valitut kohteet kirjastosta"
},
"encrypted": { "encrypted": {
"tooltip": "Piirroksesi ovat päästä-päähän-salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä.", "tooltip": "Piirroksesi ovat päästä-päähän-salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä.",
"link": "Blogiartikkeli päästä päähän -salauksesta Excalidraw:ssa" "link": "Blogiartikkeli päästä päähän -salauksesta Excalidraw:ssa"
@@ -345,7 +289,6 @@
"width": "Leveys" "width": "Leveys"
}, },
"toast": { "toast": {
"addedToLibrary": "Lisätty kirjastoon",
"copyStyles": "Tyylit kopioitiin.", "copyStyles": "Tyylit kopioitiin.",
"copyToClipboard": "Kopioitiin leikepöydälle.", "copyToClipboard": "Kopioitiin leikepöydälle.",
"copyToClipboardAsPng": "Kopioitiin {{exportSelection}} leikepöydälle PNG:nä\n({{exportColorScheme}})", "copyToClipboardAsPng": "Kopioitiin {{exportSelection}} leikepöydälle PNG:nä\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Partager", "share": "Partager",
"showStroke": "Afficher le sélecteur de couleur de trait", "showStroke": "Afficher le sélecteur de couleur de trait",
"showBackground": "Afficher le sélecteur de couleur d'arrière-plan", "showBackground": "Afficher le sélecteur de couleur d'arrière-plan",
"toggleTheme": "Changer le thème", "toggleTheme": "Changer le thème"
"personalLib": "Bibliothèque personnelle",
"excalidrawLib": "Bibliothèque Excalidraw"
}, },
"buttons": { "buttons": {
"clearReset": "Réinitialiser le canevas", "clearReset": "Réinitialiser le canevas",
@@ -137,11 +135,7 @@
"zenMode": "Mode zen", "zenMode": "Mode zen",
"exitZenMode": "Quitter le mode zen", "exitZenMode": "Quitter le mode zen",
"cancel": "Annuler", "cancel": "Annuler",
"clear": "Effacer", "clear": "Effacer"
"remove": "Supprimer",
"publishLibrary": "Publier",
"submit": "Envoyer",
"confirm": "Confirmer"
}, },
"alerts": { "alerts": {
"clearReset": "L'intégralité du canevas va être effacée. Êtes-vous sûr ?", "clearReset": "L'intégralité du canevas va être effacée. Êtes-vous sûr ?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Impossible de restaurer la scène depuis ce fichier image", "cannotRestoreFromImage": "Impossible de restaurer la scène depuis ce fichier image",
"invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.", "invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.",
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?", "resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?",
"removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?",
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée." "invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Double-cliquez ou appuyez sur Entrée pour éditer les points", "lineEditor_info": "Double-cliquez ou appuyez sur Entrée pour éditer les points",
"lineEditor_pointSelected": "Appuyez sur Supprimer pour supprimer le point, Ctrl ou Cmd+D pour le dupliquer, ou faites-le glisser pour le déplacer", "lineEditor_pointSelected": "Appuyez sur Supprimer pour supprimer le point, Ctrl ou Cmd+D pour le dupliquer, ou faites-le glisser pour le déplacer",
"lineEditor_nothingSelected": "Sélectionnez un point à déplacer ou supprimer, ou maintenez Alt et cliquez pour ajouter de nouveaux points", "lineEditor_nothingSelected": "Sélectionnez un point à déplacer ou supprimer, ou maintenez Alt et cliquez pour ajouter de nouveaux points",
"placeImage": "Cliquez pour placer l'image, ou cliquez et faites glisser pour définir sa taille manuellement", "placeImage": "Cliquez pour placer l'image, ou cliquez et faites glisser pour définir sa taille manuellement"
"publishLibrary": "Publier votre propre bibliothèque"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Impossible dafficher laperçu", "cannotShowPreview": "Impossible dafficher laperçu",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Effacer la zone de dessin" "title": "Effacer la zone de dessin"
}, },
"publishDialog": {
"title": "Publier la bibliothèque",
"itemName": "Nom de l’élément",
"authorName": "Nom de l'auteur",
"githubUsername": "Nom d'utilisateur GitHub",
"twitterUsername": "Nom d'utilisateur Twitter",
"libraryName": "Nom de la bibliothèque",
"libraryDesc": "Description de la bibliothèque",
"website": "Site web",
"placeholder": {
"authorName": "Votre nom ou nom d'utilisateur",
"libraryName": "Nom de votre bibliothèque",
"libraryDesc": "Description de votre bibliothèque pour aider les gens à comprendre son usage",
"githubHandle": "Nom d'utilisateur GitHub (optionnel), pour que tu puisses modifier la bibliothèque une fois soumise pour vérification",
"twitterHandle": "Nom d'utilisateur Twitter (optionnel), pour savoir qui créditer lors de la promotion sur Twitter",
"website": "Lien vers votre site web personnel ou autre (optionnel)"
},
"errors": {
"required": "Requis",
"website": "Entrer une URL valide"
},
"noteDescription": {
"pre": "Soumets ta bibliothèque pour l'inclure au ",
"link": "dépôt de bibliothèque publique",
"post": "pour permettre son utilisation par autrui dans leurs dessins."
},
"noteGuidelines": {
"pre": "La bibliothèque doit d'abord être approuvée manuellement. Veuillez lire les ",
"link": "lignes directrices",
"post": " avant de la soumettre. Vous aurez besoin d'un compte GitHub pour communiquer et apporter des modifications si demandé, mais ce n'est pas obligatoire."
},
"noteLicense": {
"pre": "En soumettant, vous acceptez que la bibliothèque soit publiée sous la ",
"link": "Licence MIT, ",
"post": "ce qui en gros signifie que tout le monde peut l'utiliser sans restrictions."
},
"noteItems": "Chaque élément de la bibliothèque doit avoir son propre nom afin qu'il soit filtrable. Les éléments de bibliothèque suivants seront inclus :",
"atleastOneLibItem": "Veuillez sélectionner au moins un élément de bibliothèque pour commencer"
},
"publishSuccessDialog": {
"title": "Bibliothèque soumise",
"content": "Merci {{authorName}}. Votre bibliothèque a été soumise pour examen. Vous pouvez suivre le statut",
"link": "ici"
},
"confirmDialog": {
"resetLibrary": "Réinitialiser la bibliothèque",
"removeItemsFromLib": "Enlever les éléments sélectionnés de la bibliothèque"
},
"encrypted": { "encrypted": {
"tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais.", "tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais.",
"link": "Article de blog sur le chiffrement de bout en bout dans Excalidraw" "link": "Article de blog sur le chiffrement de bout en bout dans Excalidraw"
@@ -345,7 +289,6 @@
"width": "Largeur" "width": "Largeur"
}, },
"toast": { "toast": {
"addedToLibrary": "Ajouté à la bibliothèque",
"copyStyles": "Styles copiés.", "copyStyles": "Styles copiés.",
"copyToClipboard": "Copié dans le presse-papier.", "copyToClipboard": "Copié dans le presse-papier.",
"copyToClipboardAsPng": "{{exportSelection}} copié dans le presse-papier en PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} copié dans le presse-papier en PNG\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "שתף", "share": "שתף",
"showStroke": "הצג צבעי קו מתאר", "showStroke": "הצג צבעי קו מתאר",
"showBackground": "הצג צבעי רקע", "showBackground": "הצג צבעי רקע",
"toggleTheme": "שינוי ערכת העיצוב", "toggleTheme": "שינוי ערכת העיצוב"
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "אפס את הלוח", "clearReset": "אפס את הלוח",
@@ -137,11 +135,7 @@
"zenMode": "מצב זן", "zenMode": "מצב זן",
"exitZenMode": "צא ממצב תפריט מרחף", "exitZenMode": "צא ממצב תפריט מרחף",
"cancel": "", "cancel": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "פעולה זו תנקה את כל הלוח. אתה בטוח?", "clearReset": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "לא הצלחנו לשחזר את התצוגה מקובץ התמונה", "cannotRestoreFromImage": "לא הצלחנו לשחזר את התצוגה מקובץ התמונה",
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.", "invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?", "resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "לחץ לחיצה כפולה או אנטר לעריכת הנקודות", "lineEditor_info": "לחץ לחיצה כפולה או אנטר לעריכת הנקודות",
"lineEditor_pointSelected": "לחץ על Delete להסרת נקודה, CtrlOrCmd+D לשכפל, או גרור להזזה", "lineEditor_pointSelected": "לחץ על Delete להסרת נקודה, CtrlOrCmd+D לשכפל, או גרור להזזה",
"lineEditor_nothingSelected": "בחר נקודה להזזה או הסרה, או החזק את כפתור Alt והקלק להוספת נקודות חדשות", "lineEditor_nothingSelected": "בחר נקודה להזזה או הסרה, או החזק את כפתור Alt והקלק להוספת נקודות חדשות",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה", "cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.", "tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.",
"link": "פוסט בבלוג על הצפנה מקצה לקצב ב-Excalidraw" "link": "פוסט בבלוג על הצפנה מקצה לקצב ב-Excalidraw"
@@ -345,7 +289,6 @@
"width": "רוחב" "width": "רוחב"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "העתק סגנונות.", "copyStyles": "העתק סגנונות.",
"copyToClipboard": "הועתק אל הלוח.", "copyToClipboard": "הועתק אל הלוח.",
"copyToClipboardAsPng": "{{exportSelection}} הועתקה ללוח כ-PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} הועתקה ללוח כ-PNG\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": ""
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "कैनवास रीसेट करें", "clearReset": "कैनवास रीसेट करें",
@@ -137,11 +135,7 @@
"zenMode": "ज़ेन मोड", "zenMode": "ज़ेन मोड",
"exitZenMode": "जेन मोड से बाहर निकलें", "exitZenMode": "जेन मोड से बाहर निकलें",
"cancel": "", "cancel": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "इससे पूरा कैनवास साफ हो जाएगा। क्या आपको यकीन है?", "clearReset": "इससे पूरा कैनवास साफ हो जाएगा। क्या आपको यकीन है?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "छवि फ़ाइल बहाल दृश्य नहीं है", "cannotRestoreFromImage": "छवि फ़ाइल बहाल दृश्य नहीं है",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "बिंदुओं को संपादित करने के लिए Enter पर डबल-क्लिक करें या दबाएँ", "lineEditor_info": "बिंदुओं को संपादित करने के लिए Enter पर डबल-क्लिक करें या दबाएँ",
"lineEditor_pointSelected": "बिंदु हटाने के लिए डिलीट दबाएं, प्रतिरूपित करने के लिए कण्ट्रोल या कमांड डी दबाएं या स्थानांतरित करने के लिए खींचे", "lineEditor_pointSelected": "बिंदु हटाने के लिए डिलीट दबाएं, प्रतिरूपित करने के लिए कण्ट्रोल या कमांड डी दबाएं या स्थानांतरित करने के लिए खींचे",
"lineEditor_nothingSelected": "स्थानांतरित करने या हटाने के लिए एक बिंदु का चयन करें, या Alt दबाए रखें और नए बिंदुओं को जोड़ने के लिए क्लिक करें", "lineEditor_nothingSelected": "स्थानांतरित करने या हटाने के लिए एक बिंदु का चयन करें, या Alt दबाए रखें और नए बिंदुओं को जोड़ने के लिए क्लिक करें",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं", "cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।", "tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।",
"link": "" "link": ""
@@ -345,7 +289,6 @@
"width": "चौड़ाई" "width": "चौड़ाई"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "काॅपी कीए स्टाइल", "copyStyles": "काॅपी कीए स्टाइल",
"copyToClipboard": "क्लिपबोर्ड में कॉपी कीए", "copyToClipboard": "क्लिपबोर्ड में कॉपी कीए",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": ""
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Vászon törlése", "clearReset": "Vászon törlése",
@@ -137,11 +135,7 @@
"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": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"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?",
@@ -163,7 +157,6 @@
"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": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"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": "Nyomd meg a delete gombot a pont eltávolításához, Ctrl vagy Cmd + D-t a duplikáláshoz, vagy húzva mozgasd", "lineEditor_pointSelected": "Nyomd meg a delete gombot a pont eltávolításához, Ctrl vagy Cmd + D-t a duplikáláshoz, vagy húzva mozgasd",
"lineEditor_nothingSelected": "Válassz ki egy pontot a mozgatáshoz vagy törtléshez, vagy az Alt lenyomása mellett kattintva hozz létre új pontokat", "lineEditor_nothingSelected": "Válassz ki egy pontot a mozgatáshoz vagy törtléshez, vagy az Alt lenyomása mellett kattintva hozz létre új pontokat",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Előnézet nem jeleníthető meg", "cannotShowPreview": "Előnézet nem jeleníthető meg",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "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": ""
@@ -345,7 +289,6 @@
"width": "Szélesség" "width": "Szélesség"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "", "copyStyles": "",
"copyToClipboard": "", "copyToClipboard": "",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Bagikan", "share": "Bagikan",
"showStroke": "Tampilkan garis pengambil warna", "showStroke": "Tampilkan garis pengambil warna",
"showBackground": "Tampilkan latar pengambil warna", "showBackground": "Tampilkan latar pengambil warna",
"toggleTheme": "Ubah tema", "toggleTheme": "Ubah tema"
"personalLib": "Pustaka Pribadi",
"excalidrawLib": "Pustaka Excalidraw"
}, },
"buttons": { "buttons": {
"clearReset": "Setel Ulang Kanvas", "clearReset": "Setel Ulang Kanvas",
@@ -137,11 +135,7 @@
"zenMode": "Mode zen", "zenMode": "Mode zen",
"exitZenMode": "Keluar dari mode zen", "exitZenMode": "Keluar dari mode zen",
"cancel": "Batal", "cancel": "Batal",
"clear": "Hapus", "clear": "Hapus"
"remove": "Hapus",
"publishLibrary": "Terbitkan",
"submit": "Kirimkan",
"confirm": "Konfirmasi"
}, },
"alerts": { "alerts": {
"clearReset": "Ini akan menghapus semua yang ada dikanvas. Apakah kamu yakin ?", "clearReset": "Ini akan menghapus semua yang ada dikanvas. Apakah kamu yakin ?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Pemandangan tidak dapat dipulihkan dari file gambar ini", "cannotRestoreFromImage": "Pemandangan tidak dapat dipulihkan dari file gambar ini",
"invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.", "invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.",
"resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?", "resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?",
"removeItemsFromsLibrary": "Hapus {{count}} item dari pustaka?",
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan." "invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Klik ganda atau tekan Enter untuk mengedit titik", "lineEditor_info": "Klik ganda atau tekan Enter untuk mengedit titik",
"lineEditor_pointSelected": "Tekan Delete untuk menghapus titik, Ctrl/Cmd + D untuk menduplikasi, atau seret untuk memindahkan", "lineEditor_pointSelected": "Tekan Delete untuk menghapus titik, Ctrl/Cmd + D untuk menduplikasi, atau seret untuk memindahkan",
"lineEditor_nothingSelected": "Pilih sebuah titik untuk memindah atau menghapus, atau tekan Alt dan klik untuk menambahkan titik baru", "lineEditor_nothingSelected": "Pilih sebuah titik untuk memindah atau menghapus, atau tekan Alt dan klik untuk menambahkan titik baru",
"placeImage": "Klik untuk tempatkan gambar, atau klik dan jatuhkan untuk tetapkan ukuran secara manual", "placeImage": "Klik untuk tempatkan gambar, atau klik dan jatuhkan untuk tetapkan ukuran secara manual"
"publishLibrary": "Terbitkan pustaka Anda"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Tidak dapat menampilkan pratinjau", "cannotShowPreview": "Tidak dapat menampilkan pratinjau",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Hapus kanvas" "title": "Hapus kanvas"
}, },
"publishDialog": {
"title": "Terbitkan pustaka",
"itemName": "Nama item",
"authorName": "Nama pembuat",
"githubUsername": "Nama pengguna github",
"twitterUsername": "Nama pengguna Twitter",
"libraryName": "Nama Pustaka",
"libraryDesc": "Deskripsi pustaka",
"website": "Situs Web",
"placeholder": {
"authorName": "Nama atau nama pengguna Anda",
"libraryName": "Nama dari pustaka Anda",
"libraryDesc": "Deskripsi pustaka Anda untuk membantu orang mengerti penggunaannya",
"githubHandle": "Akun GitHub (opsional), jadi Anda dapat mengubah pustaka ketika diserahkan untuk review",
"twitterHandle": "Nama pengguna Twitter (opsional), jadi kami tahu siapa dipuji ketika mempromosikannya melalui Twitter",
"website": "Hubungkan ke situs personal Anda atau lainnya (opsional)"
},
"errors": {
"required": "Dibutuhkan",
"website": "Masukkan URL valid"
},
"noteDescription": {
"pre": "Kirimkan pustaka Anda untuk disertakan di ",
"link": "repositori pustaka publik",
"post": "untuk orang lain menggunakannya dalam gambar mereka."
},
"noteGuidelines": {
"pre": "Pustaka butuh disetujui secara manual terlebih dahulu. Baca ",
"link": "pedoman",
"post": " sebelum mengirim. Anda butuh akun GitHub untuk berkomunikasi dan membuat perubahan jika dibutuhkan, tetapi tidak wajib dibutukan."
},
"noteLicense": {
"pre": "Dengan mengkirimkannya, Anda setuju pustaka akan diterbitkan dibawah ",
"link": "Lisensi MIT, ",
"post": "yang artinya siapa pun dapat menggunakannya tanpa batasan."
},
"noteItems": "Setiap item pustaka harus memiliki nama, sehingga bisa disortir. Item pustaka di bawah ini akan dimasukan:",
"atleastOneLibItem": "Pilih setidaknya satu item pustaka untuk mulai"
},
"publishSuccessDialog": {
"title": "Pustaka telah dikirm",
"content": "Terima kasih {{authorName}}. pustaka Anda telah diserahkan untuk ditinjau ulang. Anda dapat cek statusnya",
"link": "di sini"
},
"confirmDialog": {
"resetLibrary": "Reset pustaka",
"removeItemsFromLib": "Hapus item yang dipilih dari pustaka"
},
"encrypted": { "encrypted": {
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya.", "tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya.",
"link": "Pos blog tentang enkripsi ujung ke ujung di Excalidraw" "link": "Pos blog tentang enkripsi ujung ke ujung di Excalidraw"
@@ -345,7 +289,6 @@
"width": "Lebar" "width": "Lebar"
}, },
"toast": { "toast": {
"addedToLibrary": "Tambahkan ke pustaka",
"copyStyles": "Gaya tersalin.", "copyStyles": "Gaya tersalin.",
"copyToClipboard": "Tersalin ke papan klip.", "copyToClipboard": "Tersalin ke papan klip.",
"copyToClipboardAsPng": "Tersalin {{exportSelection}} ke clipboard sebagai PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Tersalin {{exportSelection}} ke clipboard sebagai PNG\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Condividi", "share": "Condividi",
"showStroke": "Mostra selettore colore del tratto", "showStroke": "Mostra selettore colore del tratto",
"showBackground": "Mostra selettore colore di sfondo", "showBackground": "Mostra selettore colore di sfondo",
"toggleTheme": "Cambia tema", "toggleTheme": "Cambia tema"
"personalLib": "Libreria Personale",
"excalidrawLib": "Libreria di Excalidraw"
}, },
"buttons": { "buttons": {
"clearReset": "Svuota la tela", "clearReset": "Svuota la tela",
@@ -137,11 +135,7 @@
"zenMode": "Modalità Zen", "zenMode": "Modalità Zen",
"exitZenMode": "Uscire dalla modalità zen", "exitZenMode": "Uscire dalla modalità zen",
"cancel": "Annulla", "cancel": "Annulla",
"clear": "Cancella", "clear": "Cancella"
"remove": "Rimuovi",
"publishLibrary": "Pubblica",
"submit": "Invia",
"confirm": "Conferma"
}, },
"alerts": { "alerts": {
"clearReset": "Questa azione cancellerà l'intera tela. Sei sicuro?", "clearReset": "Questa azione cancellerà l'intera tela. Sei sicuro?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Impossibile ripristinare la scena da questo file immagine", "cannotRestoreFromImage": "Impossibile ripristinare la scena da questo file immagine",
"invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.", "invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.",
"resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?", "resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?",
"removeItemsFromsLibrary": "Eliminare {{count}} elementi dalla libreria?",
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata." "invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"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": "Premere Elimina per rimuovere il punto, CtrlOrCmd+D per duplicare o trascinare per spostare", "lineEditor_pointSelected": "Premere Elimina per rimuovere il punto, CtrlOrCmd+D per duplicare o trascinare per spostare",
"lineEditor_nothingSelected": "Seleziona un punto per spostare o rimuovere, oppure tieni premuto Alt e fai clic per aggiungere nuovi punti", "lineEditor_nothingSelected": "Seleziona un punto per spostare o rimuovere, oppure 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"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Impossibile visualizzare l'anteprima", "cannotShowPreview": "Impossibile visualizzare l'anteprima",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Svuota la tela" "title": "Svuota la tela"
}, },
"publishDialog": {
"title": "Pubblica la libreria",
"itemName": "Nome dell'elemento",
"authorName": "Nome dell'autore",
"githubUsername": "Nome utente di GitHub",
"twitterUsername": "Nome utente di Twitter",
"libraryName": "Nome della libreria",
"libraryDesc": "Descrizione della libreria",
"website": "Sito Web",
"placeholder": {
"authorName": "Il tuo nome o nome utente",
"libraryName": "Nome della tua libreria",
"libraryDesc": "Descrizione della tua libreria per aiutare le persone a comprenderne lo scopo",
"githubHandle": "Handle di GitHub (opzionale), così che tu possa modificare la libreria una volta inviata per la revisione",
"twitterHandle": "Nome utente di Twitter (opzionale), così che sappiamo chi accreditare promuovendo su Twitter",
"website": "Link al tuo sito web personale o altro (opzionale)"
},
"errors": {
"required": "Obbligatorio",
"website": "Inserisci un URL valido"
},
"noteDescription": {
"pre": "Invia la tua libreria da includere nella ",
"link": "repository della libreria pubblica",
"post": "perché sia usata da altri nei loro disegni."
},
"noteGuidelines": {
"pre": "La libreria dev'esser prima approvata manualmente. Sei pregato di leggere le ",
"link": "linee guida",
"post": " prima di inviarla. Necessiterai di un profilo di GitHub per comunicare ed effettuare modifiche se richiesto, ma non è strettamente necessario."
},
"noteLicense": {
"pre": "Inviando, acconsenti che la libreria sarà pubblicata sotto la ",
"link": "Licenza MIT, ",
"post": "che in breve significa che chiunque possa usarla senza restrizioni."
},
"noteItems": "Ogni elemento della libreria deve avere il proprio nome, così che sia filtrabile. Gli elementi della seguente libreria saranno inclusi:",
"atleastOneLibItem": "Sei pregato di selezionare almeno un elemento della libreria per iniziare"
},
"publishSuccessDialog": {
"title": "Libreria inviata",
"content": "Grazie {{authorName}}. La tua libreria è stata inviata per la revisione. Puoi monitorarne lo stato",
"link": "qui"
},
"confirmDialog": {
"resetLibrary": "Ripristina la libreria",
"removeItemsFromLib": "Rimuovi gli elementi selezionati dalla libreria"
},
"encrypted": { "encrypted": {
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere.", "tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere.",
"link": "Articolo del blog sulla crittografia end-to-end di Excalidraw" "link": "Articolo del blog sulla crittografia end-to-end di Excalidraw"
@@ -345,7 +289,6 @@
"width": "Larghezza" "width": "Larghezza"
}, },
"toast": { "toast": {
"addedToLibrary": "Aggiunto alla libreria",
"copyStyles": "Stili copiati.", "copyStyles": "Stili copiati.",
"copyToClipboard": "Copiato negli appunti.", "copyToClipboard": "Copiato negli appunti.",
"copyToClipboardAsPng": "{{exportSelection}} copiato negli appunti come PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} copiato negli appunti come PNG\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "共有", "share": "共有",
"showStroke": "ストロークカラーピッカーを表示", "showStroke": "ストロークカラーピッカーを表示",
"showBackground": "背景色ピッカーを表示", "showBackground": "背景色ピッカーを表示",
"toggleTheme": "テーマの切り替え", "toggleTheme": "テーマの切り替え"
"personalLib": "個人ライブラリ",
"excalidrawLib": "Excalidrawライブラリ"
}, },
"buttons": { "buttons": {
"clearReset": "キャンバスのリセット", "clearReset": "キャンバスのリセット",
@@ -137,11 +135,7 @@
"zenMode": "Zenモード", "zenMode": "Zenモード",
"exitZenMode": "集中モードをやめる", "exitZenMode": "集中モードをやめる",
"cancel": "キャンセル", "cancel": "キャンセル",
"clear": "消去", "clear": "消去"
"remove": "削除",
"publishLibrary": "公開",
"submit": "送信",
"confirm": "確認"
}, },
"alerts": { "alerts": {
"clearReset": "この操作によってキャンバス全体が消えます。よろしいですか?", "clearReset": "この操作によってキャンバス全体が消えます。よろしいですか?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "このイメージファイルからシーンを復元できませんでした", "cannotRestoreFromImage": "このイメージファイルからシーンを復元できませんでした",
"invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。", "invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。",
"resetLibrary": "ライブラリを消去します。本当によろしいですか?", "resetLibrary": "ライブラリを消去します。本当によろしいですか?",
"removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?",
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。" "invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。"
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "ポイントを編集するには、ダブルクリックまたはEnterキーを押します", "lineEditor_info": "ポイントを編集するには、ダブルクリックまたはEnterキーを押します",
"lineEditor_pointSelected": "削除ボタンを押して点を削除します。Ctrl+D または Cmd+D で複製します。またはドラッグして移動します", "lineEditor_pointSelected": "削除ボタンを押して点を削除します。Ctrl+D または Cmd+D で複製します。またはドラッグして移動します",
"lineEditor_nothingSelected": "移動または削除する点を選択するか、Altキーを押しながらクリックして新しい点を追加します", "lineEditor_nothingSelected": "移動または削除する点を選択するか、Altキーを押しながらクリックして新しい点を追加します",
"placeImage": "クリックして画像を配置するか、クリックしてドラッグしてサイズを手動で設定します", "placeImage": "クリックして画像を配置するか、クリックしてドラッグしてサイズを手動で設定します"
"publishLibrary": "自分のライブラリを公開"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "プレビューを表示できません", "cannotShowPreview": "プレビューを表示できません",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "キャンバスを消去" "title": "キャンバスを消去"
}, },
"publishDialog": {
"title": "ライブラリを公開",
"itemName": "アイテム名",
"authorName": "作成者名",
"githubUsername": "GitHub ユーザ名",
"twitterUsername": "Twitter ユーザ名",
"libraryName": "ライブラリ名",
"libraryDesc": "ライブラリの説明",
"website": "Webサイト",
"placeholder": {
"authorName": "お名前またはユーザー名",
"libraryName": "あなたのライブラリ名",
"libraryDesc": "ライブラリの使い方を理解するための説明",
"githubHandle": "GitHubハンドル(任意)。一度レビューのために送信されると、ライブラリを編集できます",
"twitterHandle": "Twitterのユーザー名 (任意)。Twitterでプロモーションする際にクレジットする人を知っておくためのものです",
"website": "個人のウェブサイトまたは他のサイトへのリンク (オプション)"
},
"errors": {
"required": "必須項目",
"website": "有効な URL を入力してください"
},
"noteDescription": {
"pre": "以下に含めるライブラリを提出してください ",
"link": "公開ライブラリのリポジトリ",
"post": "他の人が作図に使えるようにするためです"
},
"noteGuidelines": {
"pre": "最初にライブラリを手動で承認する必要があります。次をお読みください ",
"link": "ガイドライン",
"post": " 送信する前に、GitHubアカウントが必要になりますが、必須ではありません。"
},
"noteLicense": {
"pre": "提出することにより、ライブラリが次の下で公開されることに同意します: ",
"link": "MIT ライセンス",
"post": "つまり誰でも制限なく使えるということです"
},
"noteItems": "",
"atleastOneLibItem": "開始するには少なくとも1つのライブラリ項目を選択してください"
},
"publishSuccessDialog": {
"title": "ライブラリを送信しました",
"content": "{{authorName}} さん、ありがとうございます。あなたのライブラリはレビューのために提出されました。状況を追跡できます。",
"link": "こちら"
},
"confirmDialog": {
"resetLibrary": "ライブラリをリセット",
"removeItemsFromLib": "選択したアイテムをライブラリから削除"
},
"encrypted": { "encrypted": {
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。", "tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。",
"link": "Excalidrawのエンドツーエンド暗号化に関するブログ記事" "link": "Excalidrawのエンドツーエンド暗号化に関するブログ記事"
@@ -345,7 +289,6 @@
"width": "幅" "width": "幅"
}, },
"toast": { "toast": {
"addedToLibrary": "ライブラリに追加しました",
"copyStyles": "スタイルをコピーしました。", "copyStyles": "スタイルをコピーしました。",
"copyToClipboard": "クリップボードにコピー", "copyToClipboard": "クリップボードにコピー",
"copyToClipboardAsPng": "{{exportSelection}} を PNG 形式でクリップボードにコピーしました\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} を PNG 形式でクリップボードにコピーしました\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Bḍu", "share": "Bḍu",
"showStroke": "Beqqeḍ amelqaḍ n yini n yizirig", "showStroke": "Beqqeḍ amelqaḍ n yini n yizirig",
"showBackground": "Beqqeḍ amelqaḍ n yini n ugilal", "showBackground": "Beqqeḍ amelqaḍ n yini n ugilal",
"toggleTheme": "Snifel asentel", "toggleTheme": "Snifel asentel"
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Ales awennez n teɣzut n usuneɣ", "clearReset": "Ales awennez n teɣzut n usuneɣ",
@@ -137,11 +135,7 @@
"zenMode": "Askar Zen", "zenMode": "Askar Zen",
"exitZenMode": "Ffeɣ seg uskar Zen", "exitZenMode": "Ffeɣ seg uskar Zen",
"cancel": "Sefsex", "cancel": "Sefsex",
"clear": "Sfeḍ", "clear": "Sfeḍ"
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "Ayagi ad isfeḍ akk taɣzut n usuneɣ. Tetḥeqqeḍ?", "clearReset": "Ayagi ad isfeḍ akk taɣzut n usuneɣ. Tetḥeqqeḍ?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Asayes ulamek ara d-yettwarr seg ufaylu-agi n tugna", "cannotRestoreFromImage": "Asayes ulamek ara d-yettwarr seg ufaylu-agi n tugna",
"invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.", "invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.",
"resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?", "resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa." "invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Ssit snat n tikkal neɣ ssed taqeffalt Kcem akken ad tẓergeḍ tinqiḍin", "lineEditor_info": "Ssit snat n tikkal neɣ ssed taqeffalt Kcem akken ad tẓergeḍ tinqiḍin",
"lineEditor_pointSelected": "Ssed taqeffalt kkes akken ad tekkseḍ tanqiḍt, CtrlOrCmd+D akken ad tsiselgeḍ, neɣ zuɣer akken ad tesmuttiḍ", "lineEditor_pointSelected": "Ssed taqeffalt kkes akken ad tekkseḍ tanqiḍt, CtrlOrCmd+D akken ad tsiselgeḍ, neɣ zuɣer akken ad tesmuttiḍ",
"lineEditor_nothingSelected": "Fren tanqiḍt ara tesmuttiḍ neɣ ara tekkseḍ, neɣ ṭṭef taqeffalt Alt akken ad ternuḍ tinqiḍin timaynutin", "lineEditor_nothingSelected": "Fren tanqiḍt ara tesmuttiḍ neɣ ara tekkseḍ, neɣ ṭṭef taqeffalt Alt akken ad ternuḍ tinqiḍin timaynutin",
"placeImage": "Ssit akken ad tserseḍ tugna, neɣ ssit u zuɣer akken ad tesbaduḍ tiddi-ines s ufus", "placeImage": "Ssit akken ad tserseḍ tugna, neɣ ssit u zuɣer akken ad tesbaduḍ tiddi-ines s ufus"
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Ulamek abeqqeḍ n teskant", "cannotShowPreview": "Ulamek abeqqeḍ n teskant",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "Unuɣen-inek (m) ttuwgelhnen seg yixef s ixef dɣa iqeddacen n Excalidraw werǧin ad ten-walin. ", "tooltip": "Unuɣen-inek (m) ttuwgelhnen seg yixef s ixef dɣa iqeddacen n Excalidraw werǧin ad ten-walin. ",
"link": "Amagrad ɣef uwgelhen ixef s ixef di Excalidraw" "link": "Amagrad ɣef uwgelhen ixef s ixef di Excalidraw"
@@ -345,7 +289,6 @@
"width": "Tehri" "width": "Tehri"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Iɣunab yettwaneɣlen.", "copyStyles": "Iɣunab yettwaneɣlen.",
"copyToClipboard": "Yettwaɣel ɣer tecfawit.", "copyToClipboard": "Yettwaɣel ɣer tecfawit.",
"copyToClipboardAsPng": "{{exportSelection}} yettwanɣel ɣer tecfawit am PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} yettwanɣel ɣer tecfawit am PNG\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": ""
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "", "clearReset": "",
@@ -137,11 +135,7 @@
"zenMode": "", "zenMode": "",
"exitZenMode": "", "exitZenMode": "",
"cancel": "", "cancel": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "", "clearReset": "",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "", "cannotRestoreFromImage": "",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "", "lineEditor_info": "",
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "", "cannotShowPreview": "",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "Сіздің сызбаларыңыз өтпелі шифрлеу арқылы шифрланған, сондықтан Excalidraw серверлері оларды ешқашан көрмейді.", "tooltip": "Сіздің сызбаларыңыз өтпелі шифрлеу арқылы шифрланған, сондықтан Excalidraw серверлері оларды ешқашан көрмейді.",
"link": "Excalidraw қолданатын өтпелі шифрлеу туралы блог жазбасы" "link": "Excalidraw қолданатын өтпелі шифрлеу туралы блог жазбасы"
@@ -345,7 +289,6 @@
"width": "Ені" "width": "Ені"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Стильдер көшірілді.", "copyStyles": "Стильдер көшірілді.",
"copyToClipboard": "", "copyToClipboard": "",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": ""
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "캔버스 초기화", "clearReset": "캔버스 초기화",
@@ -137,11 +135,7 @@
"zenMode": "젠 모드", "zenMode": "젠 모드",
"exitZenMode": "젠 모드 종료하기", "exitZenMode": "젠 모드 종료하기",
"cancel": "", "cancel": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "모든 작업 내용이 초기화됩니다. 계속하시겠습니까?", "clearReset": "모든 작업 내용이 초기화됩니다. 계속하시겠습니까?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "이미지 파일에서 화면을 복구할 수 없었습니다", "cannotRestoreFromImage": "이미지 파일에서 화면을 복구할 수 없었습니다",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "지점을 수정하려면 두 번 클릭하거나 Enter 키를 누르세요.", "lineEditor_info": "지점을 수정하려면 두 번 클릭하거나 Enter 키를 누르세요.",
"lineEditor_pointSelected": "제거하려면 Delete 키, 복제하려면 CtrlOrCmd+D, 이동하려면 드래그하세요.", "lineEditor_pointSelected": "제거하려면 Delete 키, 복제하려면 CtrlOrCmd+D, 이동하려면 드래그하세요.",
"lineEditor_nothingSelected": "옮기거나 지울 지점을 선택하거나, Alt를 누른 상태로 클릭해 새 지점을 만드세요", "lineEditor_nothingSelected": "옮기거나 지울 지점을 선택하거나, Alt를 누른 상태로 클릭해 새 지점을 만드세요",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "미리보기를 볼 수 없습니다", "cannotShowPreview": "미리보기를 볼 수 없습니다",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다.", "tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다.",
"link": "" "link": ""
@@ -345,7 +289,6 @@
"width": "너비" "width": "너비"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "스타일 복사.", "copyStyles": "스타일 복사.",
"copyToClipboard": "클립보드로 복사.", "copyToClipboard": "클립보드로 복사.",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+4 -61
View File
@@ -43,7 +43,7 @@
"exportEmbedScene": "Iegult ainu", "exportEmbedScene": "Iegult ainu",
"exportEmbedScene_details": "Ainas dati tiks iekļauti saglabātajā PNG/SVG datnē, lai no tās būtu iespējams ainu atgūt. Tas palielinās datnes izmēru.", "exportEmbedScene_details": "Ainas dati tiks iekļauti saglabātajā PNG/SVG datnē, lai no tās būtu iespējams ainu atgūt. Tas palielinās datnes izmēru.",
"addWatermark": "Pievienot \"Radīts ar Excalidraw\"", "addWatermark": "Pievienot \"Radīts ar Excalidraw\"",
"handDrawn": "Rokraksts", "handDrawn": "Ar roku zīmēts",
"normal": "Parasts", "normal": "Parasts",
"code": "Kods", "code": "Kods",
"small": "Mazs", "small": "Mazs",
@@ -100,9 +100,7 @@
"share": "Kopīgot", "share": "Kopīgot",
"showStroke": "Rādīt svītras krāsas atlasītāju", "showStroke": "Rādīt svītras krāsas atlasītāju",
"showBackground": "Rādīt fona krāsas atlasītāju", "showBackground": "Rādīt fona krāsas atlasītāju",
"toggleTheme": "Pārslēgt krāsu tēmu", "toggleTheme": "Pārslēgt krāsu tēmu"
"personalLib": "Personīgā bibliotēka",
"excalidrawLib": "Excalidraw bibliotēka"
}, },
"buttons": { "buttons": {
"clearReset": "Atiestatīt tāfeli", "clearReset": "Atiestatīt tāfeli",
@@ -137,11 +135,7 @@
"zenMode": "Zen režīms", "zenMode": "Zen režīms",
"exitZenMode": "Pamest Zen režīmu", "exitZenMode": "Pamest Zen režīmu",
"cancel": "Atcelt", "cancel": "Atcelt",
"clear": "Notīrīt", "clear": "Notīrīt"
"remove": "Noņemt",
"publishLibrary": "Publicēt",
"submit": "Iesniegt",
"confirm": "Apstiprināt"
}, },
"alerts": { "alerts": {
"clearReset": "Šī funkcija notīrīs visu tāfeli. Vai turpināt?", "clearReset": "Šī funkcija notīrīs visu tāfeli. Vai turpināt?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Ainu nevarēja atgūt no attēla faila", "cannotRestoreFromImage": "Ainu nevarēja atgūt no attēla faila",
"invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.", "invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.",
"resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?", "resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?",
"removeItemsFromsLibrary": "Vai izņemt {{count}} vienumu(s) no bibliotēkas?",
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta." "invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Dubultklikšķiniet vai spiediet ievades taustiņu, lai rediģētu punktus", "lineEditor_info": "Dubultklikšķiniet vai spiediet ievades taustiņu, lai rediģētu punktus",
"lineEditor_pointSelected": "Spiediet dzēšanas taustiņu, lai noņemtu punktu, CtrlOrCmd+D, lai to kopētu, vai velciet, lai pārvietotu", "lineEditor_pointSelected": "Spiediet dzēšanas taustiņu, lai noņemtu punktu, CtrlOrCmd+D, lai to kopētu, vai velciet, lai pārvietotu",
"lineEditor_nothingSelected": "Atlasiet punktu, lai to pārvietotu vai noņemtu; lai pievienotu jaunus punktus, turiet nospiestu Alt taustiņu", "lineEditor_nothingSelected": "Atlasiet punktu, lai to pārvietotu vai noņemtu; lai pievienotu jaunus punktus, turiet nospiestu Alt taustiņu",
"placeImage": "Klikšķiniet, lai novietotu attēlu, vai spiediet un velciet, lai iestatītu tā izmēru", "placeImage": "Klikšķiniet, lai novietotu attēlu, vai spiediet un velciet, lai iestatītu tā izmēru"
"publishLibrary": "Publicēt savu bibliotēku"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Nevar rādīt priekšskatījumu", "cannotShowPreview": "Nevar rādīt priekšskatījumu",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Notīrīt tāfeli" "title": "Notīrīt tāfeli"
}, },
"publishDialog": {
"title": "Publicēt bibliotēku",
"itemName": "Vienuma nosaukums",
"authorName": "Autora vārds",
"githubUsername": "GitHub lietotājvārds",
"twitterUsername": "Twitter lietotājvārds",
"libraryName": "Bibliotēkas nosaukums",
"libraryDesc": "Bibliotēkas apraksts",
"website": "Mājaslapa",
"placeholder": {
"authorName": "Jūsu vārds vai lietotājvārds",
"libraryName": "Jūsu bibliotēkas nosaukums",
"libraryDesc": "Bibliotēkas apraksts, kas palīdzēs citiem saprast tās pielietojumu",
"githubHandle": "GitHub lietotājvārds (neobligāts), lai jūs varētu rediģēt bibliotēku pēc tās iesniegšanas izskatīšanai",
"twitterHandle": "Twitter lietotājvārds (neobligāts), lai mēs varētu jūs pieminēt kā autoru, kad reklamēsim bibliotēku platformā Twitter",
"website": "Saikne uz jūsu personīgo mājaslapu vai kādu citu lapu (neobligāta)"
},
"errors": {
"required": "Obligāts",
"website": "Ievadiet derīgu URL"
},
"noteDescription": {
"pre": "Iesniegt savu bibliotēku iekļaušanai ",
"link": "publiskajā bibliotēku datubāzē",
"post": ", lai citi to varētu izmantot savos zīmējumos."
},
"noteGuidelines": {
"pre": "Šai bibliotēkai vispirms jātiek manuāli apstiprinātai. Lūdzu, izlasiet ",
"link": "norādījumus",
"post": " pirms iesniegšanas. Jums vajadzēs GitHub kontu, lai sazinātos un veiktu izmaiņas, ja tādas būs pieprasītas, bet tas nav absolūti nepieciešams."
},
"noteLicense": {
"pre": "Iesniedzot bibliotēku, jūs piekrītat tās publicēšanai saskaņā ar ",
"link": "MIT Licenci, ",
"post": "kas īsumā nozīmē, ka jebkurš to varēs izmantot bez ierobežojumiem."
},
"noteItems": "Katram bibliotēkas vienumam jābūt savam nosaukumam, lai to varētu atrast filtrējot. Tiks iekļauti sekojošie bibliotēkas vienumi:",
"atleastOneLibItem": "Lūdzu, atlasiet vismaz vienu bibliotēkas vienumu, lai sāktu darbu"
},
"publishSuccessDialog": {
"title": "Bibliotēka iesniegta",
"content": "Paldies, {{authorName}}! Jūsu bibliotēka iesniegta izskatīšanai. Jūs varat izsekot iesnieguma statusam",
"link": "šeit"
},
"confirmDialog": {
"resetLibrary": "Atiestatīt bibliotēku",
"removeItemsFromLib": "Noņemt atlasītos vienumus no bibliotēkas"
},
"encrypted": { "encrypted": {
"tooltip": "Jūsu zīmējumi ir šifrēti no gala līdz galam; līdz ar to Excalidraw serveri tos nekad neredzēs.", "tooltip": "Jūsu zīmējumi ir šifrēti no gala līdz galam; līdz ar to Excalidraw serveri tos nekad neredzēs.",
"link": "Ieraksts par šifrēšanu no gala līdz galam Excalidraw blogā" "link": "Ieraksts par šifrēšanu no gala līdz galam Excalidraw blogā"
@@ -345,7 +289,6 @@
"width": "Platums" "width": "Platums"
}, },
"toast": { "toast": {
"addedToLibrary": "Pievienots bibliotēkai",
"copyStyles": "Nokopēja stilus.", "copyStyles": "Nokopēja stilus.",
"copyToClipboard": "Nokopēja starpliktuvē.", "copyToClipboard": "Nokopēja starpliktuvē.",
"copyToClipboardAsPng": "Nokopēja {{exportSelection}} starpliktuvē kā PNG ({{exportColorScheme}})", "copyToClipboardAsPng": "Nokopēja {{exportSelection}} starpliktuvē kā PNG ({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": ""
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "ကားချပ်ရှင်းလင်း", "clearReset": "ကားချပ်ရှင်းလင်း",
@@ -137,11 +135,7 @@
"zenMode": "", "zenMode": "",
"exitZenMode": "ဇင်မြင်ကွင်းမှထွက်", "exitZenMode": "ဇင်မြင်ကွင်းမှထွက်",
"cancel": "", "cancel": "",
"clear": "", "clear": ""
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "ကားချပ်တစ်ခုလုံးရှင်းလင်းပါတော့မည်။ အတည်ပြုပါ။", "clearReset": "ကားချပ်တစ်ခုလုံးရှင်းလင်းပါတော့မည်။ အတည်ပြုပါ။",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "ဤပုံဖြင့်မြင်ကွင်းပြန်လည်မရယူနိုင်ပါ။", "cannotRestoreFromImage": "ဤပုံဖြင့်မြင်ကွင်းပြန်လည်မရယူနိုင်ပါ။",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "အမှတ်များပြင်ဆင်သတ်မှတ်ရင် ကလစ်နှစ်ချက် (သို့) Enter ကိုနှိပ်ပါ", "lineEditor_info": "အမှတ်များပြင်ဆင်သတ်မှတ်ရင် ကလစ်နှစ်ချက် (သို့) Enter ကိုနှိပ်ပါ",
"lineEditor_pointSelected": "အမှတ်များအား ဖျက်ရန် Delete နှင့် ပွားရန် Ctrl/Cmd + D သုံးပါ၊ ရွှေ့လိုပါက တရွတ်ဆွဲပါ", "lineEditor_pointSelected": "အမှတ်များအား ဖျက်ရန် Delete နှင့် ပွားရန် Ctrl/Cmd + D သုံးပါ၊ ရွှေ့လိုပါက တရွတ်ဆွဲပါ",
"lineEditor_nothingSelected": "ရွှေ့လို (သို့) ဖယ်ရှားလိုသောအမှတ်ကိုရွေးပါ၊ Alt နှင့် ကလစ်တွဲနှိပ်၍လည်းအမှတ်အသစ်ထပ်ထည့်နိုင်သည်", "lineEditor_nothingSelected": "ရွှေ့လို (သို့) ဖယ်ရှားလိုသောအမှတ်ကိုရွေးပါ၊ Alt နှင့် ကလစ်တွဲနှိပ်၍လည်းအမှတ်အသစ်ထပ်ထည့်နိုင်သည်",
"placeImage": "", "placeImage": ""
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "နမူနာမပြသနိုင်ပါ", "cannotShowPreview": "နမူနာမပြသနိုင်ပါ",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။", "tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။",
"link": "" "link": ""
@@ -345,7 +289,6 @@
"width": "အကျယ်" "width": "အကျယ်"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "", "copyStyles": "",
"copyToClipboard": "", "copyToClipboard": "",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Del", "share": "Del",
"showStroke": "Vis fargevelger for kantfarge", "showStroke": "Vis fargevelger for kantfarge",
"showBackground": "Vis fargevelger for bakgrunnsfarge", "showBackground": "Vis fargevelger for bakgrunnsfarge",
"toggleTheme": "Veksle tema", "toggleTheme": "Veksle tema"
"personalLib": "Personlig bibliotek",
"excalidrawLib": "Excalidraw-bibliotek"
}, },
"buttons": { "buttons": {
"clearReset": "Tøm lerretet og tilbakestill bakgrunnsfargen", "clearReset": "Tøm lerretet og tilbakestill bakgrunnsfargen",
@@ -137,11 +135,7 @@
"zenMode": "Zen-modus", "zenMode": "Zen-modus",
"exitZenMode": "Avslutt zen-modus", "exitZenMode": "Avslutt zen-modus",
"cancel": "Avbryt", "cancel": "Avbryt",
"clear": "Tøm", "clear": "Tøm"
"remove": "Fjern",
"publishLibrary": "Publiser",
"submit": "Send inn",
"confirm": "Bekreft"
}, },
"alerts": { "alerts": {
"clearReset": "Dette vil tømme lerretet. Er du sikker?", "clearReset": "Dette vil tømme lerretet. Er du sikker?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Scenen kunne ikke gjenopprettes fra denne bildefilen", "cannotRestoreFromImage": "Scenen kunne ikke gjenopprettes fra denne bildefilen",
"invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.", "invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.",
"resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?", "resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?",
"removeItemsFromsLibrary": "Slett {{count}} element(er) fra biblioteket?",
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert." "invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Dobbeltklikk eller trykk Enter for å redigere punkter", "lineEditor_info": "Dobbeltklikk eller trykk Enter for å redigere punkter",
"lineEditor_pointSelected": "Trykk på Slett for å fjerne punktet, Ctrl / Cmd+D for å duplisere, eller dra for å flytte", "lineEditor_pointSelected": "Trykk på Slett for å fjerne punktet, Ctrl / Cmd+D for å duplisere, eller dra for å flytte",
"lineEditor_nothingSelected": "Velg et punkt å flytte eller fjerne, eller hold Alt og klikk for å legge til nye punkter", "lineEditor_nothingSelected": "Velg et punkt å flytte eller fjerne, eller hold Alt og klikk for å legge til nye punkter",
"placeImage": "Klikk for å plassere bildet, eller klikk og dra for å angi størrelsen manuelt", "placeImage": "Klikk for å plassere bildet, eller klikk og dra for å angi størrelsen manuelt"
"publishLibrary": "Publiser ditt eget bibliotek"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Kan ikke vise forhåndsvisning", "cannotShowPreview": "Kan ikke vise forhåndsvisning",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Tøm lerret" "title": "Tøm lerret"
}, },
"publishDialog": {
"title": "Publiser bibliotek",
"itemName": "Elementnavn",
"authorName": "Forfatterens navn",
"githubUsername": "GitHub-brukernavnet",
"twitterUsername": "Twitter-brukernavn",
"libraryName": "Biblioteknavn",
"libraryDesc": "Beskrivelse av bibliotek",
"website": "Nettsted",
"placeholder": {
"authorName": "Ditt navn eller brukernavn",
"libraryName": "Navnet på biblioteket ditt",
"libraryDesc": "Beskrivelse av biblioteket ditt for å hjelpe folk med å forstå bruken",
"githubHandle": "Github-brukernavn (valgfritt), slik at du kan redigere biblioteket når du har sendt inn for gjennomgang",
"twitterHandle": "Twitter-brukernavn (valgfritt), slik at vi vet hvem vi skal kreditere når promotert på Twitter",
"website": "Lenke til din personlige nettside eller et annet sted (valgfritt)"
},
"errors": {
"required": "Påkrevd",
"website": "Angi en gyldig nettadresse"
},
"noteDescription": {
"pre": "Send inn biblioteket ditt som skal inkluderes i ",
"link": "kildekode for offentlig bibliotek",
"post": "for andre å bruke dem i tegninger."
},
"noteGuidelines": {
"pre": "Biblioteket må godkjennes manuelt først. Les ",
"link": "retningslinjene",
"post": " før innsending. Du vil trenge en GitHub-konto for å kommunisere og gjøre endringer hvis ønsket, men det er ikke påkrevd."
},
"noteLicense": {
"pre": "Ved å sende inn godtar du at biblioteket blir publisert under ",
"link": "MIT-lisens, ",
"post": "som kortfattet betyr at andre kan bruke dem uten begrensninger."
},
"noteItems": "Hvert bibliotek må ha sitt eget navn, så det er filtrerbart. Følgende bibliotekselementer vil bli inkludert:",
"atleastOneLibItem": "Vennligst velg minst ett bibliotek for å komme i gang"
},
"publishSuccessDialog": {
"title": "Bibliotek innsendt",
"content": "Takk {{authorName}}. Ditt bibliotek har blitt sendt inn for gjennomgang. Du kan spore statusen",
"link": "her"
},
"confirmDialog": {
"resetLibrary": "Nullstill bibliotek",
"removeItemsFromLib": "Fjern valgte elementer fra bibliotek"
},
"encrypted": { "encrypted": {
"tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem.", "tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem.",
"link": "Blogginnlegg om ende-til-ende-kryptering i Excalidraw" "link": "Blogginnlegg om ende-til-ende-kryptering i Excalidraw"
@@ -345,7 +289,6 @@
"width": "Bredde" "width": "Bredde"
}, },
"toast": { "toast": {
"addedToLibrary": "Lagt til i biblioteket",
"copyStyles": "Kopierte stiler.", "copyStyles": "Kopierte stiler.",
"copyToClipboard": "Kopiert til utklippstavlen.", "copyToClipboard": "Kopiert til utklippstavlen.",
"copyToClipboardAsPng": "Kopierte {{exportSelection}} til utklippstavlen som PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Kopierte {{exportSelection}} til utklippstavlen som PNG\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Deel", "share": "Deel",
"showStroke": "Toon lijn kleur kiezer", "showStroke": "Toon lijn kleur kiezer",
"showBackground": "Toon achtergrondkleur kiezer", "showBackground": "Toon achtergrondkleur kiezer",
"toggleTheme": "Thema aan/uit", "toggleTheme": "Thema aan/uit"
"personalLib": "Persoonlijke bibliotheek",
"excalidrawLib": "Excalidraw bibliotheek"
}, },
"buttons": { "buttons": {
"clearReset": "Canvas opnieuw instellen", "clearReset": "Canvas opnieuw instellen",
@@ -137,11 +135,7 @@
"zenMode": "Zen modus", "zenMode": "Zen modus",
"exitZenMode": "Verlaat zen modus", "exitZenMode": "Verlaat zen modus",
"cancel": "Annuleren", "cancel": "Annuleren",
"clear": "Wissen", "clear": "Wissen"
"remove": "Verwijderen",
"publishLibrary": "Publiceren",
"submit": "Versturen",
"confirm": "Bevestigen"
}, },
"alerts": { "alerts": {
"clearReset": "Dit zal het hele canvas verwijderen. Weet je het zeker?", "clearReset": "Dit zal het hele canvas verwijderen. Weet je het zeker?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Scène kan niet worden hersteld vanuit dit afbeeldingsbestand", "cannotRestoreFromImage": "Scène kan niet worden hersteld vanuit dit afbeeldingsbestand",
"invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.", "invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.",
"resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?", "resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?",
"removeItemsFromsLibrary": "Verwijder {{count}} item(s) uit bibliotheek?",
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld." "invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Dubbelklik of druk op Enter om punten te bewerken", "lineEditor_info": "Dubbelklik of druk op Enter om punten te bewerken",
"lineEditor_pointSelected": "Druk op Delete om een punt te verwijderen, op CtrlOrCmd+D om te kopiëren, of sleeg om te verplaatsen", "lineEditor_pointSelected": "Druk op Delete om een punt te verwijderen, op CtrlOrCmd+D om te kopiëren, of sleeg om te verplaatsen",
"lineEditor_nothingSelected": "Selecteer een punt om te verplaatsen of te verwijderen, of houd Alt ingedrukt en klik om nieuwe punten toe te voegen", "lineEditor_nothingSelected": "Selecteer een punt om te verplaatsen of te verwijderen, of houd Alt ingedrukt en klik om nieuwe punten toe te voegen",
"placeImage": "", "placeImage": ""
"publishLibrary": "Publiceer je eigen bibliotheek"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Kan voorbeeld niet tonen", "cannotShowPreview": "Kan voorbeeld niet tonen",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "Publiceer bibliotheek",
"itemName": "Itemnaam",
"authorName": "Naam auteur",
"githubUsername": "GitHub gebruikersnaam",
"twitterUsername": "Twitter gebruikersnaam",
"libraryName": "Bibliotheek naam",
"libraryDesc": "Bibliotheek beschrijving",
"website": "Website",
"placeholder": {
"authorName": "Uw naam of gebruikersnaam:",
"libraryName": "Naam van je bibliotheek",
"libraryDesc": "Beschrijving van uw bibliotheek om mensen te helpen het gebruik ervan te begrijpen",
"githubHandle": "",
"twitterHandle": "",
"website": "Link naar je persoonlijke website of elders (optioneel)"
},
"errors": {
"required": "Vereist",
"website": "Vul een geldige URL in"
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": "Hier"
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent.", "tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent.",
"link": "Blog post over end-to-end versleuteling in Excalidraw" "link": "Blog post over end-to-end versleuteling in Excalidraw"
@@ -345,7 +289,6 @@
"width": "Breedte" "width": "Breedte"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Stijlen gekopieerd.", "copyStyles": "Stijlen gekopieerd.",
"copyToClipboard": "Gekopieerd naar het klembord.", "copyToClipboard": "Gekopieerd naar het klembord.",
"copyToClipboardAsPng": "{{exportSelection}} naar klembord gekopieerd als PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} naar klembord gekopieerd als PNG\n({{exportColorScheme}})",
+3 -60
View File
@@ -100,9 +100,7 @@
"share": "Del", "share": "Del",
"showStroke": "Vis fargeveljar for linjer", "showStroke": "Vis fargeveljar for linjer",
"showBackground": "Vis fargeveljar for bakgrunn", "showBackground": "Vis fargeveljar for bakgrunn",
"toggleTheme": "Veksle tema", "toggleTheme": "Veksle tema"
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Tilbakestill lerretet", "clearReset": "Tilbakestill lerretet",
@@ -137,11 +135,7 @@
"zenMode": "Zen-modus", "zenMode": "Zen-modus",
"exitZenMode": "Avslutt zen-modus", "exitZenMode": "Avslutt zen-modus",
"cancel": "Avbryt", "cancel": "Avbryt",
"clear": "Tøm", "clear": "Tøm"
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "Dette vil tømme lerretet. Er du sikker?", "clearReset": "Dette vil tømme lerretet. Er du sikker?",
@@ -163,7 +157,6 @@
"cannotRestoreFromImage": "Scena kunne ikkje gjenopprettast frå denne biletfila", "cannotRestoreFromImage": "Scena kunne ikkje gjenopprettast frå denne biletfila",
"invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.", "invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.",
"resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?", "resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert." "invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert."
}, },
"errors": { "errors": {
@@ -206,8 +199,7 @@
"lineEditor_info": "Dobbeltklikk eller trykk Enter for å redigere punkt", "lineEditor_info": "Dobbeltklikk eller trykk Enter for å redigere punkt",
"lineEditor_pointSelected": "Trykk på Slett for å fjerne punktet, CtrlOrCmd+D for å duplisere, eller dra for å flytte", "lineEditor_pointSelected": "Trykk på Slett for å fjerne punktet, CtrlOrCmd+D for å duplisere, eller dra for å flytte",
"lineEditor_nothingSelected": "Vel eit punkt å flytte eller fjerne, eller hald Alt og klikk for å legge til nye punkt", "lineEditor_nothingSelected": "Vel eit punkt å flytte eller fjerne, eller hald Alt og klikk for å legge til nye punkt",
"placeImage": "Klikk for å plassere biletet, eller klikk og drag for å velje storleik manuelt", "placeImage": "Klikk for å plassere biletet, eller klikk og drag for å velje storleik manuelt"
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Kan ikkje vise førehandsvising", "cannotShowPreview": "Kan ikkje vise førehandsvising",
@@ -277,54 +269,6 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "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": { "encrypted": {
"tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei.", "tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei.",
"link": "Blogginnlegg om ende-til-ende-kryptering i Excalidraw" "link": "Blogginnlegg om ende-til-ende-kryptering i Excalidraw"
@@ -345,7 +289,6 @@
"width": "Breidde" "width": "Breidde"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Kopierte stilane.", "copyStyles": "Kopierte stilane.",
"copyToClipboard": "Kopiert til utklippstavla.", "copyToClipboard": "Kopiert til utklippstavla.",
"copyToClipboardAsPng": "Kopierte {{exportSelection}} til utklippstavla som PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Kopierte {{exportSelection}} til utklippstavla som PNG\n({{exportColorScheme}})",

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