Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75e2d9e359 | |||
| 6592517122 | |||
| bd953a6287 |
@@ -1,2 +1 @@
|
|||||||
#!/bin/sh
|
|
||||||
yarn lint-staged
|
yarn lint-staged
|
||||||
|
|||||||
+10
-11
@@ -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": {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,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({
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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])) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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`}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 > * {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Vendored
+4
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge",
|
"cannotRestoreFromImage": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge",
|
||||||
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
|
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
|
||||||
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
|
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
|
||||||
"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 d’Excalidraw no els veuran mai.",
|
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors d’Excalidraw 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 d’afficher l’aperçu",
|
"cannotShowPreview": "Impossible d’afficher l’aperç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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}})",
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user