Compare commits

..

33 Commits

Author SHA1 Message Date
Ryan Di 692802f8a6 fix: handle missing schemaState in migration 2026-03-25 20:10:43 +11:00
Ryan Di 91981159d2 feat: wire schema migration plugins end-to-end 2026-03-25 19:19:13 +11:00
Ryan Di b871d4ceb3 test: update coverage and snapshots for per-track schemaState 2026-03-25 18:04:22 +11:00
Ryan Di 16cf593978 refactor: replace global schema version with per-track migration 2026-03-25 18:04:13 +11:00
Ryan Di 1d35cb406b refactor: switch elements to per-track schemaState 2026-03-25 18:04:04 +11:00
Ryan Di 205d90592a test: refresh snapshots for schema changes 2026-03-23 17:30:23 +11:00
Ryan Di 08af0964f2 test: cover element schema migration flow 2026-03-23 17:30:16 +11:00
Ryan Di 0e4ae079ac refactor: remove payload-level schema version 2026-03-23 17:30:11 +11:00
Ryan Di 11ba6784aa refactor: migrate elements by schema in restore 2026-03-23 17:30:05 +11:00
Ryan Di 16838ea792 test: cover collab field preservation 2026-03-20 19:55:40 +11:00
Ryan Di 8168d46f87 chore: harden local schema hints and tests 2026-03-20 19:33:01 +11:00
Ryan Di eda7c8d6e9 fix: keep schema hints across clipboard roundtrips 2026-03-20 19:00:54 +11:00
Ryan Di b818b3fe04 chore: keep schema migration rules in code 2026-03-20 18:17:00 +11:00
Ryan Di 67967c05fa refactor: add schema migration helper APIs 2026-03-20 18:15:03 +11:00
Ryan Di 6c9914049e docs: jot down schema migration rules 2026-03-20 18:03:43 +11:00
Ryan Di 1e113e4a3b fix: wire schema scopes at import boundaries 2026-03-20 18:03:36 +11:00
Ryan Di ec458d92e3 chore: set up scoped schema migrations 2026-03-20 18:03:22 +11:00
Ryan Di b3b9b26979 fix: smooth out schema-version edges 2026-03-20 14:03:22 +11:00
Ryan Di 21d26b1afe schema: add legacy frame edge test and refresh snapshots 2026-03-20 01:11:05 +11:00
Ryan Di 6d3eb16531 schema: remove frame bg guard flag from runtime 2026-03-20 01:06:00 +11:00
Ryan Di 5bb3046dea schema: run v1->v2 frame migration on load/paste 2026-03-20 00:57:50 +11:00
Ryan Di 7b51a6ac54 schema: add payload schemaVersion plumbing 2026-03-20 00:26:08 +11:00
Ryan Di d1cbab855d pass full bg payload in actionProperties 2026-03-18 20:08:56 +11:00
Ryan Di 40158fa0a0 update test snapshots 2026-03-18 20:02:58 +11:00
Ryan Di d1144b4779 frame bg: normalize false flag to transparent 2026-03-18 19:59:17 +11:00
Ryan Di c4925dc5b9 frame bg: treat non-boolean legacy flag as missing 2026-03-18 19:55:52 +11:00
Ryan Di 48bc930c09 frame bg: drop legacy color if flag missing 2026-03-18 19:53:24 +11:00
Ryan Di 858d1d4cce frame bg: make legacy scenes opt-in 2026-03-18 19:49:46 +11:00
Ryan Di 3f405ab833 fix: align svg frame bg with canvas 2026-03-18 19:41:14 +11:00
Ryan Di 8d003a1d21 perf: tidy svg frame bg render pass 2026-03-18 19:38:53 +11:00
Ryan Di 2b3871856e perf: tidy frame bg render pass 2026-03-18 19:36:41 +11:00
Ryan Di a7281de157 fix: skip unsupported style updates 2026-03-18 19:22:48 +11:00
dwelle f944f1f7aa feat(editor): support frame backgrounds 2026-02-22 23:08:46 +01:00
150 changed files with 7006 additions and 6761 deletions
+6 -2
View File
@@ -266,7 +266,7 @@ const initializeScene = async (opts: {
repairBindings: true, repairBindings: true,
deleteInvisibleElements: true, deleteInvisibleElements: true,
}), }),
localDataState?.elements, scene.elements,
), ),
appState: restoreAppState( appState: restoreAppState(
imported.appState, imported.appState,
@@ -551,7 +551,11 @@ const ExcalidrawWrapper = () => {
const username = importUsernameFromLocalStorage(); const username = importUsernameFromLocalStorage();
setLangCode(getPreferredLanguage()); setLangCode(getPreferredLanguage());
excalidrawAPI.updateScene({ excalidrawAPI.updateScene({
...localDataState, elements: restoreElements(localDataState?.elements, null, {
repairBindings: true,
deleteInvisibleElements: true,
}),
appState: restoreAppState(localDataState?.appState, null),
captureUpdate: CaptureUpdateAction.NEVER, captureUpdate: CaptureUpdateAction.NEVER,
}); });
LibraryIndexedDBAdapter.load().then((data) => { LibraryIndexedDBAdapter.load().then((data) => {
+3 -1
View File
@@ -86,9 +86,11 @@ const saveDataStateToLocalStorage = (
_appState.openSidebar = null; _appState.openSidebar = null;
} }
const persistedElements = getNonDeletedElements(elements);
localStorage.setItem( localStorage.setItem(
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS, STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
JSON.stringify(getNonDeletedElements(elements)), JSON.stringify(persistedElements),
); );
localStorage.setItem( localStorage.setItem(
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE, STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
@@ -1,3 +1,9 @@
declare global {
interface Window {
debug: typeof Debug;
}
}
const lessPrecise = (num: number, precision = 5) => const lessPrecise = (num: number, precision = 5) =>
parseFloat(num.toPrecision(precision)); parseFloat(num.toPrecision(precision));
@@ -151,70 +157,6 @@ export class Debug {
return ret; return ret;
}; };
}; };
private static CHANGED_CACHE: Record<string, Record<string, unknown>> = {};
public static logChanged(name: string, obj: Record<string, unknown>) {
const prev = Debug.CHANGED_CACHE[name];
Debug.CHANGED_CACHE[name] = obj;
if (!prev) {
return;
}
const allKeys = new Set([...Object.keys(prev), ...Object.keys(obj)]);
const changed: Record<string, { prev: unknown; next: unknown }> = {};
for (const key of allKeys) {
const prevVal = prev[key];
const nextVal = obj[key];
if (!deepEqual(prevVal, nextVal)) {
changed[key] = { prev: prevVal, next: nextVal };
}
}
if (Object.keys(changed).length > 0) {
console.info(`[${name}] changed:`, changed);
}
}
}
function deepEqual(a: unknown, b: unknown): boolean {
if (Object.is(a, b)) {
return true;
}
if (
a === null ||
b === null ||
typeof a !== "object" ||
typeof b !== "object"
) {
return false;
}
if (Array.isArray(a) !== Array.isArray(b)) {
return false;
}
const keysA = Object.keys(a as Record<string, unknown>);
const keysB = Object.keys(b as Record<string, unknown>);
if (keysA.length !== keysB.length) {
return false;
}
for (const key of keysA) {
if (
!deepEqual(
(a as Record<string, unknown>)[key],
(b as Record<string, unknown>)[key],
)
) {
return false;
}
}
return true;
} }
//@ts-ignore
window.debug = Debug;
+125 -14
View File
@@ -69,6 +69,114 @@ vi.mock("socket.io-client", () => {
* i.e. multiplayer history tests could be a good first candidate, as we could test both history stacks simultaneously. * i.e. multiplayer history tests could be a good first candidate, as we could test both history stacks simultaneously.
*/ */
describe("collaboration", () => { describe("collaboration", () => {
it("should preserve future element fields across collab reconciliation", async () => {
await render(<ExcalidrawApp />);
const frame = API.createElement({
type: "frame",
id: "A",
width: 100,
height: 100,
x: 0,
y: 0,
backgroundColor: "#ff0000",
});
const frameWithFutureFields = {
...frame,
schemaState: {
tracks: {
...frame.schemaState.tracks,
"host.myapp.frame": 1,
},
},
futureField: "keep-me",
} as typeof frame & {
schemaState: { tracks: Record<string, number> };
futureField: string;
};
API.updateScene({
elements: [frameWithFutureFields],
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
});
await waitFor(() => {
expect((h.elements[0] as any).futureField).toBe("keep-me");
expect((h.elements[0] as any).schemaState).toEqual(
frameWithFutureFields.schemaState,
);
expect(h.elements[0].backgroundColor).toBe("#ff0000");
});
const remoteMovedFrame = newElementWith(h.elements[0] as any, {
x: 120,
y: 80,
});
const reconciled = (window.collab as any)._reconcileElements([
remoteMovedFrame,
]);
expect(reconciled[0]).toEqual(
expect.objectContaining({
x: 120,
y: 80,
backgroundColor: "#ff0000",
}),
);
expect((reconciled[0] as any).futureField).toBe("keep-me");
expect((reconciled[0] as any).schemaState).toEqual(
frameWithFutureFields.schemaState,
);
});
it("should preserve future element fields on local edits before broadcast", async () => {
await render(<ExcalidrawApp />);
const rect = API.createElement({
type: "rectangle",
id: "A",
width: 100,
height: 100,
x: 0,
y: 0,
});
const rectWithFutureFields = {
...rect,
schemaState: {
tracks: {
...rect.schemaState.tracks,
"host.myapp.rect": 1,
},
},
futureField: { value: "keep-me" },
} as typeof rect & {
schemaState: { tracks: Record<string, number> };
futureField: { value: string };
};
API.updateScene({
elements: [rectWithFutureFields],
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
});
const locallyEdited = newElementWith(h.elements[0] as any, { x: 200 });
API.updateScene({
elements: [locallyEdited],
captureUpdate: CaptureUpdateAction.NEVER,
});
await waitFor(() => {
expect((h.elements[0] as any).futureField).toEqual({ value: "keep-me" });
expect((h.elements[0] as any).schemaState).toEqual(
rectWithFutureFields.schemaState,
);
expect(h.elements[0]).toEqual(expect.objectContaining({ x: 200 }));
});
});
it("should emit two ephemeral increments even though updates get batched", async () => { it("should emit two ephemeral increments even though updates get batched", async () => {
const durableIncrements: DurableIncrement[] = []; const durableIncrements: DurableIncrement[] = [];
const ephemeralIncrements: EphemeralIncrement[] = []; const ephemeralIncrements: EphemeralIncrement[] = [];
@@ -83,14 +191,18 @@ describe("collaboration", () => {
} }
}); });
// eslint-disable-next-line dot-notation // Ensure this test starts from a deterministic scene regardless of previous
expect(h.store["scheduledMicroActions"].length).toBe(0); // test state restored from persistence.
expect(durableIncrements.length).toBe(0); API.updateScene({
expect(ephemeralIncrements.length).toBe(0); elements: [],
captureUpdate: CaptureUpdateAction.NEVER,
});
const durableBaseline = durableIncrements.length;
const ephemeralBaseline = ephemeralIncrements.length;
const rectProps = { const rectProps = {
type: "rectangle", type: "rectangle",
id: "A",
height: 200, height: 200,
width: 100, width: 100,
x: 0, x: 0,
@@ -105,8 +217,7 @@ describe("collaboration", () => {
}); });
await waitFor(() => { await waitFor(() => {
// expect(commitSpy).toHaveBeenCalledTimes(1); expect(durableIncrements.length).toBe(durableBaseline + 1);
expect(durableIncrements.length).toBe(1);
}); });
// simulate two batched remote updates // simulate two batched remote updates
@@ -130,13 +241,13 @@ describe("collaboration", () => {
// altough the updates get batched, // altough the updates get batched,
// we expect two ephemeral increments for each update, // we expect two ephemeral increments for each update,
// and each such update should have the expected change // and each such update should have the expected change
expect(ephemeralIncrements.length).toBe(2); expect(ephemeralIncrements.length).toBe(ephemeralBaseline + 2);
expect(ephemeralIncrements[0].change.elements.A).toEqual( expect(
expect.objectContaining({ x: 100 }), ephemeralIncrements[ephemeralBaseline].change.elements[rect.id],
); ).toEqual(expect.objectContaining({ x: 100 }));
expect(ephemeralIncrements[1].change.elements.A).toEqual( expect(
expect.objectContaining({ x: 200 }), ephemeralIncrements[ephemeralBaseline + 1].change.elements[rect.id],
); ).toEqual(expect.objectContaining({ x: 200 }));
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
expect(h.store["scheduledMicroActions"].length).toBe(0); expect(h.store["scheduledMicroActions"].length).toBe(0);
}); });
+15 -14
View File
@@ -240,21 +240,22 @@ export const DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE = {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// !!!MUST BE WITHOUT GRAY, TRANSPARENT AND BLACK!!! // !!!MUST BE WITHOUT GRAY, TRANSPARENT AND BLACK!!!
export const getAllColorsSpecificShade = (index: 0 | 1 | 2 | 3 | 4) => [ export const getAllColorsSpecificShade = (index: 0 | 1 | 2 | 3 | 4) =>
// 2nd row [
COLOR_PALETTE.cyan[index], // 2nd row
COLOR_PALETTE.blue[index], COLOR_PALETTE.cyan[index],
COLOR_PALETTE.violet[index], COLOR_PALETTE.blue[index],
COLOR_PALETTE.grape[index], COLOR_PALETTE.violet[index],
COLOR_PALETTE.pink[index], COLOR_PALETTE.grape[index],
COLOR_PALETTE.pink[index],
// 3rd row // 3rd row
COLOR_PALETTE.green[index], COLOR_PALETTE.green[index],
COLOR_PALETTE.teal[index], COLOR_PALETTE.teal[index],
COLOR_PALETTE.yellow[index], COLOR_PALETTE.yellow[index],
COLOR_PALETTE.orange[index], COLOR_PALETTE.orange[index],
COLOR_PALETTE.red[index], COLOR_PALETTE.red[index],
]; ] as const;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// other helpers // other helpers
-1
View File
@@ -12,4 +12,3 @@ export * from "./url";
export * from "./utils"; export * from "./utils";
export * from "./emitter"; export * from "./emitter";
export * from "./editorInterface"; export * from "./editorInterface";
export { Debug } from "../debug";
+1 -10
View File
@@ -1,7 +1,5 @@
import { average } from "@excalidraw/math"; import { average } from "@excalidraw/math";
import type { GlobalCoord } from "@excalidraw/math";
import type { FontFamilyValues, FontString } from "@excalidraw/element/types"; import type { FontFamilyValues, FontString } from "@excalidraw/element/types";
import type { import type {
@@ -443,7 +441,7 @@ export const viewportCoordsToSceneCoords = (
const x = (clientX - offsetLeft) / zoom.value - scrollX; const x = (clientX - offsetLeft) / zoom.value - scrollX;
const y = (clientY - offsetTop) / zoom.value - scrollY; const y = (clientY - offsetTop) / zoom.value - scrollY;
return { x, y } as GlobalCoord; return { x, y };
}; };
export const sceneCoordsToViewportCoords = ( export const sceneCoordsToViewportCoords = (
@@ -1332,10 +1330,3 @@ export const setFeatureFlag = <F extends keyof FEATURE_FLAGS>(
console.error("unable to set feature flag", e); console.error("unable to set feature flag", e);
} }
}; };
export const oneOf = <N extends string | number | symbol | null, H extends N>(
needle: N,
haystack: readonly H[],
): needle is H => {
return haystack.includes(needle as any);
};
-2
View File
@@ -438,8 +438,6 @@ export class Scene {
options: { options: {
informMutation: boolean; informMutation: boolean;
isDragging: boolean; isDragging: boolean;
isBindingEnabled?: boolean;
isMidpointSnappingEnabled?: boolean;
} = { } = {
informMutation: true, informMutation: true,
isDragging: false, isDragging: false,
@@ -27,6 +27,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#66a80f", "strokeColor": "#66a80f",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -64,6 +67,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#9c36b5", "strokeColor": "#9c36b5",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -116,6 +122,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
], ],
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
@@ -177,6 +186,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
], ],
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
@@ -223,6 +235,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -266,6 +281,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"originalText": "HEYYYYY", "originalText": "HEYYYYY",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#c2255c", "strokeColor": "#c2255c",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -312,6 +330,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"originalText": "Whats up ?", "originalText": "Whats up ?",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -372,6 +393,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
], ],
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
@@ -419,6 +443,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"originalText": "HELLO WORLD!!", "originalText": "HELLO WORLD!!",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -479,6 +506,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
], ],
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
@@ -526,6 +556,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"originalText": "HELLO WORLD!!", "originalText": "HELLO WORLD!!",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -566,6 +599,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -603,6 +639,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -660,6 +699,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
], ],
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
@@ -707,6 +749,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"originalText": "HELLO WORLD!!", "originalText": "HELLO WORLD!!",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -753,6 +798,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"originalText": "HEYYYYY", "originalText": "HEYYYYY",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -799,6 +847,9 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"originalText": "WHATS UP ?", "originalText": "WHATS UP ?",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -834,6 +885,9 @@ exports[`Test Transform > should not allow duplicate ids 1`] = `
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -879,6 +933,9 @@ exports[`Test Transform > should transform linear elements 1`] = `
], ],
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": null, "startBinding": null,
@@ -926,6 +983,9 @@ exports[`Test Transform > should transform linear elements 2`] = `
], ],
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": "dot", "startArrowhead": "dot",
"startBinding": null, "startBinding": null,
@@ -973,6 +1033,9 @@ exports[`Test Transform > should transform linear elements 3`] = `
"polygon": false, "polygon": false,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": null, "startBinding": null,
@@ -1020,6 +1083,9 @@ exports[`Test Transform > should transform linear elements 4`] = `
"polygon": false, "polygon": false,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": null, "startBinding": null,
@@ -1054,6 +1120,9 @@ exports[`Test Transform > should transform regular shapes 1`] = `
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1086,6 +1155,9 @@ exports[`Test Transform > should transform regular shapes 2`] = `
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1118,6 +1190,9 @@ exports[`Test Transform > should transform regular shapes 3`] = `
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1150,6 +1225,9 @@ exports[`Test Transform > should transform regular shapes 4`] = `
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1182,6 +1260,9 @@ exports[`Test Transform > should transform regular shapes 5`] = `
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "dotted", "strokeStyle": "dotted",
@@ -1214,6 +1295,9 @@ exports[`Test Transform > should transform regular shapes 6`] = `
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1971c2", "strokeColor": "#1971c2",
"strokeStyle": "dashed", "strokeStyle": "dashed",
@@ -1252,6 +1336,9 @@ exports[`Test Transform > should transform text element 1`] = `
"originalText": "HELLO WORLD!", "originalText": "HELLO WORLD!",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1293,6 +1380,9 @@ exports[`Test Transform > should transform text element 2`] = `
"originalText": "STYLED HELLO WORLD!", "originalText": "STYLED HELLO WORLD!",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#5f3dc4", "strokeColor": "#5f3dc4",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1339,6 +1429,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1378,6 +1471,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1421,6 +1517,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1468,6 +1567,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1527,6 +1629,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"roundness": { "roundness": {
"type": 2, "type": 2,
}, },
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
@@ -1591,6 +1696,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"roundness": { "roundness": {
"type": 2, "type": 2,
}, },
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
@@ -1640,6 +1748,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "B", "originalText": "B",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1683,6 +1794,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "A", "originalText": "A",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1726,6 +1840,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "Alice", "originalText": "Alice",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1769,6 +1886,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "Bob", "originalText": "Bob",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1810,6 +1930,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "How are you?", "originalText": "How are you?",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1851,6 +1974,9 @@ exports[`Test Transform > should transform the elements correctly when linear el
"originalText": "Friendship", "originalText": "Friendship",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -1904,6 +2030,9 @@ exports[`Test Transform > should transform to labelled arrows when label provide
], ],
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": null, "startBinding": null,
@@ -1956,6 +2085,9 @@ exports[`Test Transform > should transform to labelled arrows when label provide
], ],
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": null, "startBinding": null,
@@ -2008,6 +2140,9 @@ exports[`Test Transform > should transform to labelled arrows when label provide
], ],
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": null, "startBinding": null,
@@ -2060,6 +2195,9 @@ exports[`Test Transform > should transform to labelled arrows when label provide
], ],
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": null, "startBinding": null,
@@ -2100,6 +2238,9 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"originalText": "LABELED ARROW", "originalText": "LABELED ARROW",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2141,6 +2282,9 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"originalText": "STYLED LABELED ARROW", "originalText": "STYLED LABELED ARROW",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#099268", "strokeColor": "#099268",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2182,6 +2326,9 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"originalText": "ANOTHER STYLED LABELLED ARROW", "originalText": "ANOTHER STYLED LABELLED ARROW",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1098ad", "strokeColor": "#1098ad",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2224,6 +2371,9 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"originalText": "ANOTHER STYLED LABELLED ARROW", "originalText": "ANOTHER STYLED LABELLED ARROW",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#099268", "strokeColor": "#099268",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2265,6 +2415,9 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2302,6 +2455,9 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2339,6 +2495,9 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2376,6 +2535,9 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2413,6 +2575,9 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#c2255c", "strokeColor": "#c2255c",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2450,6 +2615,9 @@ exports[`Test Transform > should transform to text containers when label provide
"opacity": 100, "opacity": 100,
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#f08c00", "strokeColor": "#f08c00",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2488,6 +2656,9 @@ exports[`Test Transform > should transform to text containers when label provide
"originalText": "RECTANGLE TEXT CONTAINER", "originalText": "RECTANGLE TEXT CONTAINER",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2529,6 +2700,9 @@ exports[`Test Transform > should transform to text containers when label provide
"originalText": "ELLIPSE TEXT CONTAINER", "originalText": "ELLIPSE TEXT CONTAINER",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2572,6 +2746,9 @@ exports[`Test Transform > should transform to text containers when label provide
TEXT CONTAINER", TEXT CONTAINER",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2615,6 +2792,9 @@ exports[`Test Transform > should transform to text containers when label provide
"originalText": "STYLED DIAMOND TEXT CONTAINER", "originalText": "STYLED DIAMOND TEXT CONTAINER",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#099268", "strokeColor": "#099268",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2657,6 +2837,9 @@ exports[`Test Transform > should transform to text containers when label provide
"originalText": "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER", "originalText": "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#c2255c", "strokeColor": "#c2255c",
"strokeStyle": "solid", "strokeStyle": "solid",
@@ -2700,6 +2883,9 @@ exports[`Test Transform > should transform to text containers when label provide
"originalText": "STYLED ELLIPSE TEXT CONTAINER", "originalText": "STYLED ELLIPSE TEXT CONTAINER",
"roughness": 1, "roughness": 1,
"roundness": null, "roundness": null,
"schemaState": {
"tracks": {},
},
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#c2255c", "strokeColor": "#c2255c",
"strokeStyle": "solid", "strokeStyle": "solid",
+33 -77
View File
@@ -1,4 +1,5 @@
import { import {
KEYS,
arrayToMap, arrayToMap,
getFeatureFlag, getFeatureFlag,
invariant, invariant,
@@ -136,6 +137,12 @@ export const maxBindingDistance_simple = (zoom?: AppState["zoom"]): number => {
); );
}; };
export const shouldEnableBindingForPointerEvent = (
event: React.PointerEvent<HTMLElement>,
) => {
return !event[KEYS.CTRL_OR_CMD];
};
export const isBindingEnabled = (appState: { export const isBindingEnabled = (appState: {
isBindingEnabled: AppState["isBindingEnabled"]; isBindingEnabled: AppState["isBindingEnabled"];
}): boolean => { }): boolean => {
@@ -170,20 +177,8 @@ export const bindOrUnbindBindingElement = (
}, },
); );
bindOrUnbindBindingElementEdge( bindOrUnbindBindingElementEdge(arrow, start, "start", scene);
arrow, bindOrUnbindBindingElementEdge(arrow, end, "end", scene);
start,
"start",
scene,
appState.isBindingEnabled,
);
bindOrUnbindBindingElementEdge(
arrow,
end,
"end",
scene,
appState.isBindingEnabled,
);
if (start.focusPoint || end.focusPoint) { if (start.focusPoint || end.focusPoint) {
// If the strategy dictates a focus point override, then // If the strategy dictates a focus point override, then
// update the arrow points to point to the focus point. // update the arrow points to point to the focus point.
@@ -226,21 +221,12 @@ const bindOrUnbindBindingElementEdge = (
{ mode, element, focusPoint }: BindingStrategy, { mode, element, focusPoint }: BindingStrategy,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
scene: Scene, scene: Scene,
shouldSnapToOutline = true,
): void => { ): void => {
if (mode === null) { if (mode === null) {
// null means break the binding // null means break the binding
unbindBindingElement(arrow, startOrEnd, scene); unbindBindingElement(arrow, startOrEnd, scene);
} else if (mode !== undefined) { } else if (mode !== undefined) {
bindBindingElement( bindBindingElement(arrow, element, mode, startOrEnd, scene, focusPoint);
arrow,
element,
mode,
startOrEnd,
scene,
focusPoint,
shouldSnapToOutline,
);
} }
}; };
@@ -812,7 +798,6 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
startDragged ? "start" : "end", startDragged ? "start" : "end",
elementsMap, elementsMap,
appState.zoom, appState.zoom,
appState.isMidpointSnappingEnabled,
) || globalPoint, ) || globalPoint,
} }
: { mode: null }; : { mode: null };
@@ -857,7 +842,6 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
startDragged ? "end" : "start", startDragged ? "end" : "start",
elementsMap, elementsMap,
appState.zoom, appState.zoom,
appState.isMidpointSnappingEnabled,
) || otherEndpoint, ) || otherEndpoint,
} }
: { mode: undefined } : { mode: undefined }
@@ -1021,7 +1005,6 @@ export const bindBindingElement = (
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
scene: Scene, scene: Scene,
focusPoint?: GlobalPoint, focusPoint?: GlobalPoint,
shouldSnapToOutline = true,
): void => { ): void => {
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
@@ -1036,7 +1019,6 @@ export const bindBindingElement = (
hoveredElement, hoveredElement,
startOrEnd, startOrEnd,
elementsMap, elementsMap,
shouldSnapToOutline,
), ),
}; };
} else { } else {
@@ -1370,7 +1352,6 @@ export const bindPointToSnapToElementOutline = (
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: ElementsMap, elementsMap: ElementsMap,
customIntersector?: LineSegment<GlobalPoint>, customIntersector?: LineSegment<GlobalPoint>,
isMidpointSnappingEnabled = true,
): GlobalPoint => { ): GlobalPoint => {
const elbowed = isElbowArrow(arrowElement); const elbowed = isElbowArrow(arrowElement);
const point = LinearElementEditor.getPointAtIndexGlobalCoordinates( const point = LinearElementEditor.getPointAtIndexGlobalCoordinates(
@@ -1410,9 +1391,13 @@ export const bindPointToSnapToElementOutline = (
const isHorizontal = headingIsHorizontal( const isHorizontal = headingIsHorizontal(
headingForPointFromElement(bindableElement, aabb, point), headingForPointFromElement(bindableElement, aabb, point),
); );
const snapPoint = isMidpointSnappingEnabled const snapPoint = snapToMid(
? snapToMid(bindableElement, elementsMap, edgePoint, 0.05, arrowElement) bindableElement,
: undefined; elementsMap,
edgePoint,
0.05,
arrowElement,
);
const resolved = snapPoint || point; const resolved = snapPoint || point;
const otherPoint = pointFrom<GlobalPoint>( const otherPoint = pointFrom<GlobalPoint>(
isHorizontal ? bindableCenter[0] : resolved[0], isHorizontal ? bindableCenter[0] : resolved[0],
@@ -1791,13 +1776,10 @@ export const updateBoundPoint = (
); );
const otherArrowPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( const otherArrowPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
arrow, arrow,
startOrEnd === "startBinding" ? 1 : -2, startOrEnd === "startBinding" ? -1 : 0,
elementsMap, elementsMap,
); );
const otherFocusPointOrArrowPoint = const otherFocusPointOrArrowPoint = otherFocusPoint || otherArrowPoint;
arrow.points.length === 2
? otherFocusPoint || otherArrowPoint
: otherArrowPoint;
const intersector = const intersector =
otherFocusPointOrArrowPoint && otherFocusPointOrArrowPoint &&
lineSegment(focusPoint, otherFocusPointOrArrowPoint); lineSegment(focusPoint, otherFocusPointOrArrowPoint);
@@ -1907,8 +1889,6 @@ export const calculateFixedPointForElbowArrowBinding = (
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: ElementsMap, elementsMap: ElementsMap,
shouldSnapToOutline = true,
isMidpointSnappingEnabled = true,
): { fixedPoint: FixedPoint } => { ): { fixedPoint: FixedPoint } => {
const bounds = [ const bounds = [
hoveredElement.x, hoveredElement.x,
@@ -1916,20 +1896,12 @@ export const calculateFixedPointForElbowArrowBinding = (
hoveredElement.x + hoveredElement.width, hoveredElement.x + hoveredElement.width,
hoveredElement.y + hoveredElement.height, hoveredElement.y + hoveredElement.height,
] as Bounds; ] as Bounds;
const snappedPoint = shouldSnapToOutline const snappedPoint = bindPointToSnapToElementOutline(
? bindPointToSnapToElementOutline( linearElement,
linearElement, hoveredElement,
hoveredElement, startOrEnd,
startOrEnd, elementsMap,
elementsMap, );
undefined,
isMidpointSnappingEnabled,
)
: LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
startOrEnd === "start" ? 0 : -1,
elementsMap,
);
const globalMidPoint = pointFrom( const globalMidPoint = pointFrom(
bounds[0] + (bounds[2] - bounds[0]) / 2, bounds[0] + (bounds[2] - bounds[0]) / 2,
bounds[1] + (bounds[3] - bounds[1]) / 2, bounds[1] + (bounds[3] - bounds[1]) / 2,
@@ -2475,37 +2447,21 @@ export const getArrowLocalFixedPoints = (
]; ];
}; };
export const isFixedPoint = ( export const normalizeFixedPoint = <T extends FixedPoint | null>(
fixedPoint: any,
): fixedPoint is FixedPointBinding["fixedPoint"] => {
return (
Array.isArray(fixedPoint) &&
fixedPoint.length === 2 &&
fixedPoint.every((coord) => Number.isFinite(coord))
);
};
export const normalizeFixedPoint = <T extends FixedPoint>(
fixedPoint: T, fixedPoint: T,
): FixedPoint => { ): T extends null ? null : FixedPoint => {
if (!isFixedPoint(fixedPoint)) {
return [0.5001, 0.5001];
}
const EPSILON = 0.0001;
// Do not allow a precise 0.5 for fixed point ratio // Do not allow a precise 0.5 for fixed point ratio
// to avoid jumping arrow heading due to floating point imprecision // to avoid jumping arrow heading due to floating point imprecision
if ( if (
Math.abs(fixedPoint[0] - 0.5) < EPSILON || fixedPoint &&
Math.abs(fixedPoint[1] - 0.5) < EPSILON (Math.abs(fixedPoint[0] - 0.5) < 0.0001 ||
Math.abs(fixedPoint[1] - 0.5) < 0.0001)
) { ) {
return fixedPoint.map((ratio) => return fixedPoint.map((ratio) =>
Math.abs(ratio - 0.5) < EPSILON ? 0.5001 : ratio, Math.abs(ratio - 0.5) < 0.0001 ? 0.5001 : ratio,
) as FixedPoint; ) as T extends null ? null : FixedPoint;
} }
return fixedPoint as any as T extends null ? null : FixedPoint;
return fixedPoint;
}; };
type Side = type Side =
+6 -1
View File
@@ -78,7 +78,12 @@ import type {
} from "./types"; } from "./types";
export const shouldTestInside = (element: ExcalidrawElement) => { export const shouldTestInside = (element: ExcalidrawElement) => {
if (element.type === "arrow") { if (
element.type === "arrow" ||
// frame elements should ignore inside hit test even if background is not
// transparent, so we can select children easily
isFrameLikeElement(element)
) {
return false; return false;
} }
+2 -14
View File
@@ -915,8 +915,6 @@ export const updateElbowArrowPoints = (
}, },
options?: { options?: {
isDragging?: boolean; isDragging?: boolean;
isBindingEnabled?: boolean;
isMidpointSnappingEnabled?: boolean;
}, },
): ElementUpdate<ExcalidrawElbowArrowElement> => { ): ElementUpdate<ExcalidrawElbowArrowElement> => {
if (arrow.points.length < 2) { if (arrow.points.length < 2) {
@@ -1204,8 +1202,6 @@ const getElbowArrowData = (
options?: { options?: {
isDragging?: boolean; isDragging?: boolean;
zoom?: AppState["zoom"]; zoom?: AppState["zoom"];
isBindingEnabled?: boolean;
isMidpointSnappingEnabled?: boolean;
}, },
) => { ) => {
const origStartGlobalPoint: GlobalPoint = pointTranslate< const origStartGlobalPoint: GlobalPoint = pointTranslate<
@@ -1219,7 +1215,7 @@ const getElbowArrowData = (
let hoveredStartElement = null; let hoveredStartElement = null;
let hoveredEndElement = null; let hoveredEndElement = null;
if (options?.isDragging && options?.isBindingEnabled !== false) { if (options?.isDragging) {
const elements = Array.from(elementsMap.values()); const elements = Array.from(elementsMap.values());
hoveredStartElement = hoveredStartElement =
getHoveredElement( getHoveredElement(
@@ -1259,8 +1255,6 @@ const getElbowArrowData = (
hoveredStartElement, hoveredStartElement,
elementsMap, elementsMap,
options?.isDragging, options?.isDragging,
options?.isBindingEnabled,
options?.isMidpointSnappingEnabled,
); );
const endGlobalPoint = getGlobalPoint( const endGlobalPoint = getGlobalPoint(
{ {
@@ -1276,8 +1270,6 @@ const getElbowArrowData = (
hoveredEndElement, hoveredEndElement,
elementsMap, elementsMap,
options?.isDragging, options?.isDragging,
options?.isBindingEnabled,
options?.isMidpointSnappingEnabled,
); );
const startHeading = getBindPointHeading( const startHeading = getBindPointHeading(
startGlobalPoint, startGlobalPoint,
@@ -2221,18 +2213,14 @@ const getGlobalPoint = (
element?: ExcalidrawBindableElement | null, element?: ExcalidrawBindableElement | null,
elementsMap?: ElementsMap, elementsMap?: ElementsMap,
isDragging?: boolean, isDragging?: boolean,
isBindingEnabled = true,
isMidpointSnappingEnabled = true,
): GlobalPoint => { ): GlobalPoint => {
if (isDragging) { if (isDragging) {
if (isBindingEnabled && element && elementsMap) { if (element && elementsMap) {
return bindPointToSnapToElementOutline( return bindPointToSnapToElementOutline(
arrow, arrow,
element, element,
startOrEnd, startOrEnd,
elementsMap, elementsMap,
undefined,
isMidpointSnappingEnabled,
); );
} }
+1
View File
@@ -83,6 +83,7 @@ export * from "./positionElementsOnGrid";
export * from "./renderElement"; export * from "./renderElement";
export * from "./resizeElements"; export * from "./resizeElements";
export * from "./resizeTest"; export * from "./resizeTest";
export * from "./schema";
export * from "./Scene"; export * from "./Scene";
export * from "./selection"; export * from "./selection";
export * from "./shape"; export * from "./shape";
+18 -50
View File
@@ -359,20 +359,11 @@ export class LinearElementEditor {
linearElementEditor, linearElementEditor,
); );
LinearElementEditor.movePoints( LinearElementEditor.movePoints(element, app.scene, positions, {
element, startBinding: updates?.startBinding,
app.scene, endBinding: updates?.endBinding,
positions, moveMidPointsWithElement: updates?.moveMidPointsWithElement,
{ });
startBinding: updates?.startBinding,
endBinding: updates?.endBinding,
moveMidPointsWithElement: updates?.moveMidPointsWithElement,
},
{
isBindingEnabled: app.state.isBindingEnabled,
isMidpointSnappingEnabled: app.state.isMidpointSnappingEnabled,
},
);
// Set the suggested binding from the updates if available // Set the suggested binding from the updates if available
if (isBindingElement(element, false)) { if (isBindingElement(element, false)) {
if (isBindingEnabled(app.state)) { if (isBindingEnabled(app.state)) {
@@ -427,7 +418,6 @@ export class LinearElementEditor {
"start", "start",
elementsMap, elementsMap,
app.state.zoom, app.state.zoom,
app.state.isMidpointSnappingEnabled,
) )
: linearElementEditor.initialState.altFocusPoint, : linearElementEditor.initialState.altFocusPoint,
}, },
@@ -548,20 +538,11 @@ export class LinearElementEditor {
linearElementEditor, linearElementEditor,
); );
LinearElementEditor.movePoints( LinearElementEditor.movePoints(element, app.scene, positions, {
element, startBinding: updates?.startBinding,
app.scene, endBinding: updates?.endBinding,
positions, moveMidPointsWithElement: updates?.moveMidPointsWithElement,
{ });
startBinding: updates?.startBinding,
endBinding: updates?.endBinding,
moveMidPointsWithElement: updates?.moveMidPointsWithElement,
},
{
isBindingEnabled: app.state.isBindingEnabled,
isMidpointSnappingEnabled: app.state.isMidpointSnappingEnabled,
},
);
// Set the suggested binding from the updates if available // Set the suggested binding from the updates if available
if (isBindingElement(element, false)) { if (isBindingElement(element, false)) {
@@ -655,7 +636,6 @@ export class LinearElementEditor {
"start", "start",
elementsMap, elementsMap,
app.state.zoom, app.state.zoom,
app.state.isMidpointSnappingEnabled,
) )
: linearElementEditor.initialState.altFocusPoint, : linearElementEditor.initialState.altFocusPoint,
}, },
@@ -1544,10 +1524,6 @@ export class LinearElementEditor {
endBinding?: FixedPointBinding | null; endBinding?: FixedPointBinding | null;
moveMidPointsWithElement?: boolean | null; moveMidPointsWithElement?: boolean | null;
}, },
options?: {
isBindingEnabled?: boolean;
isMidpointSnappingEnabled?: boolean;
},
) { ) {
const { points } = element; const { points } = element;
@@ -1616,8 +1592,6 @@ export class LinearElementEditor {
otherUpdates, otherUpdates,
{ {
isDragging: Array.from(pointUpdates.values()).some((t) => t.isDragging), isDragging: Array.from(pointUpdates.values()).some((t) => t.isDragging),
isBindingEnabled: options?.isBindingEnabled,
isMidpointSnappingEnabled: options?.isMidpointSnappingEnabled,
}, },
); );
} }
@@ -1732,8 +1706,6 @@ export class LinearElementEditor {
isDragging?: boolean; isDragging?: boolean;
zoom?: AppState["zoom"]; zoom?: AppState["zoom"];
sceneElementsMap?: NonDeletedSceneElementsMap; sceneElementsMap?: NonDeletedSceneElementsMap;
isBindingEnabled?: boolean;
isMidpointSnappingEnabled?: boolean;
}, },
) { ) {
if (isElbowArrow(element)) { if (isElbowArrow(element)) {
@@ -1754,8 +1726,6 @@ export class LinearElementEditor {
scene.mutateElement(element, updates, { scene.mutateElement(element, updates, {
informMutation: true, informMutation: true,
isDragging: options?.isDragging ?? false, isDragging: options?.isDragging ?? false,
isBindingEnabled: options?.isBindingEnabled,
isMidpointSnappingEnabled: options?.isMidpointSnappingEnabled,
}); });
} else { } else {
// TODO do we need to get precise coords here just to calc centers? // TODO do we need to get precise coords here just to calc centers?
@@ -2175,16 +2145,14 @@ const pointDraggingUpdates = (
suggestedBinding: suggestedBindingElement suggestedBinding: suggestedBindingElement
? { ? {
element: suggestedBindingElement, element: suggestedBindingElement,
midPoint: app.state.isMidpointSnappingEnabled midPoint: snapToMid(
? snapToMid( suggestedBindingElement,
suggestedBindingElement, elementsMap,
elementsMap, pointFrom<GlobalPoint>(
pointFrom<GlobalPoint>( scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerX - linearElementEditor.pointerOffset.x, scenePointerY - linearElementEditor.pointerOffset.y,
scenePointerY - linearElementEditor.pointerOffset.y, ),
), ),
)
: undefined,
} }
: null, : null,
}, },
+14 -3
View File
@@ -11,6 +11,7 @@ import type { Mutable } from "@excalidraw/common/utility-types";
import { ShapeCache } from "./shape"; import { ShapeCache } from "./shape";
import { updateElbowArrowPoints } from "./elbowArrow"; import { updateElbowArrowPoints } from "./elbowArrow";
import { ensureSchemaStateForElementType } from "./schema";
import { isElbowArrow } from "./typeChecks"; import { isElbowArrow } from "./typeChecks";
@@ -40,8 +41,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
updates: ElementUpdate<TElement>, updates: ElementUpdate<TElement>,
options?: { options?: {
isDragging?: boolean; isDragging?: boolean;
isBindingEnabled?: boolean;
isMidpointSnappingEnabled?: boolean;
}, },
) => { ) => {
let didChange = false; let didChange = false;
@@ -139,6 +138,10 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
element.version = updates.version ?? element.version + 1; element.version = updates.version ?? element.version + 1;
element.versionNonce = updates.versionNonce ?? randomInteger(); element.versionNonce = updates.versionNonce ?? randomInteger();
element.updated = getUpdatedTimestamp(); element.updated = getUpdatedTimestamp();
element.schemaState = ensureSchemaStateForElementType(
element.schemaState,
element.type,
) as TElement["schemaState"];
return element; return element;
}; };
@@ -168,13 +171,21 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
return element; return element;
} }
return { const updatedElement = {
...element, ...element,
...updates, ...updates,
version: updates.version ?? element.version + 1, version: updates.version ?? element.version + 1,
versionNonce: updates.versionNonce ?? randomInteger(), versionNonce: updates.versionNonce ?? randomInteger(),
updated: getUpdatedTimestamp(), updated: getUpdatedTimestamp(),
}; };
return {
...updatedElement,
schemaState: ensureSchemaStateForElementType(
updatedElement.schemaState,
updatedElement.type,
),
};
}; };
/** /**
+3
View File
@@ -20,6 +20,7 @@ import {
getElementAbsoluteCoords, getElementAbsoluteCoords,
getResizedElementAbsoluteCoords, getResizedElementAbsoluteCoords,
} from "./bounds"; } from "./bounds";
import { ensureSchemaStateForElementType } from "./schema";
import { newElementWith } from "./mutateElement"; import { newElementWith } from "./mutateElement";
import { getBoundTextMaxWidth } from "./textElement"; import { getBoundTextMaxWidth } from "./textElement";
import { normalizeText, measureText } from "./textMeasurements"; import { normalizeText, measureText } from "./textMeasurements";
@@ -70,6 +71,7 @@ export type ElementConstructorOpts = MarkOptional<
| "roughness" | "roughness"
| "strokeWidth" | "strokeWidth"
| "roundness" | "roundness"
| "schemaState"
| "locked" | "locked"
| "opacity" | "opacity"
| "customData" | "customData"
@@ -144,6 +146,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
roundness, roundness,
seed: rest.seed ?? randomInteger(), seed: rest.seed ?? randomInteger(),
version: rest.version || 1, version: rest.version || 1,
schemaState: ensureSchemaStateForElementType(rest.schemaState, type),
versionNonce: rest.versionNonce ?? 0, versionNonce: rest.versionNonce ?? 0,
isDeleted: false as false, isDeleted: false as false,
boundElements, boundElements,
+41 -1
View File
@@ -22,6 +22,7 @@ import {
isRTL, isRTL,
getVerticalOffset, getVerticalOffset,
invariant, invariant,
isTransparent,
applyDarkModeFilter, applyDarkModeFilter,
isSafari, isSafari,
} from "@excalidraw/common"; } from "@excalidraw/common";
@@ -78,6 +79,7 @@ import type {
ExcalidrawFrameLikeElement, ExcalidrawFrameLikeElement,
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
ElementsMap, ElementsMap,
ExcalidrawFrameElement,
} from "./types"; } from "./types";
import type { RoughCanvas } from "roughjs/bin/canvas"; import type { RoughCanvas } from "roughjs/bin/canvas";
@@ -777,6 +779,45 @@ export const renderSelectionElement = (
context.restore(); context.restore();
}; };
export const renderFrameBackground = (
frame: ExcalidrawFrameElement,
context: CanvasRenderingContext2D,
appState: StaticCanvasAppState | InteractiveCanvasAppState,
opts?: {
roundCorners?: boolean;
},
) => {
if (isTransparent(frame.backgroundColor)) {
return;
}
context.save();
context.translate(frame.x + appState.scrollX, frame.y + appState.scrollY);
context.fillStyle =
appState.theme === THEME.DARK
? applyDarkModeFilter(frame.backgroundColor)
: frame.backgroundColor;
const shouldRoundCorners = opts?.roundCorners ?? true;
if (shouldRoundCorners && FRAME_STYLE.radius && context.roundRect) {
context.beginPath();
context.roundRect(
0,
0,
frame.width,
frame.height,
FRAME_STYLE.radius / appState.zoom.value,
);
context.fill();
context.closePath();
} else {
context.fillRect(0, 0, frame.width, frame.height);
}
context.restore();
};
export const renderElement = ( export const renderElement = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: RenderableElementsMap, elementsMap: RenderableElementsMap,
@@ -808,7 +849,6 @@ export const renderElement = (
element.x + appState.scrollX, element.x + appState.scrollX,
element.y + appState.scrollY, element.y + appState.scrollY,
); );
context.fillStyle = "rgba(0, 0, 200, 0.04)";
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value; context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
context.strokeStyle = context.strokeStyle =
+91
View File
@@ -0,0 +1,91 @@
/**
* Shared schema primitives used by element types and higher-level migrations.
*/
export const SCHEMA_INITIAL_TRACK_VERSION = 1 as const;
/** Core namespace reserved for built-in Excalidraw migrations. */
export const SCHEMA_CORE_NAMESPACE = "core" as const;
export type SchemaNamespace = typeof SCHEMA_CORE_NAMESPACE | `host.${string}`;
/**
* A schema track is an independent version line:
* - core tracks: "excalidraw.*"
* - host tracks: "host.<appId>.<track>"
*/
export type SchemaTrack = `excalidraw.${string}` | `host.${string}.${string}`;
export type ElementSchemaState = Readonly<{
tracks: Readonly<Record<string, number>>;
}>;
/** Core frame track id used by the frame background migration. */
export const CORE_FRAME_SCHEMA_TRACK = "excalidraw.shape.frame" as const;
/** Latest core track versions supported by this build. */
export const CORE_SUPPORTED_TRACKS = {
[CORE_FRAME_SCHEMA_TRACK]: 2,
} as const;
const getRequiredCoreTracksForElementType = (type: string) => {
if (type === "frame") {
return {
[CORE_FRAME_SCHEMA_TRACK]: CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK],
} as const;
}
return {} as const;
};
const isValidTrackVersion = (version: unknown): version is number =>
typeof version === "number" &&
Number.isInteger(version) &&
version >= SCHEMA_INITIAL_TRACK_VERSION;
/**
* Ensures an element schema state is normalized and satisfies type defaults.
* Required core tracks are only ever bumped forward (never downgraded).
*/
export const ensureSchemaStateForElementType = (
schemaState: ElementSchemaState | undefined,
type: string,
): ElementSchemaState => {
const requiredTracks = getRequiredCoreTracksForElementType(type);
const currentTracks = schemaState?.tracks || {};
const nextTracks: Record<string, number> = {};
let didChange = !schemaState;
for (const [track, version] of Object.entries(
currentTracks as Record<string, unknown>,
)) {
if (isValidTrackVersion(version)) {
nextTracks[track] = version;
continue;
}
nextTracks[track] = SCHEMA_INITIAL_TRACK_VERSION;
didChange = true;
}
for (const [track, requiredVersion] of Object.entries(requiredTracks)) {
const currentVersion = nextTracks[track];
if (
!isValidTrackVersion(currentVersion) ||
currentVersion < requiredVersion
) {
nextTracks[track] = requiredVersion;
didChange = true;
}
}
if (!didChange) {
return schemaState!;
}
return { tracks: nextTracks };
};
/**
* Default schema state for newly created elements.
* New frames are created at the latest supported frame track version.
*/
export const getDefaultSchemaStateForElementType = (
type: string,
): ElementSchemaState => ensureSchemaStateForElementType(undefined, type);
+5 -1
View File
@@ -15,7 +15,9 @@ import type {
ValueOf, ValueOf,
} from "@excalidraw/common/utility-types"; } from "@excalidraw/common/utility-types";
export type ChartType = "bar" | "line" | "radar"; import type { ElementSchemaState } from "./schema";
export type ChartType = "bar" | "line";
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag"; export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
export type FontFamilyKeys = keyof typeof FONT_FAMILY; export type FontFamilyKeys = keyof typeof FONT_FAMILY;
export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys]; export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
@@ -58,6 +60,8 @@ type _ExcalidrawElementBase = Readonly<{
/** Integer that is sequentially incremented on each change. Used to reconcile /** Integer that is sequentially incremented on each change. Used to reconcile
elements during collaboration or when saving to server. */ elements during collaboration or when saving to server. */
version: number; version: number;
/** Per-track schema state used by migrations during restore. */
schemaState: ElementSchemaState;
/** Random integer that is regenerated on each change. /** Random integer that is regenerated on each change.
Used for deterministic reconciliation of updates during collaboration, Used for deterministic reconciliation of updates during collaboration,
in case the versions (see above) are identical. */ in case the versions (see above) are identical. */
+8 -11
View File
@@ -659,23 +659,20 @@ export const projectFixedPointOntoDiagonal = (
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: ElementsMap, elementsMap: ElementsMap,
zoom: AppState["zoom"], zoom: AppState["zoom"],
isMidpointSnappingEnabled: boolean = true,
): GlobalPoint | null => { ): GlobalPoint | null => {
invariant(arrow.points.length >= 2, "Arrow must have at least two points"); invariant(arrow.points.length >= 2, "Arrow must have at least two points");
if (arrow.width < 3 && arrow.height < 3) { if (arrow.width < 3 && arrow.height < 3) {
return null; return null;
} }
if (isMidpointSnappingEnabled) { const sideMidPoint = getSnapOutlineMidPoint(
const sideMidPoint = getSnapOutlineMidPoint( point,
point, element,
element, elementsMap,
elementsMap, zoom,
zoom, );
); if (sideMidPoint) {
if (sideMidPoint) { return sideMidPoint;
return sideMidPoint;
}
} }
// Do the projection onto the diagonals (or center lines // Do the projection onto the diagonals (or center lines
+47
View File
@@ -38,6 +38,53 @@ describe("check rotated elements can be hit:", () => {
}); });
}); });
describe("frame hit testing", () => {
it.each(["transparent", "#ffffff"])(
"does not hit frame inside regardless of background color (%s)",
(backgroundColor) => {
const element = API.createElement({
type: "frame",
x: 0,
y: 0,
width: 100,
height: 100,
backgroundColor,
});
const elementsMap = arrayToMap([element]);
expect(
hitElementItself({
point: pointFrom<GlobalPoint>(50, 50),
element,
threshold: 10,
elementsMap,
}),
).toBe(false);
},
);
it("hits frame outline", () => {
const element = API.createElement({
type: "frame",
x: 0,
y: 0,
width: 100,
height: 100,
backgroundColor: "#ffffff",
});
const elementsMap = arrayToMap([element]);
expect(
hitElementItself({
point: pointFrom<GlobalPoint>(0, 50),
element,
threshold: 1,
elementsMap,
}),
).toBe(true);
});
});
describe("hitElementItself cache", () => { describe("hitElementItself cache", () => {
beforeEach(async () => { beforeEach(async () => {
// reset cache // reset cache
@@ -118,6 +118,7 @@ export const actionClearCanvas = register({
gridStep: appState.gridStep, gridStep: appState.gridStep,
gridModeEnabled: appState.gridModeEnabled, gridModeEnabled: appState.gridModeEnabled,
stats: appState.stats, stats: appState.stats,
pasteDialog: appState.pasteDialog,
activeTool: activeTool:
appState.activeTool.type === "image" appState.activeTool.type === "image"
? { ? {
+5 -1
View File
@@ -271,7 +271,11 @@ export const actionLoadScene = register({
elements: loadedElements, elements: loadedElements,
appState: loadedAppState, appState: loadedAppState,
files, files,
} = await loadFromJSON(appState, elements); } = await loadFromJSON(
appState,
elements,
app.getSchemaMigrationRegistry(),
);
return { return {
elements: loadedElements, elements: loadedElements,
appState: loadedAppState, appState: loadedAppState,
@@ -6,11 +6,23 @@ import {
FONT_FAMILY, FONT_FAMILY,
STROKE_WIDTH, STROKE_WIDTH,
} from "@excalidraw/common"; } from "@excalidraw/common";
import {
CORE_FRAME_SCHEMA_TRACK,
CORE_SUPPORTED_TRACKS,
} from "@excalidraw/element";
import { Excalidraw } from "../index"; import { Excalidraw } from "../index";
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
import { UI } from "../tests/helpers/ui"; import { UI } from "../tests/helpers/ui";
import { render } from "../tests/test-utils"; import { act, render } from "../tests/test-utils";
import {
actionChangeBackgroundColor,
actionChangeRoundness,
actionChangeStrokeWidth,
} from "./actionProperties";
const { h } = window;
describe("element locking", () => { describe("element locking", () => {
beforeEach(async () => { beforeEach(async () => {
@@ -109,6 +121,21 @@ describe("element locking", () => {
expect(crossHatchButton).toBe(null); expect(crossHatchButton).toBe(null);
}); });
it("should show background color picker for selected frame", () => {
const frame = API.createElement({
type: "frame",
});
API.setElements([frame]);
API.setSelectedElements([frame]);
expect(
queryByTestId(
document.body,
`color-top-pick-${DEFAULT_ELEMENT_BACKGROUND_PICKS[0]}`,
),
).not.toBe(null);
});
it("should highlight common stroke width of selected elements", () => { it("should highlight common stroke width of selected elements", () => {
const rect1 = API.createElement({ const rect1 = API.createElement({
type: "rectangle", type: "rectangle",
@@ -169,5 +196,77 @@ describe("element locking", () => {
"active", "active",
); );
}); });
it("should not update text background when changing background in mixed frame selection", () => {
const frame = API.createElement({
type: "frame",
schemaState: { tracks: {} },
});
const text = API.createElement({
type: "text",
backgroundColor: COLOR_PALETTE.transparent,
});
API.setElements([text, frame]);
API.setSelectedElements([text, frame]);
act(() => {
h.app.actionManager.executeAction(actionChangeBackgroundColor, "ui", {
viewBackgroundColor: h.state.viewBackgroundColor,
currentItemBackgroundColor: "#ffc9c9",
});
});
expect(API.getElement(frame).backgroundColor).toBe("#ffc9c9");
expect(API.getElement(text).backgroundColor).toBe(
COLOR_PALETTE.transparent,
);
expect(
API.getElement(frame).schemaState.tracks[CORE_FRAME_SCHEMA_TRACK],
).toBe(CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK]);
});
it("should not update frame stroke width when changing stroke width in mixed selection", () => {
const frame = API.createElement({
type: "frame",
});
const rect = API.createElement({
type: "rectangle",
strokeWidth: STROKE_WIDTH.thin,
});
API.setElements([rect, frame]);
API.setSelectedElements([rect, frame]);
const originalFrameStrokeWidth = API.getElement(frame).strokeWidth;
act(() => {
h.app.actionManager.executeAction(
actionChangeStrokeWidth,
"ui",
STROKE_WIDTH.extraBold,
);
});
expect(API.getElement(rect).strokeWidth).toBe(STROKE_WIDTH.extraBold);
expect(API.getElement(frame).strokeWidth).toBe(originalFrameStrokeWidth);
});
it("should not update frame roundness when changing roundness in mixed selection", () => {
const frame = API.createElement({
type: "frame",
});
const rect = API.createElement({
type: "rectangle",
roundness: null,
});
API.setElements([rect, frame]);
API.setSelectedElements([rect, frame]);
act(() => {
h.app.actionManager.executeAction(actionChangeRoundness, "ui", "round");
});
expect(API.getElement(rect).roundness).not.toBe(null);
expect(API.getElement(frame).roundness).toBe(null);
});
}); });
}); });
@@ -45,6 +45,7 @@ import {
import { import {
isArrowElement, isArrowElement,
isBoundToContainer, isBoundToContainer,
isFrameElement,
isElbowArrow, isElbowArrow,
isLinearElement, isLinearElement,
isLineElement, isLineElement,
@@ -52,7 +53,13 @@ import {
isUsingAdaptiveRadius, isUsingAdaptiveRadius,
} from "@excalidraw/element"; } from "@excalidraw/element";
import { hasStrokeColor } from "@excalidraw/element"; import {
canChangeRoundness,
hasBackground,
hasStrokeColor,
hasStrokeStyle,
hasStrokeWidth,
} from "@excalidraw/element";
import { import {
updateElbowArrowPoints, updateElbowArrowPoints,
@@ -409,11 +416,18 @@ export const actionChangeBackgroundColor = register<
return el; return el;
}); });
} else { } else {
nextElements = changeProperty(elements, appState, (el) => nextElements = changeProperty(elements, appState, (el) => {
newElementWith(el, { if (isFrameElement(el)) {
backgroundColor: value.currentItemBackgroundColor, return newElementWith(el, {
}), backgroundColor: value.currentItemBackgroundColor,
); });
}
return hasBackground(el.type)
? newElementWith(el, {
backgroundColor: value.currentItemBackgroundColor,
})
: el;
});
} }
return { return {
@@ -444,7 +458,12 @@ export const actionChangeBackgroundColor = register<
(element) => element.backgroundColor, (element) => element.backgroundColor,
true, true,
(hasSelection) => (hasSelection) =>
!hasSelection ? appState.currentItemBackgroundColor : null, !hasSelection
? appState.activeTool.type === "frame"
? // background default shouldn't apply to new frames
"transparent"
: appState.currentItemBackgroundColor
: null,
)} )}
onChange={(color) => onChange={(color) =>
updateData({ currentItemBackgroundColor: color }) updateData({ currentItemBackgroundColor: color })
@@ -471,11 +490,13 @@ export const actionChangeFillStyle = register<ExcalidrawElement["fillStyle"]>({
})`, })`,
); );
return { return {
elements: changeProperty(elements, appState, (el) => elements: changeProperty(elements, appState, (el) => {
newElementWith(el, { return hasBackground(el.type)
fillStyle: value, ? newElementWith(el, {
}), fillStyle: value,
), })
: el;
}),
appState: { ...appState, currentItemFillStyle: value }, appState: { ...appState, currentItemFillStyle: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY, captureUpdate: CaptureUpdateAction.IMMEDIATELY,
}; };
@@ -548,11 +569,13 @@ export const actionChangeStrokeWidth = register<
trackEvent: false, trackEvent: false,
perform: (elements, appState, value) => { perform: (elements, appState, value) => {
return { return {
elements: changeProperty(elements, appState, (el) => elements: changeProperty(elements, appState, (el) => {
newElementWith(el, { return hasStrokeWidth(el.type)
strokeWidth: value, ? newElementWith(el, {
}), strokeWidth: value,
), })
: el;
}),
appState: { ...appState, currentItemStrokeWidth: value }, appState: { ...appState, currentItemStrokeWidth: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY, captureUpdate: CaptureUpdateAction.IMMEDIATELY,
}; };
@@ -604,12 +627,14 @@ export const actionChangeSloppiness = register<ExcalidrawElement["roughness"]>({
trackEvent: false, trackEvent: false,
perform: (elements, appState, value) => { perform: (elements, appState, value) => {
return { return {
elements: changeProperty(elements, appState, (el) => elements: changeProperty(elements, appState, (el) => {
newElementWith(el, { return hasStrokeStyle(el.type)
seed: randomInteger(), ? newElementWith(el, {
roughness: value, seed: randomInteger(),
}), roughness: value,
), })
: el;
}),
appState: { ...appState, currentItemRoughness: value }, appState: { ...appState, currentItemRoughness: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY, captureUpdate: CaptureUpdateAction.IMMEDIATELY,
}; };
@@ -660,11 +685,13 @@ export const actionChangeStrokeStyle = register<
trackEvent: false, trackEvent: false,
perform: (elements, appState, value) => { perform: (elements, appState, value) => {
return { return {
elements: changeProperty(elements, appState, (el) => elements: changeProperty(elements, appState, (el) => {
newElementWith(el, { return hasStrokeStyle(el.type)
strokeStyle: value, ? newElementWith(el, {
}), strokeStyle: value,
), })
: el;
}),
appState: { ...appState, currentItemStrokeStyle: value }, appState: { ...appState, currentItemStrokeStyle: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY, captureUpdate: CaptureUpdateAction.IMMEDIATELY,
}; };
@@ -1476,7 +1503,7 @@ export const actionChangeRoundness = register<"sharp" | "round">({
perform: (elements, appState, value) => { perform: (elements, appState, value) => {
return { return {
elements: changeProperty(elements, appState, (el) => { elements: changeProperty(elements, appState, (el) => {
if (isElbowArrow(el)) { if (isElbowArrow(el) || !canChangeRoundness(el.type)) {
return el; return el;
} }
@@ -1830,7 +1857,6 @@ export const actionChangeArrowType = register<keyof typeof ARROW_TYPE>({
startElement, startElement,
"start", "start",
elementsMap, elementsMap,
appState.isBindingEnabled,
), ),
} }
: null; : null;
@@ -1844,7 +1870,6 @@ export const actionChangeArrowType = register<keyof typeof ARROW_TYPE>({
endElement, endElement,
"end", "end",
elementsMap, elementsMap,
appState.isBindingEnabled,
), ),
} }
: null; : null;
@@ -1,26 +0,0 @@
import { CaptureUpdateAction } from "@excalidraw/element";
import { register } from "./register";
export const actionToggleArrowBinding = register({
name: "arrowBinding",
label: "labels.arrowBinding",
viewMode: false,
trackEvent: {
category: "canvas",
predicate: (appState) => appState.bindingPreference === "disabled",
},
perform(elements, appState) {
const newPreference =
appState.bindingPreference === "enabled" ? "disabled" : "enabled";
return {
appState: {
...appState,
bindingPreference: newPreference,
isBindingEnabled: newPreference === "enabled",
},
captureUpdate: CaptureUpdateAction.NEVER,
};
},
checked: (appState) => appState.bindingPreference === "enabled",
});
@@ -1,23 +0,0 @@
import { CaptureUpdateAction } from "@excalidraw/element";
import { register } from "./register";
export const actionToggleMidpointSnapping = register({
name: "midpointSnapping",
label: "labels.midpointSnapping",
viewMode: false,
trackEvent: {
category: "canvas",
predicate: (appState) => !appState.isMidpointSnappingEnabled,
},
perform(elements, appState) {
return {
appState: {
...appState,
isMidpointSnappingEnabled: !this.checked!(appState),
},
captureUpdate: CaptureUpdateAction.NEVER,
};
},
checked: (appState) => appState.isMidpointSnappingEnabled,
});
-2
View File
@@ -79,8 +79,6 @@ export {
export { actionToggleGridMode } from "./actionToggleGridMode"; export { actionToggleGridMode } from "./actionToggleGridMode";
export { actionToggleZenMode } from "./actionToggleZenMode"; export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode"; export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode";
export { actionToggleArrowBinding } from "./actionToggleArrowBinding";
export { actionToggleMidpointSnapping } from "./actionToggleMidpointSnapping";
export { actionToggleStats } from "./actionToggleStats"; export { actionToggleStats } from "./actionToggleStats";
export { actionUnbindText, actionBindText } from "./actionBoundText"; export { actionUnbindText, actionBindText } from "./actionBoundText";
-2
View File
@@ -59,8 +59,6 @@ export type ActionName =
| "gridMode" | "gridMode"
| "zenMode" | "zenMode"
| "objectsSnapMode" | "objectsSnapMode"
| "arrowBinding"
| "midpointSnapping"
| "stats" | "stats"
| "changeStrokeColor" | "changeStrokeColor"
| "changeBackgroundColor" | "changeBackgroundColor"
+5 -5
View File
@@ -27,6 +27,7 @@ export const getDefaultAppState = (): Omit<
showWelcomeScreen: false, showWelcomeScreen: false,
theme: THEME.LIGHT, theme: THEME.LIGHT,
collaborators: new Map(), collaborators: new Map(),
currentChartType: "bar",
currentItemBackgroundColor: DEFAULT_ELEMENT_PROPS.backgroundColor, currentItemBackgroundColor: DEFAULT_ELEMENT_PROPS.backgroundColor,
currentItemEndArrowhead: "arrow", currentItemEndArrowhead: "arrow",
currentItemFillStyle: DEFAULT_ELEMENT_PROPS.fillStyle, currentItemFillStyle: DEFAULT_ELEMENT_PROPS.fillStyle,
@@ -70,8 +71,6 @@ export const getDefaultAppState = (): Omit<
gridStep: DEFAULT_GRID_STEP, gridStep: DEFAULT_GRID_STEP,
gridModeEnabled: false, gridModeEnabled: false,
isBindingEnabled: true, isBindingEnabled: true,
bindingPreference: "enabled",
isMidpointSnappingEnabled: true,
defaultSidebarDockedPreference: false, defaultSidebarDockedPreference: false,
isLoading: false, isLoading: false,
isResizing: false, isResizing: false,
@@ -84,6 +83,7 @@ export const getDefaultAppState = (): Omit<
openPopup: null, openPopup: null,
openSidebar: null, openSidebar: null,
openDialog: null, openDialog: null,
pasteDialog: { shown: false, data: null },
previousSelectedElementIds: {}, previousSelectedElementIds: {},
resizingElement: null, resizingElement: null,
scrolledOutside: false, scrolledOutside: false,
@@ -150,6 +150,7 @@ const APP_STATE_STORAGE_CONF = (<
showWelcomeScreen: { browser: true, export: false, server: false }, showWelcomeScreen: { browser: true, export: false, server: false },
theme: { browser: true, export: false, server: false }, theme: { browser: true, export: false, server: false },
collaborators: { browser: false, export: false, server: false }, collaborators: { browser: false, export: false, server: false },
currentChartType: { browser: true, export: false, server: false },
currentItemBackgroundColor: { browser: true, export: false, server: false }, currentItemBackgroundColor: { browser: true, export: false, server: false },
currentItemEndArrowhead: { browser: true, export: false, server: false }, currentItemEndArrowhead: { browser: true, export: false, server: false },
currentItemFillStyle: { browser: true, export: false, server: false }, currentItemFillStyle: { browser: true, export: false, server: false },
@@ -192,9 +193,7 @@ const APP_STATE_STORAGE_CONF = (<
gridStep: { browser: true, export: true, server: true }, gridStep: { browser: true, export: true, server: true },
gridModeEnabled: { browser: true, export: true, server: true }, gridModeEnabled: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false }, height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: true, export: false, server: false }, isBindingEnabled: { browser: false, export: false, server: false },
bindingPreference: { browser: true, export: false, server: false },
isMidpointSnappingEnabled: { browser: true, export: false, server: false },
defaultSidebarDockedPreference: { defaultSidebarDockedPreference: {
browser: true, browser: true,
export: false, export: false,
@@ -213,6 +212,7 @@ const APP_STATE_STORAGE_CONF = (<
openPopup: { browser: false, export: false, server: false }, openPopup: { browser: false, export: false, server: false },
openSidebar: { browser: true, export: false, server: false }, openSidebar: { browser: true, export: false, server: false },
openDialog: { browser: false, export: false, server: false }, openDialog: { browser: false, export: false, server: false },
pasteDialog: { browser: false, export: false, server: false },
previousSelectedElementIds: { browser: true, export: false, server: false }, previousSelectedElementIds: { browser: true, export: false, server: false },
resizingElement: { browser: false, export: false, server: false }, resizingElement: { browser: false, export: false, server: false },
scrolledOutside: { browser: true, export: false, server: false }, scrolledOutside: { browser: true, export: false, server: false },
File diff suppressed because it is too large Load Diff
+481
View File
@@ -0,0 +1,481 @@
import { pointFrom } from "@excalidraw/math";
import {
COLOR_PALETTE,
DEFAULT_CHART_COLOR_INDEX,
getAllColorsSpecificShade,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
VERTICAL_ALIGN,
randomId,
isDevEnv,
FONT_SIZES,
} from "@excalidraw/common";
import {
newTextElement,
newLinearElement,
newElement,
} from "@excalidraw/element";
import type { Radians } from "@excalidraw/math";
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
export type ChartElements = readonly NonDeletedExcalidrawElement[];
const BAR_WIDTH = 32;
const BAR_GAP = 12;
const BAR_HEIGHT = 256;
const GRID_OPACITY = 50;
export interface Spreadsheet {
title: string | null;
labels: string[] | null;
values: number[];
}
export const NOT_SPREADSHEET = "NOT_SPREADSHEET";
export const VALID_SPREADSHEET = "VALID_SPREADSHEET";
type ParseSpreadsheetResult =
| { type: typeof NOT_SPREADSHEET; reason: string }
| { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet };
/**
* @private exported for testing
*/
export const tryParseNumber = (s: string): number | null => {
const match = /^([-+]?)[$€£¥₩]?([-+]?)([\d.,]+)[%]?$/.exec(s);
if (!match) {
return null;
}
return parseFloat(`${(match[1] || match[2]) + match[3]}`.replace(/,/g, ""));
};
const isNumericColumn = (lines: string[][], columnIndex: number) =>
lines.slice(1).every((line) => tryParseNumber(line[columnIndex]) !== null);
/**
* @private exported for testing
*/
export const tryParseCells = (cells: string[][]): ParseSpreadsheetResult => {
const numCols = cells[0].length;
if (numCols > 2) {
return { type: NOT_SPREADSHEET, reason: "More than 2 columns" };
}
if (numCols === 1) {
if (!isNumericColumn(cells, 0)) {
return { type: NOT_SPREADSHEET, reason: "Value is not numeric" };
}
const hasHeader = tryParseNumber(cells[0][0]) === null;
const values = (hasHeader ? cells.slice(1) : cells).map((line) =>
tryParseNumber(line[0]),
);
if (values.length < 2) {
return { type: NOT_SPREADSHEET, reason: "Less than two rows" };
}
return {
type: VALID_SPREADSHEET,
spreadsheet: {
title: hasHeader ? cells[0][0] : null,
labels: null,
values: values as number[],
},
};
}
const labelColumnNumeric = isNumericColumn(cells, 0);
const valueColumnNumeric = isNumericColumn(cells, 1);
if (!labelColumnNumeric && !valueColumnNumeric) {
return { type: NOT_SPREADSHEET, reason: "Value is not numeric" };
}
const [labelColumnIndex, valueColumnIndex] = valueColumnNumeric
? [0, 1]
: [1, 0];
const hasHeader = tryParseNumber(cells[0][valueColumnIndex]) === null;
const rows = hasHeader ? cells.slice(1) : cells;
if (rows.length < 2) {
return { type: NOT_SPREADSHEET, reason: "Less than 2 rows" };
}
return {
type: VALID_SPREADSHEET,
spreadsheet: {
title: hasHeader ? cells[0][valueColumnIndex] : null,
labels: rows.map((row) => row[labelColumnIndex]),
values: rows.map((row) => tryParseNumber(row[valueColumnIndex])!),
},
};
};
const transposeCells = (cells: string[][]) => {
const nextCells: string[][] = [];
for (let col = 0; col < cells[0].length; col++) {
const nextCellRow: string[] = [];
for (let row = 0; row < cells.length; row++) {
nextCellRow.push(cells[row][col]);
}
nextCells.push(nextCellRow);
}
return nextCells;
};
export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
// Copy/paste from excel, spreadsheets, tsv, csv.
// For now we only accept 2 columns with an optional header
// Check for tab separated values
let lines = text
.trim()
.split("\n")
.map((line) => line.trim().split("\t"));
// Check for comma separated files
if (lines.length && lines[0].length !== 2) {
lines = text
.trim()
.split("\n")
.map((line) => line.trim().split(","));
}
if (lines.length === 0) {
return { type: NOT_SPREADSHEET, reason: "No values" };
}
const numColsFirstLine = lines[0].length;
const isSpreadsheet = lines.every((line) => line.length === numColsFirstLine);
if (!isSpreadsheet) {
return {
type: NOT_SPREADSHEET,
reason: "All rows don't have same number of columns",
};
}
const result = tryParseCells(lines);
if (result.type !== VALID_SPREADSHEET) {
const transposedResults = tryParseCells(transposeCells(lines));
if (transposedResults.type === VALID_SPREADSHEET) {
return transposedResults;
}
}
return result;
};
const bgColors = getAllColorsSpecificShade(DEFAULT_CHART_COLOR_INDEX);
// Put all the common properties here so when the whole chart is selected
// the properties dialog shows the correct selected values
const commonProps = {
fillStyle: "hachure",
fontFamily: DEFAULT_FONT_FAMILY,
fontSize: DEFAULT_FONT_SIZE,
opacity: 100,
roughness: 1,
strokeColor: COLOR_PALETTE.black,
roundness: null,
strokeStyle: "solid",
strokeWidth: 1,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
locked: false,
} as const;
const getChartDimensions = (spreadsheet: Spreadsheet) => {
const chartWidth =
(BAR_WIDTH + BAR_GAP) * spreadsheet.values.length + BAR_GAP;
const chartHeight = BAR_HEIGHT + BAR_GAP * 2;
return { chartWidth, chartHeight };
};
const chartXLabels = (
spreadsheet: Spreadsheet,
x: number,
y: number,
groupId: string,
backgroundColor: string,
): ChartElements => {
return (
spreadsheet.labels?.map((label, index) => {
return newTextElement({
groupIds: [groupId],
backgroundColor,
...commonProps,
text: label.length > 8 ? `${label.slice(0, 5)}...` : label,
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP * 2,
y: y + BAR_GAP / 2,
width: BAR_WIDTH,
angle: 5.87 as Radians,
fontSize: FONT_SIZES.sm,
textAlign: "center",
verticalAlign: "top",
});
}) || []
);
};
const chartYLabels = (
spreadsheet: Spreadsheet,
x: number,
y: number,
groupId: string,
backgroundColor: string,
): ChartElements => {
const minYLabel = newTextElement({
groupIds: [groupId],
backgroundColor,
...commonProps,
x: x - BAR_GAP,
y: y - BAR_GAP,
text: "0",
textAlign: "right",
});
const maxYLabel = newTextElement({
groupIds: [groupId],
backgroundColor,
...commonProps,
x: x - BAR_GAP,
y: y - BAR_HEIGHT - minYLabel.height / 2,
text: Math.max(...spreadsheet.values).toLocaleString(),
textAlign: "right",
});
return [minYLabel, maxYLabel];
};
const chartLines = (
spreadsheet: Spreadsheet,
x: number,
y: number,
groupId: string,
backgroundColor: string,
): ChartElements => {
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet);
const xLine = newLinearElement({
backgroundColor,
groupIds: [groupId],
...commonProps,
type: "line",
x,
y,
width: chartWidth,
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
});
const yLine = newLinearElement({
backgroundColor,
groupIds: [groupId],
...commonProps,
type: "line",
x,
y,
height: chartHeight,
points: [pointFrom(0, 0), pointFrom(0, -chartHeight)],
});
const maxLine = newLinearElement({
backgroundColor,
groupIds: [groupId],
...commonProps,
type: "line",
x,
y: y - BAR_HEIGHT - BAR_GAP,
strokeStyle: "dotted",
width: chartWidth,
opacity: GRID_OPACITY,
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
});
return [xLine, yLine, maxLine];
};
// For the maths behind it https://excalidraw.com/#json=6320864370884608,O_5xfD-Agh32tytHpRJx1g
const chartBaseElements = (
spreadsheet: Spreadsheet,
x: number,
y: number,
groupId: string,
backgroundColor: string,
debug?: boolean,
): ChartElements => {
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet);
const title = spreadsheet.title
? newTextElement({
backgroundColor,
groupIds: [groupId],
...commonProps,
text: spreadsheet.title,
x: x + chartWidth / 2,
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
roundness: null,
textAlign: "center",
})
: null;
const debugRect = debug
? newElement({
backgroundColor,
groupIds: [groupId],
...commonProps,
type: "rectangle",
x,
y: y - chartHeight,
width: chartWidth,
height: chartHeight,
strokeColor: COLOR_PALETTE.black,
fillStyle: "solid",
opacity: 6,
})
: null;
return [
...(debugRect ? [debugRect] : []),
...(title ? [title] : []),
...chartXLabels(spreadsheet, x, y, groupId, backgroundColor),
...chartYLabels(spreadsheet, x, y, groupId, backgroundColor),
...chartLines(spreadsheet, x, y, groupId, backgroundColor),
];
};
const chartTypeBar = (
spreadsheet: Spreadsheet,
x: number,
y: number,
): ChartElements => {
const max = Math.max(...spreadsheet.values);
const groupId = randomId();
const backgroundColor = bgColors[Math.floor(Math.random() * bgColors.length)];
const bars = spreadsheet.values.map((value, index) => {
const barHeight = (value / max) * BAR_HEIGHT;
return newElement({
backgroundColor,
groupIds: [groupId],
...commonProps,
type: "rectangle",
x: x + index * (BAR_WIDTH + BAR_GAP) + BAR_GAP,
y: y - barHeight - BAR_GAP,
width: BAR_WIDTH,
height: barHeight,
});
});
return [
...bars,
...chartBaseElements(
spreadsheet,
x,
y,
groupId,
backgroundColor,
isDevEnv(),
),
];
};
const chartTypeLine = (
spreadsheet: Spreadsheet,
x: number,
y: number,
): ChartElements => {
const max = Math.max(...spreadsheet.values);
const groupId = randomId();
const backgroundColor = bgColors[Math.floor(Math.random() * bgColors.length)];
let index = 0;
const points = [];
for (const value of spreadsheet.values) {
const cx = index * (BAR_WIDTH + BAR_GAP);
const cy = -(value / max) * BAR_HEIGHT;
points.push([cx, cy]);
index++;
}
const maxX = Math.max(...points.map((element) => element[0]));
const maxY = Math.max(...points.map((element) => element[1]));
const minX = Math.min(...points.map((element) => element[0]));
const minY = Math.min(...points.map((element) => element[1]));
const line = newLinearElement({
backgroundColor,
groupIds: [groupId],
...commonProps,
type: "line",
x: x + BAR_GAP + BAR_WIDTH / 2,
y: y - BAR_GAP,
height: maxY - minY,
width: maxX - minX,
strokeWidth: 2,
points: points as any,
});
const dots = spreadsheet.values.map((value, index) => {
const cx = index * (BAR_WIDTH + BAR_GAP) + BAR_GAP / 2;
const cy = -(value / max) * BAR_HEIGHT + BAR_GAP / 2;
return newElement({
backgroundColor,
groupIds: [groupId],
...commonProps,
fillStyle: "solid",
strokeWidth: 2,
type: "ellipse",
x: x + cx + BAR_WIDTH / 2,
y: y + cy - BAR_GAP * 2,
width: BAR_GAP,
height: BAR_GAP,
});
});
const lines = spreadsheet.values.map((value, index) => {
const cx = index * (BAR_WIDTH + BAR_GAP) + BAR_GAP / 2;
const cy = (value / max) * BAR_HEIGHT + BAR_GAP / 2 + BAR_GAP;
return newLinearElement({
backgroundColor,
groupIds: [groupId],
...commonProps,
type: "line",
x: x + cx + BAR_WIDTH / 2 + BAR_GAP / 2,
y: y - cy,
height: cy,
strokeStyle: "dotted",
opacity: GRID_OPACITY,
points: [pointFrom(0, 0), pointFrom(0, cy)],
});
});
return [
...chartBaseElements(
spreadsheet,
x,
y,
groupId,
backgroundColor,
isDevEnv(),
),
line,
...lines,
...dots,
];
};
export const renderSpreadsheet = (
chartType: string,
spreadsheet: Spreadsheet,
x: number,
y: number,
): ChartElements => {
if (chartType === "line") {
return chartTypeLine(spreadsheet, x, y);
}
return chartTypeBar(spreadsheet, x, y);
};
-103
View File
@@ -1,103 +0,0 @@
import { isDevEnv } from "@excalidraw/common";
import { newElement } from "@excalidraw/element";
import { commonProps } from "./charts.constants";
import {
chartBaseElements,
chartXLabels,
createSeriesLegend,
getBackgroundColor,
getCartesianChartLayout,
getChartDimensions,
getColorOffset,
getRotatedTextElementBottom,
getSeriesColors,
} from "./charts.helpers";
import type { ChartElements, Spreadsheet } from "./charts.types";
export const renderBarChart = (
spreadsheet: Spreadsheet,
x: number,
y: number,
colorSeed?: number,
): ChartElements => {
const series = spreadsheet.series;
const layout = getCartesianChartLayout("bar", series.length);
const max = Math.max(
1,
...series.flatMap((seriesData) =>
seriesData.values.map((value) => Math.max(0, value)),
),
);
const colorOffset = getColorOffset(colorSeed);
const backgroundColor = getBackgroundColor(colorOffset);
const seriesColors = getSeriesColors(series.length, colorOffset);
const interBarGap =
series.length > 1
? Math.max(1, Math.floor(layout.gap / (series.length + 1)))
: 0;
const barWidth =
series.length > 1
? Math.max(
2,
(layout.slotWidth - interBarGap * (series.length - 1)) /
series.length,
)
: layout.slotWidth;
const clusterWidth =
series.length * barWidth + interBarGap * (series.length - 1);
const clusterOffset = (layout.slotWidth - clusterWidth) / 2;
const bars = series[0].values.flatMap((_, categoryIndex) =>
series.map((seriesData, seriesIndex) => {
const value = Math.max(0, seriesData.values[categoryIndex] ?? 0);
const barHeight = (value / max) * layout.chartHeight;
const barColor =
series.length > 1 ? seriesColors[seriesIndex] : backgroundColor;
return newElement({
backgroundColor: barColor,
...commonProps,
type: "rectangle",
fillStyle: series.length > 1 ? "solid" : commonProps.fillStyle,
strokeColor: series.length > 1 ? barColor : commonProps.strokeColor,
x:
x +
categoryIndex * (layout.slotWidth + layout.gap) +
layout.gap +
clusterOffset +
seriesIndex * (barWidth + interBarGap),
y: y - barHeight - layout.gap,
width: barWidth,
height: barHeight,
});
}),
);
const baseElements = chartBaseElements(
spreadsheet,
x,
y,
backgroundColor,
layout,
max,
isDevEnv(),
);
const xLabels = chartXLabels(spreadsheet, x, y, backgroundColor, layout);
const xLabelsBottomY = Math.max(
y + layout.gap / 2,
...xLabels.map((label) => getRotatedTextElementBottom(label)),
);
const { chartWidth } = getChartDimensions(spreadsheet, layout);
const seriesLegend = createSeriesLegend(
series,
seriesColors,
x + chartWidth / 2,
xLabelsBottomY,
y + layout.gap * 5,
backgroundColor,
);
return [...baseElements, ...bars, ...seriesLegend];
};
@@ -1,63 +0,0 @@
import {
COLOR_PALETTE,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
VERTICAL_ALIGN,
} from "@excalidraw/common";
import type { Radians } from "@excalidraw/math";
export const CARTESIAN_BASE_SLOT_WIDTH = 44;
export const CARTESIAN_BAR_SLOT_EXTRA_PER_SERIES = 22;
export const CARTESIAN_BAR_SLOT_EXTRA_MAX = 66;
export const CARTESIAN_LINE_SLOT_WIDTH = 48;
export const CARTESIAN_GAP = 14;
export const CARTESIAN_BAR_HEIGHT = 304;
export const CARTESIAN_LINE_HEIGHT = 320;
export const CARTESIAN_LABEL_ROTATION = 5.87 as Radians;
export const CARTESIAN_LABEL_MIN_WIDTH = 28;
export const CARTESIAN_LABEL_SLOT_PADDING = 4;
export const CARTESIAN_LABEL_AXIS_CLEARANCE = 2;
export const CARTESIAN_LABEL_MAX_WIDTH_BUFFER = 10;
export const CARTESIAN_LABEL_ROTATED_WIDTH_BUFFER = 10;
export const CARTESIAN_LABEL_OVERFLOW_PREFERENCE_BUFFER = 8;
export const BAR_GAP = 12;
export const BAR_HEIGHT = 256;
export const GRID_OPACITY = 10;
export const RADAR_GRID_LEVELS = 4;
export const RADAR_LABEL_OFFSET = BAR_GAP * 2;
export const RADAR_PADDING = BAR_GAP * 2;
export const RADAR_SINGLE_SERIES_LOG_SCALE_THRESHOLD = 100;
export const RADAR_AXIS_LABEL_MAX_WIDTH = 140;
export const RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD = 0.35;
export const RADAR_AXIS_LABEL_CLEARANCE = BAR_GAP / 2;
export const RADAR_LEGEND_SWATCH_SIZE = 20;
export const RADAR_LEGEND_ITEM_GAP = BAR_GAP * 2;
export const RADAR_LEGEND_TEXT_GAP = BAR_GAP;
// Put all common chart element properties here so properties dialog
// shows stable values when selecting chart groups.
export const commonProps = {
fillStyle: "hachure",
fontFamily: DEFAULT_FONT_FAMILY,
fontSize: DEFAULT_FONT_SIZE,
opacity: 100,
roughness: 1,
strokeColor: COLOR_PALETTE.black,
roundness: null,
strokeStyle: "solid",
strokeWidth: 1,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
locked: false,
} as const;
export type CartesianChartType = "bar" | "line";
export type CartesianChartLayout = {
slotWidth: number;
gap: number;
chartHeight: number;
xLabelMaxWidth: number;
};
@@ -1,865 +0,0 @@
import { pointFrom } from "@excalidraw/math";
import {
COLOR_PALETTE,
DEFAULT_CHART_COLOR_INDEX,
FONT_FAMILY,
FONT_SIZES,
ROUNDNESS,
DEFAULT_FONT_SIZE,
getAllColorsSpecificShade,
getFontString,
getLineHeight,
ROUGHNESS,
} from "@excalidraw/common";
import {
getApproxMinLineWidth,
measureText,
newElement,
newLinearElement,
newTextElement,
wrapText,
} from "@excalidraw/element";
import type {
ChartType,
ExcalidrawTextElement,
} from "@excalidraw/element/types";
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
import {
BAR_GAP,
CARTESIAN_BAR_HEIGHT,
CARTESIAN_BASE_SLOT_WIDTH,
CARTESIAN_BAR_SLOT_EXTRA_MAX,
CARTESIAN_BAR_SLOT_EXTRA_PER_SERIES,
CARTESIAN_GAP,
CARTESIAN_LABEL_AXIS_CLEARANCE,
CARTESIAN_LABEL_MAX_WIDTH_BUFFER,
CARTESIAN_LABEL_MIN_WIDTH,
CARTESIAN_LABEL_OVERFLOW_PREFERENCE_BUFFER,
CARTESIAN_LABEL_ROTATED_WIDTH_BUFFER,
CARTESIAN_LABEL_ROTATION,
CARTESIAN_LABEL_SLOT_PADDING,
CARTESIAN_LINE_HEIGHT,
CARTESIAN_LINE_SLOT_WIDTH,
GRID_OPACITY,
RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD,
RADAR_AXIS_LABEL_CLEARANCE,
RADAR_AXIS_LABEL_MAX_WIDTH,
RADAR_LABEL_OFFSET,
RADAR_LEGEND_ITEM_GAP,
RADAR_LEGEND_SWATCH_SIZE,
RADAR_LEGEND_TEXT_GAP,
RADAR_PADDING,
RADAR_SINGLE_SERIES_LOG_SCALE_THRESHOLD,
BAR_HEIGHT,
commonProps,
type CartesianChartLayout,
type CartesianChartType,
} from "./charts.constants";
import type {
ChartElements,
Spreadsheet,
SpreadsheetSeries,
} from "./charts.types";
const bgColors = getAllColorsSpecificShade(DEFAULT_CHART_COLOR_INDEX);
const getSpreadsheetDimensionCount = (spreadsheet: Spreadsheet) =>
spreadsheet.labels?.length ?? spreadsheet.series[0]?.values.length ?? 0;
export const isSpreadsheetValidForChartType = (
spreadsheet: Spreadsheet | null,
chartType: ChartType,
) => {
if (!spreadsheet) {
return false;
}
const dimensionCount = getSpreadsheetDimensionCount(spreadsheet);
if (dimensionCount < 2) {
return false;
}
if (chartType === "radar") {
return dimensionCount >= 3;
}
return true;
};
const getSeriesAwareSlotWidth = (
baseSlotWidth: number,
seriesCount: number,
) => {
const extraSlotWidth =
seriesCount <= 1
? 0
: Math.min(
CARTESIAN_BAR_SLOT_EXTRA_MAX,
(seriesCount - 1) * CARTESIAN_BAR_SLOT_EXTRA_PER_SERIES,
);
return baseSlotWidth + extraSlotWidth;
};
export const getCartesianChartLayout = (
chartType: CartesianChartType,
seriesCount: number,
): CartesianChartLayout => {
if (chartType === "line") {
const slotWidth = getSeriesAwareSlotWidth(
CARTESIAN_LINE_SLOT_WIDTH,
seriesCount,
);
return {
slotWidth,
gap: CARTESIAN_GAP,
chartHeight: CARTESIAN_LINE_HEIGHT,
xLabelMaxWidth:
slotWidth + CARTESIAN_GAP * 3 + CARTESIAN_LABEL_MAX_WIDTH_BUFFER,
};
}
const slotWidth = getSeriesAwareSlotWidth(
CARTESIAN_BASE_SLOT_WIDTH,
seriesCount,
);
return {
slotWidth,
gap: CARTESIAN_GAP,
chartHeight: CARTESIAN_BAR_HEIGHT,
xLabelMaxWidth:
slotWidth + CARTESIAN_GAP * 3 + CARTESIAN_LABEL_MAX_WIDTH_BUFFER,
};
};
export const getChartDimensions = (
spreadsheet: Spreadsheet,
layout: CartesianChartLayout,
) => {
const chartWidth =
(layout.slotWidth + layout.gap) * spreadsheet.series[0].values.length +
layout.gap;
const chartHeight = layout.chartHeight + layout.gap * 2;
return { chartWidth, chartHeight };
};
export const getRadarDimensions = () => {
const chartWidth = BAR_HEIGHT + RADAR_PADDING * 2;
const chartHeight = BAR_HEIGHT + RADAR_PADDING * 2;
return { chartWidth, chartHeight };
};
const getCircularDistance = (
firstIndex: number,
secondIndex: number,
paletteSize: number,
) => {
const absoluteDistance = Math.abs(firstIndex - secondIndex);
return Math.min(absoluteDistance, paletteSize - absoluteDistance);
};
export const getSeriesColors = (
seriesCount: number,
colorOffset: number,
): readonly string[] => {
if (seriesCount <= 0 || bgColors.length === 0) {
return [];
}
const paletteSize = bgColors.length;
const startIndex = ((colorOffset % paletteSize) + paletteSize) % paletteSize;
const selectedIndices = [startIndex];
const maxUniqueColors = Math.min(seriesCount, paletteSize);
const availableIndices = new Set(
Array.from({ length: paletteSize }, (_, index) => index).filter(
(index) => index !== startIndex,
),
);
while (selectedIndices.length < maxUniqueColors) {
let bestIndex = -1;
let bestMinDistance = -1;
let bestAverageDistance = -1;
for (const candidateIndex of availableIndices) {
const distances = selectedIndices.map((selectedIndex) =>
getCircularDistance(candidateIndex, selectedIndex, paletteSize),
);
const minDistance = Math.min(...distances);
const averageDistance =
distances.reduce((total, distance) => total + distance, 0) /
distances.length;
if (
minDistance > bestMinDistance ||
(minDistance === bestMinDistance &&
averageDistance > bestAverageDistance)
) {
bestIndex = candidateIndex;
bestMinDistance = minDistance;
bestAverageDistance = averageDistance;
}
}
selectedIndices.push(bestIndex);
availableIndices.delete(bestIndex);
}
return Array.from(
{ length: seriesCount },
(_, index) => bgColors[selectedIndices[index % selectedIndices.length]],
);
};
export const getColorOffset = (colorSeed?: number) => {
if (bgColors.length === 0) {
return 0;
}
if (typeof colorSeed !== "number" || !Number.isFinite(colorSeed)) {
return Math.floor(Math.random() * bgColors.length);
}
const seedText = colorSeed.toString();
let hash = 0;
for (let index = 0; index < seedText.length; index++) {
hash = (hash * 31 + seedText.charCodeAt(index)) | 0;
}
return Math.abs(hash) % bgColors.length;
};
export const getBackgroundColor = (colorOffset: number) =>
bgColors[colorOffset];
export const getRadarValueScale = (
series: SpreadsheetSeries[],
_labelsLength: number,
) => {
const allValues = series.flatMap((s) =>
s.values.map((value) => Math.max(0, value)),
);
const positiveValues = allValues.filter((value) => value > 0);
const max = Math.max(1, ...allValues);
const minPositive =
positiveValues.length > 0 ? Math.min(...positiveValues) : 1;
const useLogScale =
series.length === 1 &&
minPositive > 0 &&
max / minPositive >= RADAR_SINGLE_SERIES_LOG_SCALE_THRESHOLD;
return {
renderSteps: false,
normalize: (value: number, _axisIndex: number) => {
const safeValue = Math.max(0, value);
return useLogScale
? Math.log10(safeValue + 1) / Math.log10(max + 1)
: safeValue / max;
},
};
};
const shouldWrapRadarText = (text: string) => /\s/.test(text.trim());
export const getRadarDisplayText = (
text: string,
fontString: ReturnType<typeof getFontString>,
maxWidth: number,
) => {
return shouldWrapRadarText(text)
? wrapText(text, fontString, maxWidth)
: text;
};
export const createRadarAxisLabels = (
labels: readonly string[],
angles: readonly number[],
centerX: number,
centerY: number,
radius: number,
backgroundColor: string,
): {
axisLabels: ChartElements;
axisLabelTopY: number;
axisLabelBottomY: number;
} => {
const fontFamily = FONT_FAMILY.Excalifont;
const fontSize = FONT_SIZES.sm;
const lineHeight = getLineHeight(fontFamily);
const fontString = getFontString({ fontFamily, fontSize });
const baseLabelWidth = Math.min(
RADAR_AXIS_LABEL_MAX_WIDTH,
radius * (labels.length > 8 ? 0.56 : 0.72),
);
const minLabelWidth = getApproxMinLineWidth(fontString, lineHeight);
const axisLabels = labels.map((label, index) => {
const angle = angles[index];
const longestWordWidth = Math.max(
0,
...label
.trim()
.split(/\s+/)
.filter(Boolean)
.map((word) => measureText(word, fontString, lineHeight).width),
);
const maxLabelWidth = Math.max(
minLabelWidth,
baseLabelWidth,
longestWordWidth,
);
const displayLabel = getRadarDisplayText(label, fontString, maxLabelWidth);
const metrics = measureText(displayLabel, fontString, lineHeight);
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const textAlign: "left" | "center" | "right" =
cos > RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD
? "left"
: cos < -RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD
? "right"
: "center";
// Keep labels outside the radar ring by projecting text extents
// onto the axis direction.
const centerAlignedXExtent = textAlign === "center" ? metrics.width / 2 : 0;
const projectedExtent =
Math.abs(cos) * centerAlignedXExtent +
Math.abs(sin) * (metrics.height / 2);
const radialOffset =
RADAR_LABEL_OFFSET + projectedExtent + RADAR_AXIS_LABEL_CLEARANCE;
const anchorX = centerX + cos * (radius + radialOffset);
const anchorY = centerY + sin * (radius + radialOffset);
const yNudge =
sin > RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD
? BAR_GAP / 3
: sin < -RADAR_AXIS_LABEL_ALIGNMENT_THRESHOLD
? -BAR_GAP / 3
: 0;
return newTextElement({
backgroundColor,
...commonProps,
text: displayLabel,
originalText: label,
x: anchorX,
y: anchorY + yNudge,
fontFamily,
fontSize,
lineHeight,
textAlign,
verticalAlign: "middle",
});
});
const axisLabelTopY = Math.min(...axisLabels.map((axisLabel) => axisLabel.y));
const axisLabelBottomY = Math.max(
...axisLabels.map((axisLabel) => axisLabel.y + axisLabel.height),
);
return { axisLabels, axisLabelTopY, axisLabelBottomY };
};
export const createSeriesLegend = (
series: SpreadsheetSeries[],
seriesColors: readonly string[],
centerX: number,
minLegendTopY: number,
fallbackLegendY: number,
backgroundColor: string,
): ChartElements => {
if (series.length <= 1) {
return [];
}
const fontFamily = FONT_FAMILY["Lilita One"];
const fontSize = FONT_SIZES.lg;
const lineHeight = getLineHeight(fontFamily);
const fontString = getFontString({ fontFamily, fontSize });
const legendItems = series.map((seriesItem, index) => {
const label = seriesItem.title?.trim() || `Series ${index + 1}`;
const displayLabel = getRadarDisplayText(label, fontString, BAR_HEIGHT);
const metrics = measureText(displayLabel, fontString, lineHeight);
const itemWidth =
RADAR_LEGEND_SWATCH_SIZE + RADAR_LEGEND_TEXT_GAP + metrics.width;
return {
label,
displayLabel,
color: seriesColors[index],
width: itemWidth,
height: metrics.height,
};
});
const maxLegendHalfHeight = Math.max(
RADAR_LEGEND_SWATCH_SIZE / 2,
...legendItems.map((item) => item.height / 2),
);
const legendY = Math.max(
fallbackLegendY,
minLegendTopY + maxLegendHalfHeight + RADAR_LABEL_OFFSET,
);
const pillPaddingX = RADAR_LEGEND_ITEM_GAP;
const pillPaddingY = RADAR_LEGEND_SWATCH_SIZE * 0.6;
const totalLegendWidth =
legendItems.reduce((total, item) => total + item.width, 0) +
RADAR_LEGEND_ITEM_GAP * Math.max(0, legendItems.length - 1);
const pillWidth = totalLegendWidth + pillPaddingX * 2;
const pillHeight = maxLegendHalfHeight * 2 + pillPaddingY * 2;
const legendElements: NonDeletedExcalidrawElement[] = [];
// rounded pill background
legendElements.push(
newElement({
...commonProps,
backgroundColor: "transparent",
type: "rectangle",
fillStyle: "solid",
strokeColor: COLOR_PALETTE.black,
x: centerX - pillWidth / 2,
y: legendY - pillHeight / 2,
width: pillWidth,
height: pillHeight,
roughness: ROUGHNESS.architect,
roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
}),
);
let cursorX = centerX - totalLegendWidth / 2;
legendItems.forEach((item) => {
// solid filled swatch
legendElements.push(
newElement({
...commonProps,
backgroundColor: item.color,
type: "rectangle",
x: cursorX,
y: legendY - RADAR_LEGEND_SWATCH_SIZE / 2,
width: RADAR_LEGEND_SWATCH_SIZE,
height: RADAR_LEGEND_SWATCH_SIZE,
fillStyle: "solid",
strokeColor: item.color,
roughness: ROUGHNESS.architect,
roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
}),
);
// label in default (black) color
legendElements.push(
newTextElement({
...commonProps,
text: item.displayLabel,
originalText: item.label,
autoResize: false,
x: cursorX + RADAR_LEGEND_SWATCH_SIZE + RADAR_LEGEND_TEXT_GAP,
y: legendY,
fontFamily,
fontSize,
lineHeight,
textAlign: "left",
verticalAlign: "middle",
}),
);
cursorX += item.width + RADAR_LEGEND_ITEM_GAP;
});
return legendElements;
};
const ellipsifyTextToWidth = (
text: string,
maxWidth: number,
fontString: ReturnType<typeof getFontString>,
lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
if (measureText(text, fontString, lineHeight).width <= maxWidth) {
return text;
}
let end = text.length;
while (end > 1) {
const candidate = `${text.slice(0, end)}...`;
if (measureText(candidate, fontString, lineHeight).width <= maxWidth) {
return candidate;
}
end--;
}
return text[0] ? `${text[0]}...` : text;
};
const wrapOrEllipsifyTextToWidth = (
text: string,
maxWidth: number,
fontString: ReturnType<typeof getFontString>,
lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
if (measureText(text, fontString, lineHeight).width <= maxWidth) {
return { wrapped: false, text };
}
const words = text.trim().split(/\s+/).filter(Boolean);
if (words.length > 1) {
const hasLongWord = words.some((word) => {
return measureText(word, fontString, lineHeight).width > maxWidth;
});
if (
!hasLongWord &&
maxWidth >= getApproxMinLineWidth(fontString, lineHeight)
) {
return { wrapped: true, text: wrapText(text, fontString, maxWidth) };
}
}
return {
wrapped: false,
text: ellipsifyTextToWidth(text, maxWidth, fontString, lineHeight),
};
};
const getRotatedBoundingBox = (
width: number,
height: number,
angle: number,
) => {
const cos = Math.abs(Math.cos(angle));
const sin = Math.abs(Math.sin(angle));
return {
width: width * cos + height * sin,
height: width * sin + height * cos,
};
};
type CartesianAxisLabelSpec = {
originalText: string;
text: string;
wrapped: boolean;
metrics: ReturnType<typeof measureText>;
rotatedWidth: number;
rotatedHeight: number;
};
const isEllipsifiedLabel = (text: string) => text.includes("...");
const getCartesianAxisLabelSpec = (
label: string,
maxLabelWidth: number,
maxRotatedWidth: number,
fontString: ReturnType<typeof getFontString>,
lineHeight: ExcalidrawTextElement["lineHeight"],
): CartesianAxisLabelSpec => {
const minWidth = Math.max(
CARTESIAN_LABEL_MIN_WIDTH,
Math.ceil(getApproxMinLineWidth(fontString, lineHeight)),
);
const maxWidth = Math.max(minWidth, Math.floor(maxLabelWidth));
const candidateWidths: number[] = [];
for (let width = maxWidth; width >= minWidth; width -= 4) {
candidateWidths.push(width);
}
if (candidateWidths[candidateWidths.length - 1] !== minWidth) {
candidateWidths.push(minWidth);
}
const getRank = (spec: CartesianAxisLabelSpec) => {
const ellipsified = isEllipsifiedLabel(spec.text);
const visibleChars = spec.text
.replace(/\.\.\./g, "")
.replace(/\n/g, "").length;
const lineCount = spec.text.split("\n").length;
return {
ellipsified,
visibleChars,
lineCount,
};
};
const shouldPrefer = (
candidate: CartesianAxisLabelSpec,
current: CartesianAxisLabelSpec,
) => {
const candidateRank = getRank(candidate);
const currentRank = getRank(current);
if (candidateRank.ellipsified !== currentRank.ellipsified) {
return !candidateRank.ellipsified;
}
if (candidateRank.visibleChars !== currentRank.visibleChars) {
return candidateRank.visibleChars > currentRank.visibleChars;
}
if (candidateRank.lineCount !== currentRank.lineCount) {
return candidateRank.lineCount < currentRank.lineCount;
}
return candidate.rotatedHeight < current.rotatedHeight;
};
let bestFit: CartesianAxisLabelSpec | null = null;
let bestOverflowAny: {
overflow: number;
spec: CartesianAxisLabelSpec;
} | null = null;
let bestOverflowNonEllipsified: {
overflow: number;
spec: CartesianAxisLabelSpec;
} | null = null;
for (const width of candidateWidths) {
const { wrapped, text } = wrapOrEllipsifyTextToWidth(
label,
width,
fontString,
lineHeight,
);
const metrics = measureText(text, fontString, lineHeight);
const rotated = getRotatedBoundingBox(
metrics.width,
metrics.height,
CARTESIAN_LABEL_ROTATION,
);
const spec = {
originalText: label,
text,
metrics,
rotatedWidth: rotated.width,
rotatedHeight: rotated.height,
wrapped,
};
const overflow = rotated.width - maxRotatedWidth;
if (overflow <= 0) {
if (!bestFit || shouldPrefer(spec, bestFit)) {
bestFit = spec;
}
continue;
}
if (
!bestOverflowAny ||
overflow < bestOverflowAny.overflow ||
(overflow === bestOverflowAny.overflow &&
shouldPrefer(spec, bestOverflowAny.spec))
) {
bestOverflowAny = { overflow, spec };
}
if (
!isEllipsifiedLabel(spec.text) &&
(!bestOverflowNonEllipsified ||
overflow < bestOverflowNonEllipsified.overflow ||
(overflow === bestOverflowNonEllipsified.overflow &&
shouldPrefer(spec, bestOverflowNonEllipsified.spec)))
) {
bestOverflowNonEllipsified = { overflow, spec };
}
}
if (bestFit) {
return bestFit;
}
if (
bestOverflowNonEllipsified &&
bestOverflowAny &&
bestOverflowNonEllipsified.overflow <=
bestOverflowAny.overflow + CARTESIAN_LABEL_OVERFLOW_PREFERENCE_BUFFER
) {
return bestOverflowNonEllipsified.spec;
}
return bestOverflowAny!.spec;
};
export const getRotatedTextElementBottom = (
element: NonDeletedExcalidrawElement,
) => {
if (element.type !== "text") {
return element.y + element.height;
}
const rotated = getRotatedBoundingBox(
element.width,
element.height,
element.angle,
);
return element.y + element.height / 2 + rotated.height / 2;
};
export const chartXLabels = (
spreadsheet: Spreadsheet,
x: number,
y: number,
backgroundColor: string,
layout: CartesianChartLayout,
): ChartElements => {
const fontFamily = commonProps.fontFamily;
const fontSize = FONT_SIZES.sm;
const lineHeight = getLineHeight(fontFamily);
const fontString = getFontString({ fontFamily, fontSize });
const maxRotatedWidth = Math.max(
1,
layout.slotWidth +
layout.gap -
CARTESIAN_LABEL_SLOT_PADDING * 2 +
CARTESIAN_LABEL_ROTATED_WIDTH_BUFFER,
);
const axisY = y;
return (
spreadsheet.labels?.map((label, index) => {
const labelSpec = getCartesianAxisLabelSpec(
label,
layout.xLabelMaxWidth,
maxRotatedWidth,
fontString,
lineHeight,
);
const centerX =
x +
index * (layout.slotWidth + layout.gap) +
layout.gap +
layout.slotWidth / 2;
const labelY =
axisY +
CARTESIAN_LABEL_AXIS_CLEARANCE +
(labelSpec.rotatedHeight - labelSpec.metrics.height) / 2;
return newTextElement({
backgroundColor,
...commonProps,
text: labelSpec.text,
originalText: labelSpec.wrapped ? label : labelSpec.text,
autoResize: !labelSpec.wrapped,
x: centerX,
y: labelY,
angle: CARTESIAN_LABEL_ROTATION,
fontSize,
lineHeight,
textAlign: "center",
verticalAlign: "top",
});
}) || []
);
};
const chartYLabels = (
spreadsheet: Spreadsheet,
x: number,
y: number,
backgroundColor: string,
layout: CartesianChartLayout,
maxValue = Math.max(...spreadsheet.series[0].values),
): ChartElements => {
const minYLabel = newTextElement({
backgroundColor,
...commonProps,
x: x - layout.gap,
y: y - layout.gap,
text: "0",
textAlign: "right",
});
const maxYLabel = newTextElement({
backgroundColor,
...commonProps,
x: x - layout.gap,
y: y - layout.chartHeight - minYLabel.height / 2,
text: maxValue.toLocaleString(),
textAlign: "right",
});
return [minYLabel, maxYLabel];
};
const chartLines = (
spreadsheet: Spreadsheet,
x: number,
y: number,
backgroundColor: string,
layout: CartesianChartLayout,
): ChartElements => {
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet, layout);
const xLine = newLinearElement({
backgroundColor,
...commonProps,
type: "line",
x,
y,
width: chartWidth,
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
});
const yLine = newLinearElement({
backgroundColor,
...commonProps,
type: "line",
x,
y,
height: chartHeight,
points: [pointFrom(0, 0), pointFrom(0, -chartHeight)],
});
const maxLine = newLinearElement({
backgroundColor,
...commonProps,
type: "line",
x,
y: y - layout.chartHeight - layout.gap,
strokeStyle: "dotted",
width: chartWidth,
opacity: GRID_OPACITY,
points: [pointFrom(0, 0), pointFrom(chartWidth, 0)],
});
return [xLine, yLine, maxLine];
};
// For the maths behind it https://excalidraw.com/#json=6320864370884608,O_5xfD-Agh32tytHpRJx1g
export const chartBaseElements = (
spreadsheet: Spreadsheet,
x: number,
y: number,
backgroundColor: string,
layout: CartesianChartLayout,
maxValue = Math.max(...spreadsheet.series[0].values),
debug?: boolean,
): ChartElements => {
const { chartWidth, chartHeight } = getChartDimensions(spreadsheet, layout);
const title = spreadsheet.title
? newTextElement({
backgroundColor,
...commonProps,
text: spreadsheet.title,
x: x + chartWidth / 2,
y: y - layout.chartHeight - layout.gap * 2 - DEFAULT_FONT_SIZE,
roundness: null,
textAlign: "center",
fontSize: FONT_SIZES.xl,
fontFamily: FONT_FAMILY["Lilita One"],
})
: null;
const debugRect = debug
? newElement({
backgroundColor,
...commonProps,
type: "rectangle",
x,
y: y - chartHeight,
width: chartWidth,
height: chartHeight,
strokeColor: COLOR_PALETTE.black,
fillStyle: "solid",
opacity: 6,
})
: null;
return [
...(debugRect ? [debugRect] : []),
...(title ? [title] : []),
...chartXLabels(spreadsheet, x, y, backgroundColor, layout),
...chartYLabels(spreadsheet, x, y, backgroundColor, layout, maxValue),
...chartLines(spreadsheet, x, y, backgroundColor, layout),
];
};
-130
View File
@@ -1,130 +0,0 @@
import { pointFrom } from "@excalidraw/math";
import { isDevEnv } from "@excalidraw/common";
import { newElement, newLinearElement } from "@excalidraw/element";
import type { LocalPoint } from "@excalidraw/math";
import { GRID_OPACITY, commonProps } from "./charts.constants";
import {
chartBaseElements,
chartXLabels,
createSeriesLegend,
getBackgroundColor,
getCartesianChartLayout,
getChartDimensions,
getColorOffset,
getRotatedTextElementBottom,
getSeriesColors,
} from "./charts.helpers";
import type { ChartElements, Spreadsheet } from "./charts.types";
export const renderLineChart = (
spreadsheet: Spreadsheet,
x: number,
y: number,
colorSeed?: number,
): ChartElements => {
const series = spreadsheet.series;
const layout = getCartesianChartLayout("line", series.length);
const max = Math.max(1, ...series.flatMap((seriesData) => seriesData.values));
const colorOffset = getColorOffset(colorSeed);
const backgroundColor = getBackgroundColor(colorOffset);
const seriesColors = getSeriesColors(series.length, colorOffset);
const lines = series.map((seriesData, seriesIndex) => {
const points = seriesData.values.map((value, valueIndex) =>
pointFrom<LocalPoint>(
valueIndex * (layout.slotWidth + layout.gap),
-(value / max) * layout.chartHeight,
),
);
const maxX = Math.max(...points.map((point) => point[0]));
const maxY = Math.max(...points.map((point) => point[1]));
const minX = Math.min(...points.map((point) => point[0]));
const minY = Math.min(...points.map((point) => point[1]));
return newLinearElement({
backgroundColor: "transparent",
...commonProps,
type: "line",
x: x + layout.gap + layout.slotWidth / 2,
y: y - layout.gap,
height: maxY - minY,
width: maxX - minX,
strokeColor: seriesColors[seriesIndex],
strokeWidth: 2,
points,
});
});
const dots = series.flatMap((seriesData, seriesIndex) =>
seriesData.values.map((value, valueIndex) => {
const cx = valueIndex * (layout.slotWidth + layout.gap) + layout.gap / 2;
const cy = -(value / max) * layout.chartHeight + layout.gap / 2;
return newElement({
backgroundColor: seriesColors[seriesIndex],
...commonProps,
fillStyle: "solid",
strokeColor: seriesColors[seriesIndex],
strokeWidth: 2,
type: "ellipse",
x: x + cx + layout.slotWidth / 2,
y: y + cy - layout.gap * 2,
width: layout.gap,
height: layout.gap,
});
}),
);
const guideValues = series[0].values.map((_, valueIndex) =>
Math.max(
0,
...series.map((seriesData) => seriesData.values[valueIndex] ?? 0),
),
);
const guides = guideValues.map((value, valueIndex) => {
const cx = valueIndex * (layout.slotWidth + layout.gap) + layout.gap / 2;
const cy = (value / max) * layout.chartHeight + layout.gap / 2 + layout.gap;
return newLinearElement({
backgroundColor,
...commonProps,
type: "line",
x: x + cx + layout.slotWidth / 2 + layout.gap / 2,
y: y - cy,
height: cy,
strokeStyle: "dotted",
opacity: GRID_OPACITY,
points: [pointFrom(0, 0), pointFrom(0, cy)],
});
});
const baseElements = chartBaseElements(
spreadsheet,
x,
y,
backgroundColor,
layout,
max,
isDevEnv(),
);
const xLabels = chartXLabels(spreadsheet, x, y, backgroundColor, layout);
const xLabelsBottomY = Math.max(
y + layout.gap / 2,
...xLabels.map((label) => getRotatedTextElementBottom(label)),
);
const { chartWidth } = getChartDimensions(spreadsheet, layout);
const seriesLegend = createSeriesLegend(
series,
seriesColors,
x + chartWidth / 2,
xLabelsBottomY,
y + layout.gap * 5,
backgroundColor,
);
return [...baseElements, ...lines, ...guides, ...dots, ...seriesLegend];
};
-174
View File
@@ -1,174 +0,0 @@
import { type ParseSpreadsheetResult } from "./charts.types";
/**
* @private exported for testing
*/
export const tryParseNumber = (s: string): number | null => {
const match =
/^([-+]?)[$\u20AC\u00A3\u00A5\u20A9]?([-+]?)([\d.,]+)[%]?$/.exec(s);
if (!match) {
return null;
}
return parseFloat(`${(match[1] || match[2]) + match[3]}`.replace(/,/g, ""));
};
const isNumericColumn = (lines: string[][], columnIndex: number) =>
lines.slice(1).every((line) => tryParseNumber(line[columnIndex]) !== null);
/**
* @private exported for testing
*/
export const tryParseCells = (cells: string[][]): ParseSpreadsheetResult => {
const numCols = cells[0].length;
if (numCols > 2) {
const hasHeader = cells[0].every((cell) => tryParseNumber(cell) === null);
const rows = hasHeader ? cells.slice(1) : cells;
if (rows.length < 1) {
return { ok: false, reason: "No data rows" };
}
const invalidNumericColumn = rows.some((row) =>
row.slice(1).some((value) => tryParseNumber(value) === null),
);
if (invalidNumericColumn) {
return { ok: false, reason: "Value is not numeric" };
}
// When there are more value columns than data rows, the data is in
// "wide" format — transpose so columns become labels (dimensions)
// and rows become series. This enables e.g. radar charts for wide data.
const numValueCols = numCols - 1;
if (numValueCols > rows.length) {
const labels = hasHeader ? cells[0].slice(1).map((h) => h.trim()) : null;
const series = rows.map((row) => ({
title: row[0]?.trim() || null,
values: row.slice(1).map((v) => tryParseNumber(v)!),
}));
const title =
series.length === 1
? series[0].title
: hasHeader
? cells[0][0].trim() || null
: null;
return {
ok: true,
data: { title, labels, series },
};
}
const series = cells[0].slice(1).map((seriesTitle, index) => {
const valueColumnIndex = index + 1;
const fallbackTitle = `Series ${valueColumnIndex}`;
return {
title: hasHeader ? seriesTitle.trim() || fallbackTitle : fallbackTitle,
values: rows.map((row) => tryParseNumber(row[valueColumnIndex])!),
};
});
return {
ok: true,
data: {
title: hasHeader ? cells[0][0].trim() || null : null,
labels: rows.map((row) => row[0]),
series,
},
};
}
if (numCols === 1) {
if (!isNumericColumn(cells, 0)) {
return { ok: false, reason: "Value is not numeric" };
}
const hasHeader = tryParseNumber(cells[0][0]) === null;
const title = hasHeader ? cells[0][0] : null;
const values = (hasHeader ? cells.slice(1) : cells).map((line) =>
tryParseNumber(line[0]),
);
if (values.length < 2) {
return { ok: false, reason: "Less than two rows" };
}
return {
ok: true,
data: {
title,
labels: null,
series: [{ title, values: values as number[] }],
},
};
}
const hasHeader = tryParseNumber(cells[0][1]) === null;
const rows = hasHeader ? cells.slice(1) : cells;
if (rows.length < 2) {
return { ok: false, reason: "Less than 2 rows" };
}
const invalidNumericColumn = rows.some(
(row) => tryParseNumber(row[1]) === null,
);
if (invalidNumericColumn) {
return { ok: false, reason: "Value is not numeric" };
}
const title = hasHeader ? cells[0][1] : null;
return {
ok: true,
data: {
title,
labels: rows.map((row) => row[0]),
series: [{ title, values: rows.map((row) => tryParseNumber(row[1])!) }],
},
};
};
export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
// Copy/paste from excel, spreadsheets, TSV, CSV, semicolon-separated.
const parseDelimitedLines = (delimiter: "\t" | "," | ";") =>
text
.replace(/\r\n?/g, "\n")
.split("\n")
.filter((line) => line.trim().length > 0)
.map((line) => line.split(delimiter).map((cell) => cell.trim()));
// Score each delimiter: prefer consistent column counts with the most columns.
// A delimiter that produces all single-column rows likely isn't the right one.
const candidates = (["\t", ",", ";"] as const).map((delimiter) => {
const parsed = parseDelimitedLines(delimiter);
const numCols = parsed[0]?.length ?? 0;
const isConsistent =
parsed.length > 0 && parsed.every((line) => line.length === numCols);
return { delimiter, parsed, numCols, isConsistent };
});
// Prefer: consistent + most columns. Among ties, tab > comma > semicolon
// (the array order already encodes this priority).
const best =
candidates.find((c) => c.isConsistent && c.numCols > 1) ??
candidates.find((c) => c.isConsistent) ??
candidates[0];
const lines = best.parsed;
if (lines.length === 0) {
return { ok: false, reason: "No values" };
}
const numColsFirstLine = lines[0].length;
const isSpreadsheet = lines.every((line) => line.length === numColsFirstLine);
if (!isSpreadsheet) {
return {
ok: false,
reason: "All rows don't have same number of columns",
};
}
return tryParseCells(lines);
};
-199
View File
@@ -1,199 +0,0 @@
import { pointFrom } from "@excalidraw/math";
import {
FONT_FAMILY,
FONT_SIZES,
getFontString,
getLineHeight,
ROUGHNESS,
} from "@excalidraw/common";
import {
measureText,
newLinearElement,
newTextElement,
} from "@excalidraw/element";
import type { LocalPoint } from "@excalidraw/math";
import {
BAR_GAP,
BAR_HEIGHT,
GRID_OPACITY,
RADAR_GRID_LEVELS,
RADAR_LABEL_OFFSET,
commonProps,
} from "./charts.constants";
import {
createRadarAxisLabels,
createSeriesLegend,
getBackgroundColor,
getColorOffset,
getRadarDimensions,
getRadarDisplayText,
getRadarValueScale,
getSeriesColors,
isSpreadsheetValidForChartType,
} from "./charts.helpers";
import type { ChartElements, Spreadsheet } from "./charts.types";
export const renderRadarChart = (
spreadsheet: Spreadsheet,
x: number,
y: number,
colorSeed?: number,
): ChartElements | null => {
if (!isSpreadsheetValidForChartType(spreadsheet, "radar")) {
return null;
}
const labels =
spreadsheet.labels ??
spreadsheet.series[0].values.map((_, index) => `Value ${index + 1}`);
const series = spreadsheet.series;
const { normalize, renderSteps } = getRadarValueScale(series, labels.length);
const colorOffset = getColorOffset(colorSeed);
const backgroundColor = getBackgroundColor(colorOffset);
const seriesColors = getSeriesColors(series.length, colorOffset);
const { chartWidth, chartHeight } = getRadarDimensions();
const centerX = x + chartWidth / 2;
const centerY = y - chartHeight / 2;
const radius = BAR_HEIGHT / 2;
const angles = labels.map(
(_, index) => -Math.PI / 2 + (Math.PI * 2 * index) / labels.length,
);
const { axisLabels, axisLabelTopY, axisLabelBottomY } = createRadarAxisLabels(
labels,
angles,
centerX,
centerY,
radius,
backgroundColor,
);
const titleFontFamily = FONT_FAMILY["Lilita One"];
const titleFontSize = FONT_SIZES.xl;
const titleLineHeight = getLineHeight(titleFontFamily);
const titleFontString = getFontString({
fontFamily: titleFontFamily,
fontSize: titleFontSize,
});
const titleText = spreadsheet.title
? getRadarDisplayText(
spreadsheet.title,
titleFontString,
chartWidth + RADAR_LABEL_OFFSET * 2,
)
: null;
const titleTextMetrics = titleText
? measureText(titleText, titleFontString, titleLineHeight)
: null;
const title = titleText
? newTextElement({
backgroundColor,
...commonProps,
text: titleText,
originalText: spreadsheet.title ?? titleText,
x: x + chartWidth / 2,
y: axisLabelTopY - RADAR_LABEL_OFFSET - titleTextMetrics!.height / 2,
fontFamily: titleFontFamily,
fontSize: titleFontSize,
lineHeight: titleLineHeight,
textAlign: "center",
})
: null;
const radarGridLines = renderSteps
? Array.from({ length: RADAR_GRID_LEVELS }, (_, levelIndex) => {
const levelRatio = (levelIndex + 1) / RADAR_GRID_LEVELS;
const levelRadius = radius * levelRatio;
const points = angles.map((angle) =>
pointFrom<LocalPoint>(
Math.cos(angle) * levelRadius,
Math.sin(angle) * levelRadius,
),
);
points.push(pointFrom(points[0][0], points[0][1]));
return newLinearElement({
backgroundColor: "transparent",
...commonProps,
type: "line",
x: centerX,
y: centerY,
width: levelRadius * 2,
height: levelRadius * 2,
strokeStyle: "solid",
roughness: ROUGHNESS.architect,
opacity: GRID_OPACITY,
polygon: true,
points,
});
})
: [];
const spokes = angles.map((angle) => {
const px = Math.cos(angle) * radius;
const py = Math.sin(angle) * radius;
return newLinearElement({
backgroundColor: "transparent",
...commonProps,
type: "line",
x: centerX,
y: centerY,
width: Math.abs(px),
height: Math.abs(py),
strokeStyle: "solid",
roughness: ROUGHNESS.architect,
opacity: GRID_OPACITY,
points: [pointFrom(0, 0), pointFrom(px, py)],
});
});
const seriesPolygons = series.map((seriesData, index) => {
const points = angles.map((angle, axisIndex) => {
const value = seriesData.values[axisIndex] ?? 0;
const pointRadius = normalize(value, axisIndex) * radius;
return pointFrom<LocalPoint>(
Math.cos(angle) * pointRadius,
Math.sin(angle) * pointRadius,
);
});
points.push(pointFrom(points[0][0], points[0][1]));
return newLinearElement({
backgroundColor: "transparent",
...commonProps,
type: "line",
x: centerX,
y: centerY,
width: radius * 2,
height: radius * 2,
strokeColor: seriesColors[index],
strokeWidth: 2,
polygon: true,
points,
});
});
const seriesLegend = createSeriesLegend(
series,
seriesColors,
centerX,
axisLabelBottomY,
y + BAR_GAP * 5,
backgroundColor,
);
return [
...(title ? [title] : []),
...axisLabels,
...radarGridLines,
...spokes,
...seriesPolygons,
...seriesLegend,
];
};
@@ -1,18 +0,0 @@
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
export type ChartElements = readonly NonDeletedExcalidrawElement[];
export interface Spreadsheet {
title: string | null;
labels: string[] | null;
series: SpreadsheetSeries[];
}
export interface SpreadsheetSeries {
title: string | null;
values: number[];
}
export type ParseSpreadsheetResult =
| { ok: false; reason: string }
| { ok: true; data: Spreadsheet };
-38
View File
@@ -1,38 +0,0 @@
import type { ChartType } from "@excalidraw/element/types";
import { renderBarChart } from "./charts.bar";
import { renderLineChart } from "./charts.line";
import {
tryParseCells,
tryParseNumber,
tryParseSpreadsheet,
} from "./charts.parse";
import { renderRadarChart } from "./charts.radar";
import type { ChartElements, Spreadsheet } from "./charts.types";
export {
type ParseSpreadsheetResult,
type Spreadsheet,
type SpreadsheetSeries,
type ChartElements,
} from "./charts.types";
export { isSpreadsheetValidForChartType } from "./charts.helpers";
export { tryParseCells, tryParseNumber, tryParseSpreadsheet };
export const renderSpreadsheet = (
chartType: ChartType,
spreadsheet: Spreadsheet,
x: number,
y: number,
colorSeed?: number,
): ChartElements | null => {
if (chartType === "line") {
return renderLineChart(spreadsheet, x, y, colorSeed);
}
if (chartType === "radar") {
return renderRadarChart(spreadsheet, x, y, colorSeed);
}
return renderBarChart(spreadsheet, x, y, colorSeed);
};
+134 -3
View File
@@ -54,7 +54,13 @@ describe("parseClipboard()", () => {
}), }),
), ),
); );
expect(clipboardData.elements).toEqual([rect]); expect(clipboardData.elements).toEqual([
expect.objectContaining({
id: rect.id,
type: rect.type,
schemaState: rect.schemaState,
}),
]);
}); });
it("should parse valid excalidraw JSON if inside text/html", async () => { it("should parse valid excalidraw JSON if inside text/html", async () => {
@@ -73,7 +79,13 @@ describe("parseClipboard()", () => {
}), }),
), ),
); );
expect(clipboardData.elements).toEqual([rect]); expect(clipboardData.elements).toEqual([
expect.objectContaining({
id: rect.id,
type: rect.type,
schemaState: rect.schemaState,
}),
]);
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
json = serializeAsClipboardJSON({ elements: [rect], files: null }); json = serializeAsClipboardJSON({ elements: [rect], files: null });
clipboardData = await parseClipboard( clipboardData = await parseClipboard(
@@ -85,10 +97,66 @@ describe("parseClipboard()", () => {
}), }),
), ),
); );
expect(clipboardData.elements).toEqual([rect]); expect(clipboardData.elements).toEqual([
expect.objectContaining({
id: rect.id,
type: rect.type,
schemaState: rect.schemaState,
}),
]);
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
}); });
it("should preserve per-element schema on clipboard payload", async () => {
const rect = API.createElement({ type: "rectangle" });
const clipboardPayload = JSON.parse(
serializeAsClipboardJSON({ elements: [rect], files: null }),
);
const clipboardData = await parseClipboard(
await parseDataTransferEvent(
createPasteEvent({
types: {
"text/plain": JSON.stringify(clipboardPayload),
},
}),
),
);
expect(clipboardData.elements?.[0]).toEqual(
expect.objectContaining({
id: rect.id,
schemaState: rect.schemaState,
}),
);
});
it("should not upcast legacy elements to latest schema on clipboard serialize", async () => {
const rect = API.createElement({ type: "rectangle" });
const legacyRect = { ...(rect as any) };
delete legacyRect.schemaState;
const clipboardPayload = JSON.parse(
serializeAsClipboardJSON({
elements: [legacyRect as typeof rect],
files: null,
}),
);
expect(clipboardPayload.elements[0]).not.toHaveProperty("schemaState");
const clipboardData = await parseClipboard(
await parseDataTransferEvent(
createPasteEvent({
types: {
"text/plain": JSON.stringify(clipboardPayload),
},
}),
),
);
expect(clipboardData.elements?.[0]).not.toHaveProperty("schemaState");
});
it("should parse <image> `src` urls out of text/html", async () => { it("should parse <image> `src` urls out of text/html", async () => {
let clipboardData; let clipboardData;
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -155,4 +223,67 @@ describe("parseClipboard()", () => {
}, },
]); ]);
}); });
it("should parse spreadsheet from either text/plain and text/html", async () => {
let clipboardData;
// -------------------------------------------------------------------------
clipboardData = await parseClipboard(
await parseDataTransferEvent(
createPasteEvent({
types: {
"text/plain": `a b
1 2
4 5
7 10`,
},
}),
),
);
expect(clipboardData.spreadsheet).toEqual({
title: "b",
labels: ["1", "4", "7"],
values: [2, 5, 10],
});
// -------------------------------------------------------------------------
clipboardData = await parseClipboard(
await parseDataTransferEvent(
createPasteEvent({
types: {
"text/html": `a b
1 2
4 5
7 10`,
},
}),
),
);
expect(clipboardData.spreadsheet).toEqual({
title: "b",
labels: ["1", "4", "7"],
values: [2, 5, 10],
});
// -------------------------------------------------------------------------
clipboardData = await parseClipboard(
await parseDataTransferEvent(
createPasteEvent({
types: {
"text/html": `<html>
<body>
<!--StartFragment--><google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none"><colgroup><col width="100"/><col width="100"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;a&quot;}">a</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;b&quot;}">b</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:1}">1</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:2}">2</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:4}">4</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:5}">5</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:7}">7</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:10}">10</td></tr></tbody></table><!--EndFragment-->
</body>
</html>`,
"text/plain": `a b
1 2
4 5
7 10`,
},
}),
),
);
expect(clipboardData.spreadsheet).toEqual({
title: "b",
labels: ["1", "4", "7"],
values: [2, 5, 10],
});
});
}); });
+32 -1
View File
@@ -33,8 +33,12 @@ import {
normalizeFile, normalizeFile,
} from "./data/blob"; } from "./data/blob";
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
import type { FileSystemHandle } from "./data/filesystem"; import type { FileSystemHandle } from "./data/filesystem";
import type { Spreadsheet } from "./charts";
import type { BinaryFiles } from "./types"; import type { BinaryFiles } from "./types";
type ElementsClipboard = { type ElementsClipboard = {
@@ -46,6 +50,7 @@ type ElementsClipboard = {
export type PastedMixedContent = { type: "text" | "imageUrl"; value: string }[]; export type PastedMixedContent = { type: "text" | "imageUrl"; value: string }[];
export interface ClipboardData { export interface ClipboardData {
spreadsheet?: Spreadsheet;
elements?: readonly ExcalidrawElement[]; elements?: readonly ExcalidrawElement[];
files?: BinaryFiles; files?: BinaryFiles;
text?: string; text?: string;
@@ -74,7 +79,10 @@ export const probablySupportsClipboardBlob =
const clipboardContainsElements = ( const clipboardContainsElements = (
contents: any, contents: any,
): contents is { elements: ExcalidrawElement[]; files?: BinaryFiles } => { ): contents is {
elements: ExcalidrawElement[];
files?: BinaryFiles;
} => {
if ( if (
[ [
EXPORT_DATA_TYPES.excalidraw, EXPORT_DATA_TYPES.excalidraw,
@@ -210,6 +218,16 @@ export const copyToClipboard = async (
); );
}; };
const parsePotentialSpreadsheet = (
text: string,
): { spreadsheet: Spreadsheet } | { errorMessage: string } | null => {
const result = tryParseSpreadsheet(text);
if (result.type === VALID_SPREADSHEET) {
return { spreadsheet: result.spreadsheet };
}
return null;
};
/** internal, specific to parsing paste events. Do not reuse. */ /** internal, specific to parsing paste events. Do not reuse. */
function parseHTMLTree(el: ChildNode) { function parseHTMLTree(el: ChildNode) {
let result: PastedMixedContent = []; let result: PastedMixedContent = [];
@@ -536,6 +554,19 @@ export const parseClipboard = async (
}; };
} }
try {
// if system clipboard contains spreadsheet, use it even though it's
// technically possible it's staler than in-app clipboard
const spreadsheetResult =
!isPlainPaste && parsePotentialSpreadsheet(parsedEventData.value);
if (spreadsheetResult) {
return spreadsheetResult;
}
} catch (error: any) {
console.error(error);
}
try { try {
const systemClipboardData = JSON.parse(parsedEventData.value); const systemClipboardData = JSON.parse(parsedEventData.value);
const programmaticAPI = const programmaticAPI =
+8 -8
View File
@@ -20,6 +20,7 @@ import {
isArrowElement, isArrowElement,
hasStrokeColor, hasStrokeColor,
toolIsArrow, toolIsArrow,
isFrameElement,
} from "@excalidraw/element"; } from "@excalidraw/element";
import type { import type {
@@ -129,8 +130,11 @@ export const canChangeBackgroundColor = (
targetElements: ExcalidrawElement[], targetElements: ExcalidrawElement[],
) => { ) => {
return ( return (
// frame tool shouldn't allow to set background until frame is created
hasBackground(appState.activeTool.type) || hasBackground(appState.activeTool.type) ||
targetElements.some((element) => hasBackground(element.type)) targetElements.some(
(element) => hasBackground(element.type) || isFrameElement(element),
)
); );
}; };
@@ -226,7 +230,7 @@ export const SelectedShapeActions = ({
{(appState.activeTool.type === "text" || {(appState.activeTool.type === "text" ||
targetElements.some(isTextElement)) && ( targetElements.some(isTextElement)) && (
<> <>
<fieldset>{renderAction("changeFontFamily")}</fieldset> {renderAction("changeFontFamily")}
{renderAction("changeFontSize")} {renderAction("changeFontSize")}
{(appState.activeTool.type === "text" || {(appState.activeTool.type === "text" ||
suppportsHorizontalAlign(targetElements, elementsMap)) && suppportsHorizontalAlign(targetElements, elementsMap)) &&
@@ -1081,9 +1085,8 @@ export const ShapesSwitcher = ({
return ( return (
<> <>
{getToolbarTools(app).map( {getToolbarTools(app).map(
({ value, icon, key, numericKey, fillable, toolbar }) => { ({ value, icon, key, numericKey, fillable }, index) => {
if ( if (
toolbar === false ||
UIOptions.tools?.[ UIOptions.tools?.[
value as Extract< value as Extract<
typeof value, typeof value,
@@ -1100,9 +1103,6 @@ export const ShapesSwitcher = ({
const shortcut = letter const shortcut = letter
? `${letter} ${t("helpDialog.or")} ${numericKey}` ? `${letter} ${t("helpDialog.or")} ${numericKey}`
: `${numericKey}`; : `${numericKey}`;
const keybindingLabel =
value === "hand" ? undefined : numericKey || letter;
// when in compact styles panel mode (tablet) // when in compact styles panel mode (tablet)
// use a ToolPopover for selection/lasso toggle as well // use a ToolPopover for selection/lasso toggle as well
if ( if (
@@ -1147,7 +1147,7 @@ export const ShapesSwitcher = ({
checked={activeTool.type === value} checked={activeTool.type === value}
name="editor-current-shape" name="editor-current-shape"
title={`${capitalizeString(label)}${shortcut}`} title={`${capitalizeString(label)}${shortcut}`}
keyBindingLabel={keybindingLabel} keyBindingLabel={numericKey || letter}
aria-label={capitalizeString(label)} aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut} aria-keyshortcuts={shortcut}
data-testid={`toolbar-${value}`} data-testid={`toolbar-${value}`}
+184 -222
View File
@@ -108,7 +108,6 @@ import {
loadDesktopUIModePreference, loadDesktopUIModePreference,
setDesktopUIMode, setDesktopUIMode,
isSelectionLikeTool, isSelectionLikeTool,
oneOf,
} from "@excalidraw/common"; } from "@excalidraw/common";
import { import {
@@ -119,6 +118,7 @@ import {
fixBindingsAfterDeletion, fixBindingsAfterDeletion,
getHoveredElementForBinding, getHoveredElementForBinding,
isBindingEnabled, isBindingEnabled,
shouldEnableBindingForPointerEvent,
updateBoundElements, updateBoundElements,
LinearElementEditor, LinearElementEditor,
newElementWith, newElementWith,
@@ -256,6 +256,7 @@ import {
handleFocusPointPointerUp, handleFocusPointPointerUp,
maybeHandleArrowPointlikeDrag, maybeHandleArrowPointlikeDrag,
getUncroppedWidthAndHeight, getUncroppedWidthAndHeight,
isFrameElement,
} from "@excalidraw/element"; } from "@excalidraw/element";
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math"; import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
@@ -318,8 +319,6 @@ import {
actionToggleElementLock, actionToggleElementLock,
actionToggleLinearEditor, actionToggleLinearEditor,
actionToggleObjectsSnapMode, actionToggleObjectsSnapMode,
actionToggleArrowBinding,
actionToggleMidpointSnapping,
actionToggleCropEditor, actionToggleCropEditor,
} from "../actions"; } from "../actions";
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
@@ -355,6 +354,7 @@ import {
import { exportCanvas, loadFromBlob } from "../data"; import { exportCanvas, loadFromBlob } from "../data";
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
import { restoreAppState, restoreElements } from "../data/restore"; import { restoreAppState, restoreElements } from "../data/restore";
import { createSchemaMigrationRegistry } from "../data/schema";
import { getCenter, getDistance } from "../gesture"; import { getCenter, getDistance } from "../gesture";
import { History } from "../history"; import { History } from "../history";
import { defaultLang, getLanguage, languages, setLanguage, t } from "../i18n"; import { defaultLang, getLanguage, languages, setLanguage, t } from "../i18n";
@@ -426,8 +426,6 @@ import { EraserTrail } from "../eraser";
import { getShortcutKey } from "../shortcut"; import { getShortcutKey } from "../shortcut";
import { tryParseSpreadsheet } from "../charts";
import ConvertElementTypePopup, { import ConvertElementTypePopup, {
getConversionTypeFromElements, getConversionTypeFromElements,
convertElementTypePopupAtom, convertElementTypePopupAtom,
@@ -463,6 +461,7 @@ import type {
import type { ClipboardData, PastedMixedContent } from "../clipboard"; import type { ClipboardData, PastedMixedContent } from "../clipboard";
import type { ExportedElements } from "../data"; import type { ExportedElements } from "../data";
import type { SchemaMigrationRegistry } from "../data/schema";
import type { ContextMenuItems } from "./ContextMenu"; import type { ContextMenuItems } from "./ContextMenu";
import type { FileSystemHandle } from "../data/filesystem"; import type { FileSystemHandle } from "../data/filesystem";
@@ -609,6 +608,7 @@ class App extends React.Component<AppProps, AppState> {
public fonts: Fonts; public fonts: Fonts;
public renderer: Renderer; public renderer: Renderer;
public visibleElements: readonly NonDeletedExcalidrawElement[]; public visibleElements: readonly NonDeletedExcalidrawElement[];
private schemaMigrationRegistry: SchemaMigrationRegistry;
private resizeObserver: ResizeObserver | undefined; private resizeObserver: ResizeObserver | undefined;
public library: AppClassProperties["library"]; public library: AppClassProperties["library"];
public libraryItemsFromStorage: LibraryItems | undefined; public libraryItemsFromStorage: LibraryItems | undefined;
@@ -725,6 +725,9 @@ class App extends React.Component<AppProps, AppState> {
this.stylesPanelMode = deriveStylesPanelMode(this.editorInterface); this.stylesPanelMode = deriveStylesPanelMode(this.editorInterface);
this.id = nanoid(); this.id = nanoid();
this.schemaMigrationRegistry = createSchemaMigrationRegistry(
props.schemaPlugins,
);
this.library = new Library(this); this.library = new Library(this);
this.actionManager = new ActionManager( this.actionManager = new ActionManager(
this.syncActionResult, this.syncActionResult,
@@ -771,6 +774,7 @@ class App extends React.Component<AppProps, AppState> {
setCursor: this.setCursor, setCursor: this.setCursor,
resetCursor: this.resetCursor, resetCursor: this.resetCursor,
getEditorInterface: () => this.editorInterface, getEditorInterface: () => this.editorInterface,
getSchemaMigrationRegistry: this.getSchemaMigrationRegistry,
updateFrameRendering: this.updateFrameRendering, updateFrameRendering: this.updateFrameRendering,
toggleSidebar: this.toggleSidebar, toggleSidebar: this.toggleSidebar,
onChange: (cb) => this.onChangeEmitter.on(cb), onChange: (cb) => this.onChangeEmitter.on(cb),
@@ -1223,14 +1227,12 @@ class App extends React.Component<AppProps, AppState> {
if ( if (
hitElement && hitElement &&
isIframeLikeElement(hitElement) && isIframeLikeElement(hitElement) &&
(this.state.viewModeEnabled || this.isIframeLikeElementCenter(
this.state.activeTool.type === "laser" || hitElement,
this.isIframeLikeElementCenter( moveEvent,
hitElement, scenePointer.x,
moveEvent, scenePointer.y,
scenePointer.x, )
scenePointer.y,
))
) { ) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
this.setState({ this.setState({
@@ -1245,72 +1247,61 @@ class App extends React.Component<AppProps, AppState> {
/** @returns true if iframe-like element click handled */ /** @returns true if iframe-like element click handled */
private handleIframeLikeCenterClick(): boolean { private handleIframeLikeCenterClick(): boolean {
if (!this.lastPointerDownEvent || !this.lastPointerUpEvent) {
return false;
}
const scenePointerStart = viewportCoordsToSceneCoords(
{
clientX: this.lastPointerDownEvent.clientX,
clientY: this.lastPointerDownEvent.clientY,
},
this.state,
);
const scenePointerEnd = viewportCoordsToSceneCoords(
{
clientX: this.lastPointerUpEvent.clientX,
clientY: this.lastPointerUpEvent.clientY,
},
this.state,
);
const hitElementStart = this.getElementAtPosition(
scenePointerStart.x,
scenePointerStart.y,
);
const hitElementEnd = this.getElementAtPosition(
scenePointerEnd.x,
scenePointerEnd.y,
);
if ( if (
!this.lastPointerDownEvent || !hitElementStart ||
!this.lastPointerUpEvent || !hitElementEnd ||
// middle-click or something other than primary hitElementStart !== hitElementEnd ||
this.lastPointerDownEvent.button !== POINTER_BUTTON.MAIN || this.lastPointerUpEvent.timeStamp - this.lastPointerDownEvent.timeStamp >
// panning 300 ||
isHoldingSpace || gesture.pointers.size > 1 ||
// wrong tool !isIframeLikeElement(hitElementStart) ||
!oneOf(this.state.activeTool.type, ["laser", "selection", "lasso"]) !isIframeLikeElement(hitElementEnd) ||
!this.isIframeLikeElementCenter(
hitElementStart,
this.lastPointerUpEvent,
scenePointerStart.x,
scenePointerStart.y,
) ||
!this.isIframeLikeElementCenter(
hitElementEnd,
this.lastPointerUpEvent,
scenePointerEnd.x,
scenePointerEnd.y,
)
) { ) {
return false; return false;
} }
const viewportClickStart_scenePoint = pointFrom( const iframeLikeElement = hitElementEnd;
viewportCoordsToSceneCoords(
{
clientX: this.lastPointerDownEvent.clientX,
clientY: this.lastPointerDownEvent.clientY,
},
this.state,
),
);
const viewportClickEnd_scenePoint = pointFrom(
viewportCoordsToSceneCoords(
{
clientX: this.lastPointerUpEvent.clientX,
clientY: this.lastPointerUpEvent.clientY,
},
this.state,
),
);
const draggedDistance = pointDistance(
viewportClickStart_scenePoint,
viewportClickEnd_scenePoint,
);
if (draggedDistance > DRAGGING_THRESHOLD) {
return false;
}
const hitElement = this.getElementAtPosition(
viewportClickStart_scenePoint[0],
viewportClickStart_scenePoint[1],
);
const shouldActivate =
hitElement &&
this.lastPointerUpEvent.timeStamp - this.lastPointerDownEvent.timeStamp <=
300 &&
gesture.pointers.size < 2 &&
isIframeLikeElement(hitElement) &&
(this.state.viewModeEnabled ||
this.state.activeTool.type === "laser" ||
this.isIframeLikeElementCenter(
hitElement,
this.lastPointerUpEvent,
viewportClickEnd_scenePoint[0],
viewportClickEnd_scenePoint[1],
));
if (!shouldActivate) {
return false;
}
const iframeLikeElement = hitElement;
if ( if (
this.state.activeEmbeddable?.element === iframeLikeElement && this.state.activeEmbeddable?.element === iframeLikeElement &&
@@ -2336,6 +2327,10 @@ class App extends React.Component<AppProps, AppState> {
return this.scene.getNonDeletedElements(); return this.scene.getNonDeletedElements();
}; };
public getSchemaMigrationRegistry = () => {
return this.schemaMigrationRegistry;
};
public onInsertElements = (elements: readonly ExcalidrawElement[]) => { public onInsertElements = (elements: readonly ExcalidrawElement[]) => {
this.addElementsFromPasteOrLibrary({ this.addElementsFromPasteOrLibrary({
elements, elements,
@@ -2735,9 +2730,7 @@ class App extends React.Component<AppProps, AppState> {
private onBlur = withBatchedUpdates(() => { private onBlur = withBatchedUpdates(() => {
isHoldingSpace = false; isHoldingSpace = false;
this.setState({ this.setState({ isBindingEnabled: true });
isBindingEnabled: this.state.bindingPreference === "enabled",
});
}); });
private onUnload = () => { private onUnload = () => {
@@ -2826,6 +2819,7 @@ class App extends React.Component<AppProps, AppState> {
const restoredElements = restoreElements(initialData?.elements, null, { const restoredElements = restoreElements(initialData?.elements, null, {
repairBindings: true, repairBindings: true,
deleteInvisibleElements: true, deleteInvisibleElements: true,
schemaMigrationRegistry: this.schemaMigrationRegistry,
}); });
let restoredAppState = restoreAppState(initialData?.appState, null); let restoredAppState = restoreAppState(initialData?.appState, null);
const activeTool = restoredAppState.activeTool; const activeTool = restoredAppState.activeTool;
@@ -3237,6 +3231,12 @@ class App extends React.Component<AppProps, AppState> {
} }
componentDidUpdate(prevProps: AppProps, prevState: AppState) { componentDidUpdate(prevProps: AppProps, prevState: AppState) {
if (prevProps.schemaPlugins !== this.props.schemaPlugins) {
this.schemaMigrationRegistry = createSchemaMigrationRegistry(
this.props.schemaPlugins,
);
}
this.updateEmbeddables(); this.updateEmbeddables();
const elements = this.scene.getElementsIncludingDeleted(); const elements = this.scene.getElementsIncludingDeleted();
const elementsMap = this.scene.getElementsMapIncludingDeleted(); const elementsMap = this.scene.getElementsMapIncludingDeleted();
@@ -3547,19 +3547,14 @@ class App extends React.Component<AppProps, AppState> {
} }
// ------------------- Spreadsheet ------------------- // ------------------- Spreadsheet -------------------
if (data.spreadsheet && !isPlainPaste) {
if (!isPlainPaste && data.text) { this.setState({
const result = tryParseSpreadsheet(data.text); pasteDialog: {
if (result.ok) { data: data.spreadsheet,
this.setState({ shown: true,
openDialog: { },
name: "charts", });
data: result.data, return;
rawText: data.text,
},
});
return;
}
} }
// ------------------- Images or SVG code ------------------- // ------------------- Images or SVG code -------------------
@@ -3745,6 +3740,7 @@ class App extends React.Component<AppProps, AppState> {
}) => { }) => {
const elements = restoreElements(opts.elements, null, { const elements = restoreElements(opts.elements, null, {
deleteInvisibleElements: true, deleteInvisibleElements: true,
schemaMigrationRegistry: this.schemaMigrationRegistry,
}); });
const [minX, minY, maxX, maxY] = getCommonBounds(elements); const [minX, minY, maxX, maxY] = getCommonBounds(elements);
@@ -4868,87 +4864,17 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
// view mode hardcoded from upstream -> disable tool switching for now
const shouldPreventToolSwitching = this.props.viewModeEnabled === true;
if (
!shouldPreventToolSwitching &&
this.state.viewModeEnabled &&
event.key === KEYS.ESCAPE
) {
this.setActiveTool({ type: "selection" });
return;
}
if (
!shouldPreventToolSwitching &&
!event.ctrlKey &&
!event.altKey &&
!event.metaKey &&
!this.state.newElement &&
!this.state.selectionElement &&
!this.state.selectedElementsAreBeingDragged
) {
const shape = findShapeByKey(event.key, this);
if (this.state.viewModeEnabled && !oneOf(shape, ["laser", "hand"])) {
return;
}
if (shape) {
if (this.state.activeTool.type !== shape) {
trackEvent(
"toolbar",
shape,
`keyboard (${
this.editorInterface.formFactor === "phone"
? "mobile"
: "desktop"
})`,
);
}
if (shape === "arrow" && this.state.activeTool.type === "arrow") {
this.setState((prevState) => ({
currentItemArrowType:
prevState.currentItemArrowType === ARROW_TYPE.sharp
? ARROW_TYPE.round
: prevState.currentItemArrowType === ARROW_TYPE.round
? ARROW_TYPE.elbow
: ARROW_TYPE.sharp,
}));
}
if (shape === "lasso" && this.state.activeTool.type === "laser") {
this.setActiveTool({
type: this.state.preferredSelectionTool.type,
});
} else {
this.setActiveTool({ type: shape });
}
event.stopPropagation();
return;
} else if (event.key === KEYS.Q) {
this.toggleLock("keyboard");
event.stopPropagation();
return;
}
}
if (this.state.viewModeEnabled) { if (this.state.viewModeEnabled) {
return; return;
} }
if (event[KEYS.CTRL_OR_CMD] && !event.repeat) { if (event[KEYS.CTRL_OR_CMD] && this.state.isBindingEnabled) {
if (getFeatureFlag("COMPLEX_BINDINGS")) { if (getFeatureFlag("COMPLEX_BINDINGS")) {
this.resetDelayedBindMode(); this.resetDelayedBindMode();
} }
flushSync(() => { flushSync(() => {
this.setState({ this.setState({ isBindingEnabled: false });
isBindingEnabled: this.state.bindingPreference !== "enabled",
});
}); });
maybeHandleArrowPointlikeDrag({ app: this, event }); maybeHandleArrowPointlikeDrag({ app: this, event });
@@ -5071,8 +4997,44 @@ class App extends React.Component<AppProps, AppState> {
}); });
} }
} }
} else if (
!event.ctrlKey &&
!event.altKey &&
!event.metaKey &&
!this.state.newElement &&
!this.state.selectionElement &&
!this.state.selectedElementsAreBeingDragged
) {
const shape = findShapeByKey(event.key, this);
if (shape) {
if (this.state.activeTool.type !== shape) {
trackEvent(
"toolbar",
shape,
`keyboard (${
this.editorInterface.formFactor === "phone"
? "mobile"
: "desktop"
})`,
);
}
if (shape === "arrow" && this.state.activeTool.type === "arrow") {
this.setState((prevState) => ({
currentItemArrowType:
prevState.currentItemArrowType === ARROW_TYPE.sharp
? ARROW_TYPE.round
: prevState.currentItemArrowType === ARROW_TYPE.round
? ARROW_TYPE.elbow
: ARROW_TYPE.sharp,
}));
}
this.setActiveTool({ type: shape });
event.stopPropagation();
} else if (event.key === KEYS.Q) {
this.toggleLock("keyboard");
event.stopPropagation();
}
} }
if (event.key === KEYS.SPACE && gesture.pointers.size === 0) { if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
isHoldingSpace = true; isHoldingSpace = true;
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB); setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
@@ -5095,6 +5057,8 @@ class App extends React.Component<AppProps, AppState> {
if ( if (
event.key === KEYS.G && event.key === KEYS.G &&
(hasBackground(this.state.activeTool.type) || (hasBackground(this.state.activeTool.type) ||
this.state.activeTool.type === "frame" ||
selectedElements.some((element) => isFrameElement(element)) ||
selectedElements.some((element) => hasBackground(element.type))) selectedElements.some((element) => hasBackground(element.type)))
) { ) {
this.setState({ openPopup: "elementBackground" }); this.setState({ openPopup: "elementBackground" });
@@ -5136,6 +5100,15 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
if (this.state.activeTool.type === "laser") {
this.setActiveTool({ type: this.state.preferredSelectionTool.type });
} else {
this.setActiveTool({ type: "laser" });
}
return;
}
if ( if (
event[KEYS.CTRL_OR_CMD] && event[KEYS.CTRL_OR_CMD] &&
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
@@ -5162,8 +5135,7 @@ class App extends React.Component<AppProps, AppState> {
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => { private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
if (event.key === KEYS.SPACE) { if (event.key === KEYS.SPACE) {
if ( if (
(this.state.viewModeEnabled && this.state.viewModeEnabled ||
this.state.activeTool.type !== "laser") ||
this.state.openDialog?.name === "elementLinkSelector" this.state.openDialog?.name === "elementLinkSelector"
) { ) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB); setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
@@ -5222,13 +5194,10 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
} }
if (!event[KEYS.CTRL_OR_CMD]) { if (!event[KEYS.CTRL_OR_CMD] && !this.state.isBindingEnabled) {
const preferenceEnabled = this.state.bindingPreference === "enabled"; flushSync(() => {
if (this.state.isBindingEnabled !== preferenceEnabled) { this.setState({ isBindingEnabled: true });
flushSync(() => { });
this.setState({ isBindingEnabled: preferenceEnabled });
});
}
maybeHandleArrowPointlikeDrag({ app: this, event }); maybeHandleArrowPointlikeDrag({ app: this, event });
} }
@@ -6280,8 +6249,9 @@ class App extends React.Component<AppProps, AppState> {
} }
}; };
private handleElementLinkClick = ( private redirectToLink = (
event: React.PointerEvent<HTMLCanvasElement>, event: React.PointerEvent<HTMLCanvasElement>,
isTouchScreen: boolean,
) => { ) => {
const draggedDistance = pointDistance( const draggedDistance = pointDistance(
pointFrom( pointFrom(
@@ -6855,10 +6825,6 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
if (isEraserActive(this.state)) {
return;
}
const hitElementMightBeLocked = this.getElementAtPosition( const hitElementMightBeLocked = this.getElementAtPosition(
scenePointerX, scenePointerX,
scenePointerY, scenePointerY,
@@ -6875,25 +6841,18 @@ class App extends React.Component<AppProps, AppState> {
hitElement = hitElementMightBeLocked; hitElement = hitElementMightBeLocked;
} }
if ( this.hitLinkElement = this.getElementLinkAtPosition(
!this.handleIframeLikeElementHover({ scenePointer,
hitElement, hitElementMightBeLocked,
scenePointer, );
moveEvent: event, if (isEraserActive(this.state)) {
}) return;
) {
this.hitLinkElement = this.getElementLinkAtPosition(
scenePointer,
hitElementMightBeLocked,
);
} }
if ( if (
this.hitLinkElement && this.hitLinkElement &&
!this.state.selectedElementIds[this.hitLinkElement.id] !this.state.selectedElementIds[this.hitLinkElement.id]
) { ) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
showHyperlinkTooltip( showHyperlinkTooltip(
this.hitLinkElement, this.hitLinkElement,
this.state, this.state,
@@ -6902,6 +6861,11 @@ class App extends React.Component<AppProps, AppState> {
} else { } else {
hideHyperlinkToolip(); hideHyperlinkToolip();
if (isLaserTool) { if (isLaserTool) {
this.handleIframeLikeElementHover({
hitElement,
scenePointer,
moveEvent: event,
});
return; return;
} }
if ( if (
@@ -6936,10 +6900,15 @@ class App extends React.Component<AppProps, AppState> {
!hitElement?.locked !hitElement?.locked
) { ) {
if ( if (
!hitElement || !this.handleIframeLikeElementHover({
// Elbow arrows can only be moved when unconnected hitElement,
!isElbowArrow(hitElement) || scenePointer,
!(hitElement.startBinding || hitElement.endBinding) moveEvent: event,
}) &&
(!hitElement ||
// Elbow arrows can only be moved when unconnected
!isElbowArrow(hitElement) ||
!(hitElement.startBinding || hitElement.endBinding))
) { ) {
if ( if (
this.state.activeTool.type !== "lasso" || this.state.activeTool.type !== "lasso" ||
@@ -7146,14 +7115,6 @@ class App extends React.Component<AppProps, AppState> {
private handleCanvasPointerDown = ( private handleCanvasPointerDown = (
event: React.PointerEvent<HTMLElement>, event: React.PointerEvent<HTMLElement>,
) => { ) => {
// If Ctrl is not held, ensure isBindingEnabled reflects the user preference.
if (!event.ctrlKey) {
const preferenceEnabled = this.state.bindingPreference === "enabled";
if (this.state.isBindingEnabled !== preferenceEnabled) {
this.setState({ isBindingEnabled: preferenceEnabled });
}
}
const scenePointer = viewportCoordsToSceneCoords(event, this.state); const scenePointer = viewportCoordsToSceneCoords(event, this.state);
const { x: scenePointerX, y: scenePointerY } = scenePointer; const { x: scenePointerX, y: scenePointerY } = scenePointer;
this.lastPointerMoveCoords = { this.lastPointerMoveCoords = {
@@ -7374,6 +7335,7 @@ class App extends React.Component<AppProps, AppState> {
} }
this.clearSelectionIfNotUsingSelection(); this.clearSelectionIfNotUsingSelection();
this.updateBindingEnabledOnPointerMove(event);
if (this.handleSelectionOnPointerDown(event, pointerDownState)) { if (this.handleSelectionOnPointerDown(event, pointerDownState)) {
return; return;
@@ -7596,13 +7558,6 @@ class App extends React.Component<AppProps, AppState> {
this.removePointer(event); this.removePointer(event);
this.lastPointerUpEvent = event; this.lastPointerUpEvent = event;
if (!event.ctrlKey) {
const preferenceEnabled = this.state.bindingPreference === "enabled";
if (this.state.isBindingEnabled !== preferenceEnabled) {
this.setState({ isBindingEnabled: preferenceEnabled });
}
}
const scenePointer = viewportCoordsToSceneCoords( const scenePointer = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY }, { clientX: event.clientX, clientY: event.clientY },
this.state, this.state,
@@ -7635,7 +7590,7 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement && this.hitLinkElement &&
!this.state.selectedElementIds[this.hitLinkElement.id] !this.state.selectedElementIds[this.hitLinkElement.id]
) { ) {
this.handleElementLinkClick(event); this.redirectToLink(event, this.editorInterface.isTouchScreen);
} else if (this.state.viewModeEnabled) { } else if (this.state.viewModeEnabled) {
this.setState({ this.setState({
activeEmbeddable: null, activeEmbeddable: null,
@@ -7695,8 +7650,7 @@ class App extends React.Component<AppProps, AppState> {
(event.button === POINTER_BUTTON.WHEEL || (event.button === POINTER_BUTTON.WHEEL ||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) || (event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
isHandToolActive(this.state) || isHandToolActive(this.state) ||
(this.state.viewModeEnabled && this.state.viewModeEnabled)
this.state.activeTool.type !== "laser"))
) )
) { ) {
return false; return false;
@@ -7774,10 +7728,7 @@ class App extends React.Component<AppProps, AppState> {
lastPointerUp = null; lastPointerUp = null;
isPanning = false; isPanning = false;
if (!isHoldingSpace) { if (!isHoldingSpace) {
if ( if (this.state.viewModeEnabled) {
this.state.viewModeEnabled &&
this.state.activeTool.type !== "laser"
) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB); setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
} else { } else {
setCursorForShape(this.interactiveCanvas, this.state); setCursorForShape(this.interactiveCanvas, this.state);
@@ -8658,9 +8609,7 @@ class App extends React.Component<AppProps, AppState> {
): void => { ): void => {
if (event.ctrlKey) { if (event.ctrlKey) {
flushSync(() => { flushSync(() => {
this.setState({ this.setState({ isBindingEnabled: false });
isBindingEnabled: this.state.bindingPreference !== "enabled",
});
}); });
} }
@@ -11354,6 +11303,15 @@ class App extends React.Component<AppProps, AppState> {
this.addNewImagesToImageCache(); this.addNewImagesToImageCache();
}, IMAGE_RENDER_TIMEOUT); }, IMAGE_RENDER_TIMEOUT);
private updateBindingEnabledOnPointerMove = (
event: React.PointerEvent<HTMLElement>,
) => {
const shouldEnableBinding = shouldEnableBindingForPointerEvent(event);
if (this.state.isBindingEnabled !== shouldEnableBinding) {
this.setState({ isBindingEnabled: shouldEnableBinding });
}
};
private clearSelection(hitElement: ExcalidrawElement | null): void { private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({ this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState), selectedElementIds: makeNextSelectedElementIds({}, prevState),
@@ -11489,6 +11447,7 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
this.scene.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
fileHandle, fileHandle,
this.schemaMigrationRegistry,
); );
this.syncActionResult({ this.syncActionResult({
...scene, ...scene,
@@ -11535,7 +11494,11 @@ class App extends React.Component<AppProps, AppState> {
); );
// legacy library dataTransfer format // legacy library dataTransfer format
} else if (excalidrawLibrary_data) { } else if (excalidrawLibrary_data) {
libraryItems = parseLibraryJSON(excalidrawLibrary_data); libraryItems = parseLibraryJSON(
excalidrawLibrary_data,
"unpublished",
this.schemaMigrationRegistry,
);
} }
if (libraryItems?.length) { if (libraryItems?.length) {
libraryItems = libraryItems.map((item) => ({ libraryItems = libraryItems.map((item) => ({
@@ -11605,6 +11568,7 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
elements, elements,
fileHandle, fileHandle,
this.schemaMigrationRegistry,
); );
} catch (error: any) { } catch (error: any) {
const imageSceneDataError = error instanceof ImageSceneDataError; const imageSceneDataError = error instanceof ImageSceneDataError;
@@ -12115,8 +12079,6 @@ class App extends React.Component<AppProps, AppState> {
CONTEXT_MENU_SEPARATOR, CONTEXT_MENU_SEPARATOR,
actionToggleGridMode, actionToggleGridMode,
actionToggleObjectsSnapMode, actionToggleObjectsSnapMode,
actionToggleArrowBinding,
actionToggleMidpointSnapping,
actionToggleZenMode, actionToggleZenMode,
actionToggleViewMode, actionToggleViewMode,
actionToggleStats, actionToggleStats,
@@ -15,7 +15,7 @@ export type CommandPaletteItem = {
category: string; category: string;
order?: number; order?: number;
predicate?: boolean | Action["predicate"]; predicate?: boolean | Action["predicate"];
shortcut?: string | null; shortcut?: string;
/** if false, command will not show while in view mode */ /** if false, command will not show while in view mode */
viewMode?: boolean; viewMode?: boolean;
perform: (data: { perform: (data: {
+13 -4
View File
@@ -20,6 +20,7 @@ import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
import { actionToggleStats } from "../actions"; import { actionToggleStats } from "../actions";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { isHandToolActive } from "../appState";
import { TunnelsContext, useInitializeTunnels } from "../context/tunnels"; import { TunnelsContext, useInitializeTunnels } from "../context/tunnels";
import { UIAppStateContext } from "../context/ui-appState"; import { UIAppStateContext } from "../context/ui-appState";
import { useAtom, useAtomValue } from "../editor-jotai"; import { useAtom, useAtomValue } from "../editor-jotai";
@@ -54,6 +55,7 @@ import ElementLinkDialog from "./ElementLinkDialog";
import { ErrorDialog } from "./ErrorDialog"; import { ErrorDialog } from "./ErrorDialog";
import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper"; import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
import { FixedSideContainer } from "./FixedSideContainer"; import { FixedSideContainer } from "./FixedSideContainer";
import { HandButton } from "./HandButton";
import { HelpDialog } from "./HelpDialog"; import { HelpDialog } from "./HelpDialog";
import { HintViewer } from "./HintViewer"; import { HintViewer } from "./HintViewer";
import { ImageExportDialog } from "./ImageExportDialog"; import { ImageExportDialog } from "./ImageExportDialog";
@@ -357,6 +359,13 @@ const LayerUI = ({
<div className="App-toolbar__divider" /> <div className="App-toolbar__divider" />
<HandButton
checked={isHandToolActive(appState)}
onChange={() => onHandToolToggle()}
title={t("toolBar.hand")}
isMobile
/>
<ShapesSwitcher <ShapesSwitcher
setAppState={setAppState} setAppState={setAppState}
activeTool={appState.activeTool} activeTool={appState.activeTool}
@@ -556,13 +565,13 @@ const LayerUI = ({
<tunnels.OverwriteConfirmDialogTunnel.Out /> <tunnels.OverwriteConfirmDialogTunnel.Out />
{renderImageExportDialog()} {renderImageExportDialog()}
{renderJSONExportDialog()} {renderJSONExportDialog()}
{appState.openDialog?.name === "charts" && ( {appState.pasteDialog.shown && (
<PasteChartDialog <PasteChartDialog
data={appState.openDialog.data} setAppState={setAppState}
rawText={appState.openDialog.rawText} appState={appState}
onClose={() => onClose={() =>
setAppState({ setAppState({
openDialog: null, pasteDialog: { shown: false, data: null },
}) })
} }
/> />
@@ -2,40 +2,6 @@
.excalidraw { .excalidraw {
.PasteChartDialog { .PasteChartDialog {
.PasteChartDialog__title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.PasteChartDialog__titleText {
min-width: 0;
}
.PasteChartDialog__reshuffleBtn {
margin-left: auto;
flex: 0 0 auto;
width: 1rem;
height: 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
cursor: pointer;
color: var(--text-primary-color);
transition: transform 120ms ease, background-color 120ms ease,
color 120ms ease;
user-select: none;
&:hover {
color: $color-blue-6;
}
&:active {
transform: scale(0.94);
}
}
@include isMobile { @include isMobile {
.Island { .Island {
display: flex; display: flex;
@@ -45,61 +11,35 @@
.container { .container {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: space-around;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem;
@include isMobile { @include isMobile {
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: stretch;
} }
} }
.ChartPreview { .ChartPreview {
width: 260px; margin: 8px;
min-height: 190px; text-align: center;
border-radius: 8px; width: 192px;
padding: 10px; height: 128px;
border-radius: 2px;
padding: 1px;
border: 1px solid $color-gray-4; border: 1px solid $color-gray-4;
display: flex; display: flex;
flex-direction: column; align-items: center;
align-items: stretch; justify-content: center;
justify-content: flex-start;
gap: 10px;
background: transparent; background: transparent;
.ChartPreview__canvas { div {
display: flex; display: inline-block;
flex: 1;
align-items: center;
justify-content: center;
overflow: hidden;
}
.ChartPreview__label {
font-size: 0.875rem;
font-weight: 600;
line-height: 1;
text-align: center;
color: var(--text-primary-color);
} }
svg { svg {
max-height: 144px; max-height: 120px;
max-width: 100%; max-width: 186px;
} }
&:hover { &:hover {
border-color: $color-blue-5; padding: 0;
} border: 2px solid $color-blue-5;
&:active {
border-color: $color-blue-5;
box-shadow: 0 0 0 1px $color-blue-5;
transform: scale(0.98);
}
&:focus-visible {
border-color: $color-blue-5;
box-shadow: 0 0 0 1px $color-blue-5;
}
@include isMobile {
width: 100%;
min-height: 200px;
} }
} }
} }
@@ -1,57 +1,35 @@
import React, { useLayoutEffect, useRef, useState } from "react"; import React, { useLayoutEffect, useRef, useState } from "react";
import { newTextElement } from "@excalidraw/element";
import type { ChartType } from "@excalidraw/element/types"; import type { ChartType } from "@excalidraw/element/types";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { isSpreadsheetValidForChartType, renderSpreadsheet } from "../charts"; import { renderSpreadsheet } from "../charts";
import { t } from "../i18n"; import { t } from "../i18n";
import { exportToSvg } from "../scene/export"; import { exportToSvg } from "../scene/export";
import { useUIAppState } from "../context/ui-appState";
import { useApp } from "./App"; import { useApp } from "./App";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import "./PasteChartDialog.scss"; import "./PasteChartDialog.scss";
import { bucketFillIcon } from "./icons";
import type { ChartElements, Spreadsheet } from "../charts"; import type { ChartElements, Spreadsheet } from "../charts";
import type { UIAppState } from "../types";
type OnPlainTextPaste = (rawText: string) => void;
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void; type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
const getChartTypeLabel = (chartType: ChartType) => {
switch (chartType) {
case "bar":
return t("labels.chartType_bar");
case "line":
return t("labels.chartType_line");
case "radar":
return t("labels.chartType_radar");
default:
return chartType;
}
};
const ChartPreviewBtn = (props: { const ChartPreviewBtn = (props: {
spreadsheet: Spreadsheet | null; spreadsheet: Spreadsheet | null;
chartType: ChartType; chartType: ChartType;
colorSeed: number; selected: boolean;
onClick: OnInsertChart; onClick: OnInsertChart;
}) => { }) => {
const previewRef = useRef<HTMLDivElement | null>(null); const previewRef = useRef<HTMLDivElement | null>(null);
const [chartElements, setChartElements] = useState<ChartElements | null>( const [chartElements, setChartElements] = useState<ChartElements | null>(
null, null,
); );
const { theme } = useUIAppState();
useLayoutEffect(() => { useLayoutEffect(() => {
if (!props.spreadsheet) { if (!props.spreadsheet) {
setChartElements(null);
return; return;
} }
@@ -60,13 +38,7 @@ const ChartPreviewBtn = (props: {
props.spreadsheet, props.spreadsheet,
0, 0,
0, 0,
props.colorSeed,
); );
if (!elements) {
setChartElements(null);
previewRef.current?.replaceChildren();
return;
}
setChartElements(elements); setChartElements(elements);
let svg: SVGSVGElement; let svg: SVGSVGElement;
const previewNode = previewRef.current!; const previewNode = previewRef.current!;
@@ -77,7 +49,6 @@ const ChartPreviewBtn = (props: {
{ {
exportBackground: false, exportBackground: false,
viewBackgroundColor: "#fff", viewBackgroundColor: "#fff",
exportWithDarkMode: theme === "dark",
}, },
null, // files null, // files
{ {
@@ -87,108 +58,42 @@ const ChartPreviewBtn = (props: {
svg.querySelector(".style-fonts")?.remove(); svg.querySelector(".style-fonts")?.remove();
previewNode.replaceChildren(); previewNode.replaceChildren();
previewNode.appendChild(svg); previewNode.appendChild(svg);
if (props.selected) {
(previewNode.parentNode as HTMLDivElement).focus();
}
})(); })();
return () => { return () => {
previewNode.replaceChildren(); previewNode.replaceChildren();
}; };
}, [props.spreadsheet, props.chartType, props.colorSeed, theme]); }, [props.spreadsheet, props.chartType, props.selected]);
const chartTypeLabel = getChartTypeLabel(props.chartType);
return ( return (
<button <button
type="button" type="button"
className="ChartPreview" className="ChartPreview"
aria-label={chartTypeLabel}
onClick={() => { onClick={() => {
if (chartElements) { if (chartElements) {
props.onClick(props.chartType, chartElements); props.onClick(props.chartType, chartElements);
} }
}} }}
> >
<div className="ChartPreview__canvas" ref={previewRef} /> <div ref={previewRef} />
<div className="ChartPreview__label">{chartTypeLabel}</div>
</button>
);
};
const PlainTextPreviewBtn = (props: {
rawText: string;
onClick: OnPlainTextPaste;
}) => {
const previewRef = useRef<HTMLDivElement | null>(null);
const { theme } = useUIAppState();
useLayoutEffect(() => {
if (!props.rawText) {
return;
}
const textElement = newTextElement({
text: props.rawText,
x: 0,
y: 0,
});
const previewNode = previewRef.current!;
(async () => {
const svg = await exportToSvg(
[textElement],
{
exportBackground: false,
viewBackgroundColor: "#fff",
exportWithDarkMode: theme === "dark",
},
null,
{
skipInliningFonts: true,
},
);
svg.querySelector(".style-fonts")?.remove();
previewNode.replaceChildren();
previewNode.appendChild(svg);
})();
return () => {
previewNode.replaceChildren();
};
}, [props.rawText, theme]);
return (
<button
type="button"
className="ChartPreview"
aria-label={t("labels.chartType_plaintext")}
onClick={() => {
props.onClick(props.rawText);
}}
>
<div className="ChartPreview__canvas" ref={previewRef} />
<div className="ChartPreview__label">
{t("labels.chartType_plaintext")}
</div>
</button> </button>
); );
}; };
export const PasteChartDialog = ({ export const PasteChartDialog = ({
data, setAppState,
rawText, appState,
onClose, onClose,
}: { }: {
data: Spreadsheet; appState: UIAppState;
rawText: string;
onClose: () => void; onClose: () => void;
setAppState: React.Component<any, UIAppState>["setState"];
}) => { }) => {
const { onInsertElements, focusContainer } = useApp(); const { onInsertElements } = useApp();
const [colorSeed, setColorSeed] = useState(Math.random());
const handleReshuffleColors = React.useCallback(() => {
setColorSeed(Math.random());
}, []);
const handleClose = React.useCallback(() => { const handleClose = React.useCallback(() => {
if (onClose) { if (onClose) {
onClose(); onClose();
@@ -198,72 +103,36 @@ export const PasteChartDialog = ({
const handleChartClick = (chartType: ChartType, elements: ChartElements) => { const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
onInsertElements(elements); onInsertElements(elements);
trackEvent("paste", "chart", chartType); trackEvent("paste", "chart", chartType);
onClose(); setAppState({
focusContainer(); currentChartType: chartType,
}; pasteDialog: {
shown: false,
const handlePlainTextClick = (rawText: string) => { data: null,
const textElement = newTextElement({ },
text: rawText,
x: 0,
y: 0,
}); });
onInsertElements([textElement]);
trackEvent("paste", "chart", "plaintext");
onClose();
focusContainer();
}; };
return ( return (
<Dialog <Dialog
size="regular" size="small"
onCloseRequest={handleClose} onCloseRequest={handleClose}
title={ title={t("labels.pasteCharts")}
<div className="PasteChartDialog__title">
<div className="PasteChartDialog__titleText">
{t("labels.pasteCharts")}
</div>
<div
className="PasteChartDialog__reshuffleBtn"
onClick={handleReshuffleColors}
role="button"
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleReshuffleColors();
}
}}
>
{bucketFillIcon}
</div>
</div>
}
className={"PasteChartDialog"} className={"PasteChartDialog"}
autofocus={false} autofocus={false}
> >
<div className={"container"}> <div className={"container"}>
{(["bar", "line", "radar"] as const).map((chartType) => { <ChartPreviewBtn
if (!isSpreadsheetValidForChartType(data, chartType)) { chartType="bar"
return null; spreadsheet={appState.pasteDialog.data}
} selected={appState.currentChartType === "bar"}
onClick={handleChartClick}
return ( />
<ChartPreviewBtn <ChartPreviewBtn
key={chartType} chartType="line"
chartType={chartType} spreadsheet={appState.pasteDialog.data}
spreadsheet={data} selected={appState.currentChartType === "line"}
colorSeed={colorSeed} onClick={handleChartClick}
onClick={handleChartClick} />
/>
);
})}
{rawText && (
<PlainTextPreviewBtn
rawText={rawText}
onClick={handlePlainTextClick}
/>
)}
</div> </div>
</Dialog> </Dialog>
); );
@@ -202,11 +202,9 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
style={{ style={{
width: props.appState.width, width: props.appState.width,
height: props.appState.height, height: props.appState.height,
cursor: cursor: props.appState.viewModeEnabled
props.appState.viewModeEnabled && ? CURSOR_TYPE.GRAB
props.appState.activeTool.type !== "laser" : CURSOR_TYPE.AUTO,
? CURSOR_TYPE.GRAB
: CURSOR_TYPE.AUTO,
}} }}
width={props.appState.width * props.scale} width={props.appState.width * props.scale}
height={props.appState.height * props.scale} height={props.appState.height * props.scale}
@@ -235,7 +233,6 @@ const getRelevantAppStateProps = (
width: appState.width, width: appState.width,
height: appState.height, height: appState.height,
viewModeEnabled: appState.viewModeEnabled, viewModeEnabled: appState.viewModeEnabled,
activeTool: appState.activeTool,
openDialog: appState.openDialog, openDialog: appState.openDialog,
editingGroupId: appState.editingGroupId, editingGroupId: appState.editingGroupId,
selectedElementIds: appState.selectedElementIds, selectedElementIds: appState.selectedElementIds,
@@ -249,7 +246,6 @@ const getRelevantAppStateProps = (
multiElement: appState.multiElement, multiElement: appState.multiElement,
newElement: appState.newElement, newElement: appState.newElement,
isBindingEnabled: appState.isBindingEnabled, isBindingEnabled: appState.isBindingEnabled,
isMidpointSnappingEnabled: appState.isMidpointSnappingEnabled,
suggestedBinding: appState.suggestedBinding, suggestedBinding: appState.suggestedBinding,
isRotating: appState.isRotating, isRotating: appState.isRotating,
elementsToHighlight: appState.elementsToHighlight, elementsToHighlight: appState.elementsToHighlight,
@@ -266,7 +262,6 @@ const getRelevantAppStateProps = (
frameRendering: appState.frameRendering, frameRendering: appState.frameRendering,
shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom, shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
exportScale: appState.exportScale, exportScale: appState.exportScale,
currentItemArrowType: appState.currentItemArrowType,
}); });
const areEqual = ( const areEqual = (
@@ -181,21 +181,6 @@
box-shadow: 0 0 0 1px var(--color-brand-active); box-shadow: 0 0 0 1px var(--color-brand-active);
} }
&[disabled] {
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
&:hover {
background-color: transparent;
}
&:active {
background-color: transparent;
box-shadow: none;
}
}
svg { svg {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
@@ -9,9 +9,7 @@ import {
actionLoadScene, actionLoadScene,
actionSaveToActiveFile, actionSaveToActiveFile,
actionShortcuts, actionShortcuts,
actionToggleArrowBinding,
actionToggleGridMode, actionToggleGridMode,
actionToggleMidpointSnapping,
actionToggleObjectsSnapMode, actionToggleObjectsSnapMode,
actionToggleSearchMenu, actionToggleSearchMenu,
actionToggleStats, actionToggleStats,
@@ -445,41 +443,6 @@ const PreferencesToggleSnapModeItem = () => {
); );
}; };
const PreferencesToggleArrowBindingItem = () => {
const { t } = useI18n();
const actionManager = useExcalidrawActionManager();
const appState = useUIAppState();
return (
<DropdownMenuItemCheckbox
checked={appState.bindingPreference === "enabled"}
onSelect={(event) => {
actionManager.executeAction(actionToggleArrowBinding);
event.preventDefault();
}}
>
{t("labels.arrowBinding")}
</DropdownMenuItemCheckbox>
);
};
const PreferencesToggleMidpointSnappingItem = () => {
const { t } = useI18n();
const actionManager = useExcalidrawActionManager();
const appState = useUIAppState();
return (
<DropdownMenuItemCheckbox
checked={appState.isMidpointSnappingEnabled}
disabled={appState.bindingPreference === "disabled"}
onSelect={(event) => {
actionManager.executeAction(actionToggleMidpointSnapping);
event.preventDefault();
}}
>
{t("labels.midpointSnapping")}
</DropdownMenuItemCheckbox>
);
};
export const PreferencesToggleGridModeItem = () => { export const PreferencesToggleGridModeItem = () => {
const { t } = useI18n(); const { t } = useI18n();
const actionManager = useExcalidrawActionManager(); const actionManager = useExcalidrawActionManager();
@@ -575,8 +538,6 @@ export const Preferences = ({
<PreferencesToggleZenModeItem /> <PreferencesToggleZenModeItem />
<PreferencesToggleViewModeItem /> <PreferencesToggleViewModeItem />
<PreferencesToggleElementPropertiesItem /> <PreferencesToggleElementPropertiesItem />
<PreferencesToggleArrowBindingItem />
<PreferencesToggleMidpointSnappingItem />
</> </>
)} )}
{additionalItems} {additionalItems}
@@ -587,8 +548,6 @@ export const Preferences = ({
Preferences.ToggleToolLock = PreferencesToggleToolLockItem; Preferences.ToggleToolLock = PreferencesToggleToolLockItem;
Preferences.ToggleSnapMode = PreferencesToggleSnapModeItem; Preferences.ToggleSnapMode = PreferencesToggleSnapModeItem;
Preferences.ToggleArrowBinding = PreferencesToggleArrowBindingItem;
Preferences.ToggleMidpointSnapping = PreferencesToggleMidpointSnappingItem;
Preferences.ToggleGridMode = PreferencesToggleGridModeItem; Preferences.ToggleGridMode = PreferencesToggleGridModeItem;
Preferences.ToggleZenMode = PreferencesToggleZenModeItem; Preferences.ToggleZenMode = PreferencesToggleZenModeItem;
Preferences.ToggleViewMode = PreferencesToggleViewModeItem; Preferences.ToggleViewMode = PreferencesToggleViewModeItem;
-29
View File
@@ -11,28 +11,17 @@ import {
TextIcon, TextIcon,
ImageIcon, ImageIcon,
EraserIcon, EraserIcon,
laserPointerToolIcon,
handIcon,
} from "./icons"; } from "./icons";
import type { AppClassProperties } from "../types"; import type { AppClassProperties } from "../types";
export const SHAPES = [ export const SHAPES = [
{
icon: handIcon,
value: "hand",
key: KEYS.H,
numericKey: null,
fillable: false,
toolbar: true,
},
{ {
icon: SelectionIcon, icon: SelectionIcon,
value: "selection", value: "selection",
key: KEYS.V, key: KEYS.V,
numericKey: KEYS["1"], numericKey: KEYS["1"],
fillable: true, fillable: true,
toolbar: true,
}, },
{ {
icon: RectangleIcon, icon: RectangleIcon,
@@ -40,7 +29,6 @@ export const SHAPES = [
key: KEYS.R, key: KEYS.R,
numericKey: KEYS["2"], numericKey: KEYS["2"],
fillable: true, fillable: true,
toolbar: true,
}, },
{ {
icon: DiamondIcon, icon: DiamondIcon,
@@ -48,7 +36,6 @@ export const SHAPES = [
key: KEYS.D, key: KEYS.D,
numericKey: KEYS["3"], numericKey: KEYS["3"],
fillable: true, fillable: true,
toolbar: true,
}, },
{ {
icon: EllipseIcon, icon: EllipseIcon,
@@ -56,7 +43,6 @@ export const SHAPES = [
key: KEYS.O, key: KEYS.O,
numericKey: KEYS["4"], numericKey: KEYS["4"],
fillable: true, fillable: true,
toolbar: true,
}, },
{ {
icon: ArrowIcon, icon: ArrowIcon,
@@ -64,7 +50,6 @@ export const SHAPES = [
key: KEYS.A, key: KEYS.A,
numericKey: KEYS["5"], numericKey: KEYS["5"],
fillable: true, fillable: true,
toolbar: true,
}, },
{ {
icon: LineIcon, icon: LineIcon,
@@ -72,7 +57,6 @@ export const SHAPES = [
key: KEYS.L, key: KEYS.L,
numericKey: KEYS["6"], numericKey: KEYS["6"],
fillable: true, fillable: true,
toolbar: true,
}, },
{ {
icon: FreedrawIcon, icon: FreedrawIcon,
@@ -80,7 +64,6 @@ export const SHAPES = [
key: [KEYS.P, KEYS.X], key: [KEYS.P, KEYS.X],
numericKey: KEYS["7"], numericKey: KEYS["7"],
fillable: false, fillable: false,
toolbar: true,
}, },
{ {
icon: TextIcon, icon: TextIcon,
@@ -88,7 +71,6 @@ export const SHAPES = [
key: KEYS.T, key: KEYS.T,
numericKey: KEYS["8"], numericKey: KEYS["8"],
fillable: false, fillable: false,
toolbar: true,
}, },
{ {
icon: ImageIcon, icon: ImageIcon,
@@ -96,7 +78,6 @@ export const SHAPES = [
key: null, key: null,
numericKey: KEYS["9"], numericKey: KEYS["9"],
fillable: false, fillable: false,
toolbar: true,
}, },
{ {
icon: EraserIcon, icon: EraserIcon,
@@ -104,15 +85,6 @@ export const SHAPES = [
key: KEYS.E, key: KEYS.E,
numericKey: KEYS["0"], numericKey: KEYS["0"],
fillable: false, fillable: false,
toolbar: true,
},
{
icon: laserPointerToolIcon,
value: "laser",
key: KEYS.K,
numericKey: null,
fillable: false,
toolbar: false,
}, },
] as const; ] as const;
@@ -125,7 +97,6 @@ export const getToolbarTools = (app: AppClassProperties) => {
key: KEYS.V, key: KEYS.V,
numericKey: KEYS["1"], numericKey: KEYS["1"],
fillable: true, fillable: true,
toolbar: true,
}, },
...SHAPES.slice(1), ...SHAPES.slice(1),
] as const) ] as const)
+15 -2
View File
@@ -28,6 +28,7 @@ import {
import type { AppState, DataURL, LibraryItem } from "../types"; import type { AppState, DataURL, LibraryItem } from "../types";
import type { FileSystemHandle } from "browser-fs-access"; import type { FileSystemHandle } from "browser-fs-access";
import type { SchemaMigrationRegistry } from "./schema";
import type { ImportedLibraryData } from "./types"; import type { ImportedLibraryData } from "./types";
const parseFileContents = async (blob: Blob | File): Promise<string> => { const parseFileContents = async (blob: Blob | File): Promise<string> => {
@@ -141,6 +142,7 @@ export const loadSceneOrLibraryFromBlob = async (
localElements: readonly ExcalidrawElement[] | null, localElements: readonly ExcalidrawElement[] | null,
/** FileSystemHandle. Defaults to `blob.handle` if defined, otherwise null. */ /** FileSystemHandle. Defaults to `blob.handle` if defined, otherwise null. */
fileHandle?: FileSystemHandle | null, fileHandle?: FileSystemHandle | null,
schemaMigrationRegistry?: SchemaMigrationRegistry,
) => { ) => {
const contents = await parseFileContents(blob); const contents = await parseFileContents(blob);
let data; let data;
@@ -163,6 +165,7 @@ export const loadSceneOrLibraryFromBlob = async (
elements: restoreElements(data.elements, localElements, { elements: restoreElements(data.elements, localElements, {
repairBindings: true, repairBindings: true,
deleteInvisibleElements: true, deleteInvisibleElements: true,
schemaMigrationRegistry,
}), }),
appState: restoreAppState( appState: restoreAppState(
{ {
@@ -200,12 +203,14 @@ export const loadFromBlob = async (
localElements: readonly ExcalidrawElement[] | null, localElements: readonly ExcalidrawElement[] | null,
/** FileSystemHandle. Defaults to `blob.handle` if defined, otherwise null. */ /** FileSystemHandle. Defaults to `blob.handle` if defined, otherwise null. */
fileHandle?: FileSystemHandle | null, fileHandle?: FileSystemHandle | null,
schemaMigrationRegistry?: SchemaMigrationRegistry,
) => { ) => {
const ret = await loadSceneOrLibraryFromBlob( const ret = await loadSceneOrLibraryFromBlob(
blob, blob,
localAppState, localAppState,
localElements, localElements,
fileHandle, fileHandle,
schemaMigrationRegistry,
); );
if (ret.type !== MIME_TYPES.excalidraw) { if (ret.type !== MIME_TYPES.excalidraw) {
throw new Error("Error: invalid file"); throw new Error("Error: invalid file");
@@ -216,20 +221,28 @@ export const loadFromBlob = async (
export const parseLibraryJSON = ( export const parseLibraryJSON = (
json: string, json: string,
defaultStatus: LibraryItem["status"] = "unpublished", defaultStatus: LibraryItem["status"] = "unpublished",
schemaMigrationRegistry?: SchemaMigrationRegistry,
) => { ) => {
const data: ImportedLibraryData | undefined = JSON.parse(json); const data: ImportedLibraryData | undefined = JSON.parse(json);
if (!isValidLibrary(data)) { if (!isValidLibrary(data)) {
throw new Error("Invalid library"); throw new Error("Invalid library");
} }
const libraryItems = data.libraryItems || data.library; const libraryItems = data.libraryItems || data.library;
return restoreLibraryItems(libraryItems, defaultStatus); return restoreLibraryItems(libraryItems, defaultStatus, {
schemaMigrationRegistry,
});
}; };
export const loadLibraryFromBlob = async ( export const loadLibraryFromBlob = async (
blob: Blob, blob: Blob,
defaultStatus: LibraryItem["status"] = "unpublished", defaultStatus: LibraryItem["status"] = "unpublished",
schemaMigrationRegistry?: SchemaMigrationRegistry,
) => { ) => {
return parseLibraryJSON(await parseFileContents(blob), defaultStatus); return parseLibraryJSON(
await parseFileContents(blob),
defaultStatus,
schemaMigrationRegistry,
);
}; };
export const canvasToBlob = async ( export const canvasToBlob = async (
+9 -1
View File
@@ -14,6 +14,7 @@ import { isImageFileHandle, loadFromBlob } from "./blob";
import { fileOpen, fileSave } from "./filesystem"; import { fileOpen, fileSave } from "./filesystem";
import type { AppState, BinaryFiles, LibraryItems } from "../types"; import type { AppState, BinaryFiles, LibraryItems } from "../types";
import type { SchemaMigrationRegistry } from "./schema";
import type { import type {
ExportedDataState, ExportedDataState,
ImportedDataState, ImportedDataState,
@@ -93,6 +94,7 @@ export const saveAsJSON = async (
export const loadFromJSON = async ( export const loadFromJSON = async (
localAppState: AppState, localAppState: AppState,
localElements: readonly ExcalidrawElement[] | null, localElements: readonly ExcalidrawElement[] | null,
schemaMigrationRegistry?: SchemaMigrationRegistry,
) => { ) => {
const file = await fileOpen({ const file = await fileOpen({
description: "Excalidraw files", description: "Excalidraw files",
@@ -100,7 +102,13 @@ export const loadFromJSON = async (
// gets resolved. Else, iOS users cannot open `.excalidraw` files. // gets resolved. Else, iOS users cannot open `.excalidraw` files.
// extensions: ["json", "excalidraw", "png", "svg"], // extensions: ["json", "excalidraw", "png", "svg"],
}); });
return loadFromBlob(file, localAppState, localElements, file.handle); return loadFromBlob(
file,
localAppState,
localElements,
file.handle,
schemaMigrationRegistry,
);
}; };
export const isValidExcalidrawData = (data?: { export const isValidExcalidrawData = (data?: {
+120 -63
View File
@@ -35,6 +35,7 @@ import { loadLibraryFromBlob } from "./blob";
import { restoreLibraryItems } from "./restore"; import { restoreLibraryItems } from "./restore";
import type App from "../components/App"; import type App from "../components/App";
import type { SchemaMigrationRegistry } from "./schema";
import type { import type {
LibraryItems, LibraryItems,
@@ -65,9 +66,9 @@ type LibraryUpdate = {
updatedItems: Map<LibraryItem["id"], LibraryItem>; updatedItems: Map<LibraryItem["id"], LibraryItem>;
}; };
// an object so that we can later add more properties to it without breaking, export type LibraryPersistedData = {
// such as schema version libraryItems: LibraryItems;
export type LibraryPersistedData = { libraryItems: LibraryItems }; };
const onLibraryUpdateEmitter = new Emitter< const onLibraryUpdateEmitter = new Emitter<
[update: LibraryUpdate, libraryItems: LibraryItems] [update: LibraryUpdate, libraryItems: LibraryItems]
@@ -99,7 +100,9 @@ export interface LibraryMigrationAdapter {
* loads data from legacy data source. Returns `null` if no data is * loads data from legacy data source. Returns `null` if no data is
* to be migrated. * to be migrated.
*/ */
load(): MaybePromise<{ libraryItems: LibraryItems_anyVersion } | null>; load(): MaybePromise<{
libraryItems: LibraryItems_anyVersion;
} | null>;
/** clears entire storage afterwards */ /** clears entire storage afterwards */
clear(): MaybePromise<void>; clear(): MaybePromise<void>;
@@ -314,9 +317,15 @@ class Library {
let nextItems; let nextItems;
if (source instanceof Blob) { if (source instanceof Blob) {
nextItems = await loadLibraryFromBlob(source, defaultStatus); nextItems = await loadLibraryFromBlob(
source,
defaultStatus,
this.app.getSchemaMigrationRegistry(),
);
} else { } else {
nextItems = restoreLibraryItems(source, defaultStatus); nextItems = restoreLibraryItems(source, defaultStatus, {
schemaMigrationRegistry: this.app.getSchemaMigrationRegistry(),
});
} }
if ( if (
!prompt || !prompt ||
@@ -549,12 +558,17 @@ class AdapterTransaction {
adapter: LibraryPersistenceAdapter, adapter: LibraryPersistenceAdapter,
source: LibraryAdatapterSource, source: LibraryAdatapterSource,
_queue = true, _queue = true,
schemaMigrationRegistry?: SchemaMigrationRegistry,
): Promise<LibraryItems> { ): Promise<LibraryItems> {
const task = () => const task = () =>
new Promise<LibraryItems>(async (resolve, reject) => { new Promise<LibraryItems>(async (resolve, reject) => {
try { try {
const data = await adapter.load({ source }); const data = await adapter.load({ source });
resolve(restoreLibraryItems(data?.libraryItems || [], "published")); resolve(
restoreLibraryItems(data?.libraryItems || [], "published", {
schemaMigrationRegistry,
}),
);
} catch (error: any) { } catch (error: any) {
reject(error); reject(error);
} }
@@ -569,22 +583,36 @@ class AdapterTransaction {
static run = async <T>( static run = async <T>(
adapter: LibraryPersistenceAdapter, adapter: LibraryPersistenceAdapter,
schemaMigrationRegistry: SchemaMigrationRegistry | undefined,
fn: (transaction: AdapterTransaction) => Promise<T>, fn: (transaction: AdapterTransaction) => Promise<T>,
) => { ) => {
const transaction = new AdapterTransaction(adapter); const transaction = new AdapterTransaction(
adapter,
schemaMigrationRegistry,
);
return AdapterTransaction.queue.push(() => fn(transaction)); return AdapterTransaction.queue.push(() => fn(transaction));
}; };
// ------------------ // ------------------
private adapter: LibraryPersistenceAdapter; private adapter: LibraryPersistenceAdapter;
private schemaMigrationRegistry: SchemaMigrationRegistry | undefined;
constructor(adapter: LibraryPersistenceAdapter) { constructor(
adapter: LibraryPersistenceAdapter,
schemaMigrationRegistry: SchemaMigrationRegistry | undefined,
) {
this.adapter = adapter; this.adapter = adapter;
this.schemaMigrationRegistry = schemaMigrationRegistry;
} }
getLibraryItems(source: LibraryAdatapterSource) { getLibraryItems(source: LibraryAdatapterSource) {
return AdapterTransaction.getLibraryItems(this.adapter, source, false); return AdapterTransaction.getLibraryItems(
this.adapter,
source,
false,
this.schemaMigrationRegistry,
);
} }
} }
@@ -607,68 +635,73 @@ export const getLibraryItemsHash = (items: LibraryItems) => {
const persistLibraryUpdate = async ( const persistLibraryUpdate = async (
adapter: LibraryPersistenceAdapter, adapter: LibraryPersistenceAdapter,
update: LibraryUpdate, update: LibraryUpdate,
schemaMigrationRegistry: SchemaMigrationRegistry | undefined,
): Promise<LibraryItems> => { ): Promise<LibraryItems> => {
try { try {
librarySaveCounter++; librarySaveCounter++;
return await AdapterTransaction.run(adapter, async (transaction) => { return await AdapterTransaction.run(
const nextLibraryItemsMap = arrayToMap( adapter,
await transaction.getLibraryItems("save"), schemaMigrationRegistry,
); async (transaction) => {
const nextLibraryItemsMap = arrayToMap(
await transaction.getLibraryItems("save"),
);
for (const [id] of update.deletedItems) { for (const [id] of update.deletedItems) {
nextLibraryItemsMap.delete(id); nextLibraryItemsMap.delete(id);
}
const addedItems: LibraryItem[] = [];
// we want to merge current library items with the ones stored in the
// DB so that we don't lose any elements that for some reason aren't
// in the current editor library, which could happen when:
//
// 1. we haven't received an update deleting some elements
// (in which case it's still better to keep them in the DB lest
// it was due to a different reason)
// 2. we keep a single DB for all active editors, but the editors'
// libraries aren't synced or there's a race conditions during
// syncing
// 3. some other race condition, e.g. during init where emit updates
// for partial updates (e.g. you install a 3rd party library and
// init from DB only after — we emit events for both updates)
for (const [id, item] of update.addedItems) {
if (nextLibraryItemsMap.has(id)) {
// replace item with latest version
// TODO we could prefer the newer item instead
nextLibraryItemsMap.set(id, item);
} else {
// we want to prepend the new items with the ones that are already
// in DB to preserve the ordering we do in editor (newly added
// items are added to the beginning)
addedItems.push(item);
} }
}
// replace existing items with their updated versions const addedItems: LibraryItem[] = [];
if (update.updatedItems) {
for (const [id, item] of update.updatedItems) { // we want to merge current library items with the ones stored in the
nextLibraryItemsMap.set(id, item); // DB so that we don't lose any elements that for some reason aren't
// in the current editor library, which could happen when:
//
// 1. we haven't received an update deleting some elements
// (in which case it's still better to keep them in the DB lest
// it was due to a different reason)
// 2. we keep a single DB for all active editors, but the editors'
// libraries aren't synced or there's a race conditions during
// syncing
// 3. some other race condition, e.g. during init where emit updates
// for partial updates (e.g. you install a 3rd party library and
// init from DB only after — we emit events for both updates)
for (const [id, item] of update.addedItems) {
if (nextLibraryItemsMap.has(id)) {
// replace item with latest version
// TODO we could prefer the newer item instead
nextLibraryItemsMap.set(id, item);
} else {
// we want to prepend the new items with the ones that are already
// in DB to preserve the ordering we do in editor (newly added
// items are added to the beginning)
addedItems.push(item);
}
} }
}
const nextLibraryItems = addedItems.concat( // replace existing items with their updated versions
Array.from(nextLibraryItemsMap.values()), if (update.updatedItems) {
); for (const [id, item] of update.updatedItems) {
nextLibraryItemsMap.set(id, item);
}
}
const version = getLibraryItemsHash(nextLibraryItems); const nextLibraryItems = addedItems.concat(
Array.from(nextLibraryItemsMap.values()),
);
if (version !== lastSavedLibraryItemsHash) { const version = getLibraryItemsHash(nextLibraryItems);
await adapter.save({ libraryItems: nextLibraryItems });
}
lastSavedLibraryItemsHash = version; if (version !== lastSavedLibraryItemsHash) {
await adapter.save({ libraryItems: nextLibraryItems });
}
return nextLibraryItems; lastSavedLibraryItemsHash = version;
});
return nextLibraryItems;
},
);
} finally { } finally {
librarySaveCounter--; librarySaveCounter--;
} }
@@ -852,16 +885,24 @@ export const useHandleLibrary = (
.then(async (libraryData) => { .then(async (libraryData) => {
let restoredData: LibraryItems | null = null; let restoredData: LibraryItems | null = null;
try { try {
const schemaMigrationRegistry =
optsRef.current.excalidrawAPI?.getSchemaMigrationRegistry();
// if no library data to migrate, assume no migration needed // if no library data to migrate, assume no migration needed
// and skip persisting to new data store, as well as well // and skip persisting to new data store, as well as well
// clearing the old store via `migrationAdapter.clear()` // clearing the old store via `migrationAdapter.clear()`
if (!libraryData) { if (!libraryData) {
return AdapterTransaction.getLibraryItems(adapter, "load"); return AdapterTransaction.getLibraryItems(
adapter,
"load",
true,
schemaMigrationRegistry,
);
} }
restoredData = restoreLibraryItems( restoredData = restoreLibraryItems(
libraryData.libraryItems || [], libraryData.libraryItems || [],
"published", "published",
{ schemaMigrationRegistry },
); );
// we don't queue this operation because it's running inside // we don't queue this operation because it's running inside
@@ -869,6 +910,7 @@ export const useHandleLibrary = (
const nextItems = await persistLibraryUpdate( const nextItems = await persistLibraryUpdate(
adapter, adapter,
createLibraryUpdate([], restoredData), createLibraryUpdate([], restoredData),
schemaMigrationRegistry,
); );
try { try {
await migrationAdapter.clear(); await migrationAdapter.clear();
@@ -891,12 +933,23 @@ export const useHandleLibrary = (
.catch((error: any) => { .catch((error: any) => {
console.error(`error during library migration: ${error.message}`); console.error(`error during library migration: ${error.message}`);
// as a default, load latest library from current data source // as a default, load latest library from current data source
return AdapterTransaction.getLibraryItems(adapter, "load"); return AdapterTransaction.getLibraryItems(
adapter,
"load",
true,
optsRef.current.excalidrawAPI?.getSchemaMigrationRegistry(),
);
}), }),
); );
} else { } else {
initDataPromise.resolve( initDataPromise.resolve(
promiseTry(AdapterTransaction.getLibraryItems, adapter, "load"), promiseTry(
AdapterTransaction.getLibraryItems,
adapter,
"load",
true,
optsRef.current.excalidrawAPI?.getSchemaMigrationRegistry(),
),
); );
} }
@@ -958,7 +1011,11 @@ export const useHandleLibrary = (
lastSavedLibraryItemsHash !== lastSavedLibraryItemsHash !==
getLibraryItemsHash(nextLibraryItems) getLibraryItemsHash(nextLibraryItems)
) { ) {
await persistLibraryUpdate(adapter, update); await persistLibraryUpdate(
adapter,
update,
optsRef.current.excalidrawAPI?.getSchemaMigrationRegistry(),
);
} }
} }
} catch (error: any) { } catch (error: any) {
+57 -43
View File
@@ -82,6 +82,10 @@ import {
getNormalizedZoom, getNormalizedZoom,
} from "../scene"; } from "../scene";
import { migrateElements } from "./schema";
import type { SchemaMigrationRegistry } from "./schema";
import type { import type {
AppState, AppState,
BinaryFiles, BinaryFiles,
@@ -155,7 +159,7 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
| ExcalidrawElbowArrowElement["startBinding"] | ExcalidrawElbowArrowElement["startBinding"]
| ExcalidrawElbowArrowElement["endBinding"] = { | ExcalidrawElbowArrowElement["endBinding"] = {
...binding, ...binding,
fixedPoint: normalizeFixedPoint(binding.fixedPoint), fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]),
mode: binding.mode || "orbit", mode: binding.mode || "orbit",
}; };
@@ -176,7 +180,7 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
return { return {
elementId: binding.elementId, elementId: binding.elementId,
mode: binding.mode, mode: binding.mode,
fixedPoint: normalizeFixedPoint(binding.fixedPoint), fixedPoint: normalizeFixedPoint(binding.fixedPoint || [0.5, 0.5]),
} as FixedPointBinding | null; } as FixedPointBinding | null;
} }
return null; return null;
@@ -185,14 +189,15 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
// binding schema v1 (legacy) -> attempt to migrate to v2 // binding schema v1 (legacy) -> attempt to migrate to v2
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const targetBoundElement = targetElementsMap.get(binding.elementId) as const targetBoundElement =
| ExcalidrawBindableElement (targetElementsMap.get(binding.elementId) as ExcalidrawBindableElement) ||
| undefined; undefined;
const boundElement = const boundElement =
targetBoundElement || targetBoundElement ||
(existingElementsMap?.get(binding.elementId) as (existingElementsMap?.get(
| ExcalidrawBindableElement binding.elementId,
| undefined); ) as ExcalidrawBindableElement) ||
undefined;
const elementsMap = targetBoundElement const elementsMap = targetBoundElement
? targetElementsMap ? targetElementsMap
: existingElementsMap; : existingElementsMap;
@@ -207,28 +212,11 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
const mode = isPointInElement(p, boundElement, elementsMap) const mode = isPointInElement(p, boundElement, elementsMap)
? "inside" ? "inside"
: "orbit"; : "orbit";
const safeElement = {
...element,
startBinding: element.startBinding?.elementId
? {
...element.startBinding,
mode,
fixedPoint: normalizeFixedPoint(element.startBinding.fixedPoint),
}
: null,
endBinding: element.endBinding?.elementId
? {
...element.endBinding,
mode,
fixedPoint: normalizeFixedPoint(element.endBinding.fixedPoint),
}
: null,
};
const focusPoint = const focusPoint =
mode === "inside" mode === "inside"
? p ? p
: projectFixedPointOntoDiagonal( : projectFixedPointOntoDiagonal(
safeElement, element,
p, p,
boundElement, boundElement,
startOrEnd, startOrEnd,
@@ -236,7 +224,7 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
{ value: 1 as NormalizedZoomValue }, { value: 1 as NormalizedZoomValue },
) || p; ) || p;
const { fixedPoint } = calculateFixedPointForNonElbowArrowBinding( const { fixedPoint } = calculateFixedPointForNonElbowArrowBinding(
safeElement, element,
boundElement, boundElement,
startOrEnd, startOrEnd,
elementsMap, elementsMap,
@@ -259,7 +247,7 @@ const repairBinding = <T extends ExcalidrawArrowElement>(
}; };
const restoreElementWithProperties = < const restoreElementWithProperties = <
T extends Required<Omit<ExcalidrawElement, "customData">> & { T extends Omit<ExcalidrawElement, "customData"> & {
customData?: ExcalidrawElement["customData"]; customData?: ExcalidrawElement["customData"];
/** @deprecated */ /** @deprecated */
boundElementIds?: readonly ExcalidrawElement["id"][]; boundElementIds?: readonly ExcalidrawElement["id"][];
@@ -301,6 +289,10 @@ const restoreElementWithProperties = <
width: element.width || 0, width: element.width || 0,
height: element.height || 0, height: element.height || 0,
seed: element.seed ?? 1, seed: element.seed ?? 1,
schemaState:
element.schemaState && typeof element.schemaState === "object"
? element.schemaState
: { tracks: {} },
groupIds: element.groupIds ?? [], groupIds: element.groupIds ?? [],
frameId: element.frameId ?? null, frameId: element.frameId ?? null,
roundness: element.roundness roundness: element.roundness
@@ -521,6 +513,9 @@ export const restoreElement = (
case "embeddable": case "embeddable":
return restoreElementWithProperties(element, {}); return restoreElementWithProperties(element, {});
case "magicframe": case "magicframe":
return restoreElementWithProperties(element, {
name: element.name ?? null,
});
case "frame": case "frame":
return restoreElementWithProperties(element, { return restoreElementWithProperties(element, {
name: element.name ?? null, name: element.name ?? null,
@@ -649,17 +644,25 @@ export const restoreElements = <T extends ExcalidrawElement>(
refreshDimensions?: boolean; refreshDimensions?: boolean;
repairBindings?: boolean; repairBindings?: boolean;
deleteInvisibleElements?: boolean; deleteInvisibleElements?: boolean;
schemaMigrationRegistry?: SchemaMigrationRegistry;
} }
| undefined, | undefined,
): CombineBrandsIfNeeded<T, OrderedExcalidrawElement> => { ): CombineBrandsIfNeeded<T, OrderedExcalidrawElement> => {
const migratedTargetElements = migrateElements(
targetElements as readonly ExcalidrawElement[] | undefined | null,
{
schemaMigrationRegistry: opts?.schemaMigrationRegistry,
},
) as readonly T[] | undefined | null;
// used to detect duplicate top-level element ids // used to detect duplicate top-level element ids
const existingIds = new Set<string>(); const existingIds = new Set<string>();
const targetElementsMap = arrayToMap(targetElements || []); const targetElementsMap = arrayToMap(migratedTargetElements || []);
const existingElementsMap = existingElements const existingElementsMap = existingElements
? arrayToMap(existingElements) ? arrayToMap(existingElements)
: null; : null;
const restoredElements = syncInvalidIndices( const restoredElements = syncInvalidIndices(
(targetElements || []).reduce((elements, element) => { (migratedTargetElements || []).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") { if (element.type === "selection") {
@@ -969,10 +972,14 @@ export const restoreAppState = (
}; };
}; };
const restoreLibraryItem = (libraryItem: LibraryItem) => { const restoreLibraryItem = (
libraryItem: LibraryItem,
opts?: { schemaMigrationRegistry?: SchemaMigrationRegistry },
) => {
const elements = restoreElements( const elements = restoreElements(
getNonDeletedElements(libraryItem.elements), getNonDeletedElements(libraryItem.elements),
null, null,
{ schemaMigrationRegistry: opts?.schemaMigrationRegistry },
); );
return elements.length ? { ...libraryItem, elements } : null; return elements.length ? { ...libraryItem, elements } : null;
}; };
@@ -980,17 +987,21 @@ const restoreLibraryItem = (libraryItem: LibraryItem) => {
export const restoreLibraryItems = ( export const restoreLibraryItems = (
libraryItems: ImportedDataState["libraryItems"] = [], libraryItems: ImportedDataState["libraryItems"] = [],
defaultStatus: LibraryItem["status"], defaultStatus: LibraryItem["status"],
opts?: { schemaMigrationRegistry?: SchemaMigrationRegistry },
) => { ) => {
const restoredItems: LibraryItem[] = []; const restoredItems: LibraryItem[] = [];
for (const item of libraryItems) { for (const item of libraryItems) {
// migrate older libraries // migrate older libraries
if (Array.isArray(item)) { if (Array.isArray(item)) {
const restoredItem = restoreLibraryItem({ const restoredItem = restoreLibraryItem(
status: defaultStatus, {
elements: item, status: defaultStatus,
id: randomId(), elements: item,
created: Date.now(), id: randomId(),
}); created: Date.now(),
},
opts,
);
if (restoredItem) { if (restoredItem) {
restoredItems.push(restoredItem); restoredItems.push(restoredItem);
} }
@@ -999,12 +1010,15 @@ export const restoreLibraryItems = (
LibraryItem, LibraryItem,
"id" | "status" | "created" "id" | "status" | "created"
>; >;
const restoredItem = restoreLibraryItem({ const restoredItem = restoreLibraryItem(
..._item, {
id: _item.id || randomId(), ..._item,
status: _item.status || defaultStatus, id: _item.id || randomId(),
created: _item.created || Date.now(), status: _item.status || defaultStatus,
}); created: _item.created || Date.now(),
},
opts,
);
if (restoredItem) { if (restoredItem) {
restoredItems.push(restoredItem); restoredItems.push(restoredItem);
} }
+408
View File
@@ -0,0 +1,408 @@
import { DEFAULT_ELEMENT_PROPS } from "@excalidraw/common";
import { API } from "../tests/helpers/api";
import {
CORE_FRAME_SCHEMA_TRACK,
createSchemaMigrationRegistry,
type SchemaPlugin,
type SchemaMigration,
CORE_SUPPORTED_TRACKS,
migrateElements,
resolveTrackVersion,
SCHEMA_INITIAL_TRACK_VERSION,
SCHEMA_MIGRATIONS,
validateSchemaMigrations,
validateSchemaPlugins,
} from "./schema";
describe("schema migration", () => {
it("should migrate legacy frame backgrounds to transparent", () => {
const frame = {
...API.createElement({
type: "frame",
backgroundColor: "#ffc9c9",
}),
schemaState: { tracks: {} },
};
const migrated = migrateElements([frame])!;
expect(migrated[0].backgroundColor).toBe(
DEFAULT_ELEMENT_PROPS.backgroundColor,
);
expect(migrated[0].schemaState.tracks[CORE_FRAME_SCHEMA_TRACK]).toBe(
CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK],
);
});
it("should handle elements without schemaState", () => {
const frameWithoutSchemaState = {
...API.createElement({
type: "frame",
backgroundColor: "#ffc9c9",
}),
schemaState: undefined,
} as any;
const textWithoutSchemaState = {
...API.createElement({
type: "text",
text: "",
}),
schemaState: undefined,
} as any;
const migrated = migrateElements([
frameWithoutSchemaState,
textWithoutSchemaState,
])!;
expect(migrated[0].backgroundColor).toBe(
DEFAULT_ELEMENT_PROPS.backgroundColor,
);
expect(migrated[0].schemaState.tracks[CORE_FRAME_SCHEMA_TRACK]).toBe(
CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK],
);
expect(migrated[1].schemaState).toEqual({ tracks: {} });
});
it("should keep latest-track frame backgrounds unchanged", () => {
const frame = {
...API.createElement({
type: "frame",
backgroundColor: "#ffc9c9",
}),
schemaState: {
tracks: {
[CORE_FRAME_SCHEMA_TRACK]:
CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK],
},
},
};
const migrated = migrateElements([frame])!;
expect(migrated[0].backgroundColor).toBe("#ffc9c9");
expect(migrated[0].schemaState.tracks[CORE_FRAME_SCHEMA_TRACK]).toBe(
CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK],
);
});
it("should normalize legacy frame backgrounds", () => {
const frame = {
...API.createElement({
type: "frame",
backgroundColor: "#a5d8ff",
}),
schemaState: {
tracks: {
[CORE_FRAME_SCHEMA_TRACK]: SCHEMA_INITIAL_TRACK_VERSION,
},
},
};
const migrated = migrateElements([frame])!;
expect(migrated[0].backgroundColor).toBe(
DEFAULT_ELEMENT_PROPS.backgroundColor,
);
});
it("should resolve invalid track versions to initial", () => {
expect(resolveTrackVersion(undefined)).toBe(SCHEMA_INITIAL_TRACK_VERSION);
expect(resolveTrackVersion(0)).toBe(SCHEMA_INITIAL_TRACK_VERSION);
expect(resolveTrackVersion(2)).toBe(2);
});
it("should have a valid migration registry configuration", () => {
expect(validateSchemaMigrations(SCHEMA_MIGRATIONS)).toEqual([]);
});
it("should reject invalid migration metadata", () => {
const invalidMigrations: SchemaMigration[] = [
{
id: "",
namespace: "core",
track: CORE_FRAME_SCHEMA_TRACK,
toVersion: 2.1,
title: "",
description: " ",
targetTypes: [],
apply: (element) => element,
},
{
id: "dup",
namespace: "core",
track: CORE_FRAME_SCHEMA_TRACK,
toVersion: 2.1,
title: "duplicate",
description: "duplicate version",
targetTypes: ["frame"],
apply: (element) => element,
},
{
id: "dup",
namespace: "core",
track: CORE_FRAME_SCHEMA_TRACK,
toVersion: 3,
title: "duplicate id",
description: "duplicate id",
targetTypes: ["frame"],
apply: (element) => element,
},
];
const errors = validateSchemaMigrations(invalidMigrations);
expect(errors.length).toBeGreaterThan(0);
expect(errors.join("\n")).toContain("integer version");
expect(errors.join("\n")).toContain("title must be non-empty");
expect(errors.join("\n")).toContain("non-empty description");
expect(errors.join("\n")).toContain("Duplicate schema migration id");
expect(errors.join("\n")).toContain("at least one target type");
});
it("should reject versions at or below initial", () => {
const errors = validateSchemaMigrations([
{
id: "invalid-start",
namespace: "core",
track: CORE_FRAME_SCHEMA_TRACK,
toVersion: SCHEMA_INITIAL_TRACK_VERSION,
title: "invalid start",
description: "bad version",
targetTypes: ["frame"],
apply: (element) => element,
},
]);
expect(errors.join("\n")).toContain("must be greater than 1");
});
it("should reject core track/version mismatch", () => {
const errors = validateSchemaMigrations([
{
id: "frame-v3",
namespace: "core",
track: CORE_FRAME_SCHEMA_TRACK,
toVersion: CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK] + 1,
title: "future migration",
description: "future migration for test",
targetTypes: ["frame"],
apply: (element) => element,
},
]);
expect(errors.join("\n")).toContain(
`Core supported track "${CORE_FRAME_SCHEMA_TRACK}" (${CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK]}) must match last migration version`,
);
});
it("should reject undeclared core tracks", () => {
const errors = validateSchemaMigrations([
{
id: "unknown-core-track",
namespace: "core",
track: "excalidraw.shape.unknown",
toVersion: 2,
title: "unknown core track",
description: "should require supported-track declaration",
targetTypes: ["rectangle"],
apply: (element) => element,
},
]);
expect(errors.join("\n")).toContain(
"must be declared in CORE_SUPPORTED_TRACKS",
);
});
it("should reject invalid plugin metadata", () => {
const errors = validateSchemaPlugins([
{
id: "",
migrations: [],
},
{
id: "dup",
migrations: [],
},
{
id: "dup",
migrations: [],
},
{
id: "core-overwrite",
migrations: [
{
id: "bad.core.migration",
namespace: "core",
track: CORE_FRAME_SCHEMA_TRACK,
toVersion: 2,
title: "bad",
description: "bad",
targetTypes: ["frame"],
apply: (element) => element,
},
],
},
]);
expect(errors.join("\n")).toContain("Schema plugin id must be non-empty");
expect(errors.join("\n")).toContain("Duplicate schema plugin id found");
expect(errors.join("\n")).toContain("cannot declare core migrations");
});
it("should not depend on temporary fields during migration", () => {
const frame = {
...API.createElement({
type: "frame",
backgroundColor: "#a5d8ff",
}),
schemaState: { tracks: {} },
};
const withTempField = {
...frame,
backgroundEnabled: false,
} as typeof frame & { backgroundEnabled: boolean };
const migratedBase = migrateElements([frame])!;
const migratedWithTempField = migrateElements([withTempField])!;
expect(migratedBase[0].backgroundColor).toBe(
DEFAULT_ELEMENT_PROPS.backgroundColor,
);
expect(migratedWithTempField[0].backgroundColor).toBe(
DEFAULT_ELEMENT_PROPS.backgroundColor,
);
});
it("should use per-element track hints", () => {
const frame = API.createElement({
type: "frame",
backgroundColor: "#ff0000",
});
const frameFromModernSource = {
...frame,
schemaState: {
tracks: {
[CORE_FRAME_SCHEMA_TRACK]:
CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK],
},
},
};
const migrated = migrateElements([frameFromModernSource])!;
expect(migrated[0].backgroundColor).toBe("#ff0000");
});
it("should migrate mixed-hint elements individually", () => {
const legacyFrame = {
...API.createElement({
type: "frame",
backgroundColor: "#ff0000",
}),
schemaState: { tracks: {} },
};
const modernFrame = API.createElement({
type: "frame",
backgroundColor: "#00ff00",
});
const modernFrameWithTrack = {
...modernFrame,
schemaState: {
tracks: {
[CORE_FRAME_SCHEMA_TRACK]:
CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK],
},
},
};
const migrated = migrateElements([legacyFrame, modernFrameWithTrack])!;
expect(migrated[0].backgroundColor).toBe(
DEFAULT_ELEMENT_PROPS.backgroundColor,
);
expect(migrated[1].backgroundColor).toBe("#00ff00");
});
it("should preserve higher-than-supported track versions", () => {
const frame = API.createElement({
type: "frame",
backgroundColor: "#ff0000",
});
const futureFrame = {
...frame,
schemaState: {
tracks: {
[CORE_FRAME_SCHEMA_TRACK]:
CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK] + 1,
},
},
};
const migrated = migrateElements([futureFrame])!;
expect(migrated[0].schemaState.tracks[CORE_FRAME_SCHEMA_TRACK]).toBe(
CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK] + 1,
);
expect(migrated[0].backgroundColor).toBe("#ff0000");
});
it("should normalize invalid schema state and preserve unknown tracks", () => {
const rect = {
...API.createElement({ type: "rectangle" }),
schemaState: {
tracks: {
"host.myapp.card": 4,
[CORE_FRAME_SCHEMA_TRACK]: 0,
},
},
};
const migrated = migrateElements([rect])!;
expect(migrated[0].schemaState.tracks[CORE_FRAME_SCHEMA_TRACK]).toBe(
SCHEMA_INITIAL_TRACK_VERSION,
);
expect(migrated[0].schemaState.tracks["host.myapp.card"]).toBe(4);
});
it("should not run plugin migrations unless plugins are provided", () => {
const rect = API.createElement({
type: "rectangle",
backgroundColor: "#ffd8a8",
});
const plugin: SchemaPlugin = {
id: "myapp",
migrations: [
{
id: "host.myapp.rect.normalize.v2",
namespace: "host.myapp",
track: "host.myapp.rectangle",
toVersion: 2,
title: "normalize rect background",
description: "plugin migration for testing",
targetTypes: ["rectangle"],
apply: (element) =>
element.type === "rectangle"
? { ...element, backgroundColor: "#12b886" }
: element,
},
],
};
const migratedWithoutPlugin = migrateElements([rect])!;
const migratedWithPlugin = migrateElements([rect], {
schemaMigrationRegistry: createSchemaMigrationRegistry([plugin]),
})!;
expect(migratedWithoutPlugin[0].backgroundColor).toBe("#ffd8a8");
expect(
migratedWithoutPlugin[0].schemaState.tracks["host.myapp.rectangle"],
).toBe(undefined);
expect(migratedWithPlugin[0].backgroundColor).toBe("#12b886");
expect(
migratedWithPlugin[0].schemaState.tracks["host.myapp.rectangle"],
).toBe(2);
});
});
+443
View File
@@ -0,0 +1,443 @@
import {
CORE_FRAME_SCHEMA_TRACK,
SCHEMA_CORE_NAMESPACE,
SCHEMA_INITIAL_TRACK_VERSION,
CORE_SUPPORTED_TRACKS,
} from "@excalidraw/element/schema";
import type { SchemaNamespace, SchemaTrack } from "@excalidraw/element/schema";
import type { ExcalidrawElement } from "@excalidraw/element/types";
export {
CORE_FRAME_SCHEMA_TRACK,
CORE_SUPPORTED_TRACKS,
SCHEMA_CORE_NAMESPACE,
SCHEMA_INITIAL_TRACK_VERSION,
};
export type { SchemaNamespace, SchemaTrack };
/**
* Schema migration flow:
* 0) Compile schema config from core migrations + optional host plugins.
* - validate plugin metadata
* - validate migration ordering/metadata
* - derive per-track supported versions for this registry
* 1) Normalize element.schemaState.tracks (invalid/missing -> initial track version).
* 2) Iterate compiled migrations in declaration order.
* 3) For matching element types, apply only forward migrations that are
* supported by the current registry config (never re-run, never downgrade).
* 4) Stamp migrated track versions back onto each element.
*/
/** One migration step for a single track version bump. */
export type SchemaMigration = {
/** Stable unique id for validation and debugging. */
id: string;
/** Owner of the migration: core or a host namespace. */
namespace: SchemaNamespace;
/** Version line this migration belongs to. */
track: SchemaTrack;
/** Target version reached after applying this migration. */
toVersion: number;
/** Human-readable metadata for maintainers/reviewers. */
title: string;
description: string;
/** Which element types this migration may transform ("*" = all). */
targetTypes: readonly ExcalidrawElement["type"][] | "*";
/** Pure transform for a single element. */
apply: (element: ExcalidrawElement) => ExcalidrawElement;
};
/**
* Optional host-provided migration bundle.
* Plugins are additive and may only declare host namespace migrations.
*/
export type SchemaPlugin = {
/** Stable plugin id for diagnostics. */
id: string;
/** Host migration steps merged with core migrations into one registry. */
migrations: readonly SchemaMigration[];
};
/** Default plugin registry (intentionally empty in core). */
export const SCHEMA_PLUGINS: readonly SchemaPlugin[] = [];
export type SchemaMigrationRegistry = Readonly<{
/** Fully validated core + host migrations used for this run. */
migrations: readonly SchemaMigration[];
/** Latest supported version for each known track in this run. */
supportedTrackVersions: Readonly<Record<string, number>>;
}>;
export const SCHEMA_MIGRATIONS: readonly SchemaMigration[] = [
{
id: "core.frame.background.normalize.v2",
namespace: SCHEMA_CORE_NAMESPACE,
track: CORE_FRAME_SCHEMA_TRACK,
toVersion: CORE_SUPPORTED_TRACKS[CORE_FRAME_SCHEMA_TRACK],
title: "Normalize legacy frame backgrounds",
description:
"Frames saved before frame track v2 must render without visible fill, so normalize backgroundColor to transparent on restore.",
targetTypes: ["frame"],
apply: (element) => {
if (element.type !== "frame") {
return element;
}
return {
...element,
backgroundColor: "transparent",
};
},
},
];
export const resolveTrackVersion = (trackVersion: unknown) => {
if (
Number.isInteger(trackVersion) &&
(trackVersion as number) >= SCHEMA_INITIAL_TRACK_VERSION
) {
return trackVersion as number;
}
return SCHEMA_INITIAL_TRACK_VERSION;
};
const normalizeSchemaTracks = (tracks: unknown) => {
if (!tracks || typeof tracks !== "object") {
return {} as Record<string, number>;
}
return Object.entries(tracks as Record<string, unknown>).reduce<
Record<string, number>
>((acc, [track, version]) => {
const normalizedVersion = resolveTrackVersion(version);
if (normalizedVersion >= SCHEMA_INITIAL_TRACK_VERSION) {
acc[track] = normalizedVersion;
}
return acc;
}, {});
};
const normalizeElementSchemaState = (
element: ExcalidrawElement,
): ExcalidrawElement["schemaState"] => {
const tracks = normalizeSchemaTracks(
(
element as ExcalidrawElement & {
schemaState?: ExcalidrawElement["schemaState"];
}
).schemaState?.tracks,
);
return {
tracks,
};
};
const ensureElementSchemaState = (element: ExcalidrawElement) => {
const normalizedSchemaState = normalizeElementSchemaState(element);
// Fast path: avoid reallocating when element already has normalized state.
if (element.schemaState === normalizedSchemaState) {
return element;
}
if (
element.schemaState &&
Object.keys(element.schemaState?.tracks || {}).length ===
Object.keys(normalizedSchemaState.tracks).length &&
Object.entries(normalizedSchemaState.tracks).every(
([track, version]) => element.schemaState?.tracks?.[track] === version,
)
) {
return element;
}
return {
...element,
schemaState: normalizedSchemaState,
};
};
const getTrackVersion = (element: ExcalidrawElement, track: SchemaTrack) => {
return resolveTrackVersion(element.schemaState.tracks[track]);
};
const withTrackVersion = (
element: ExcalidrawElement,
track: SchemaTrack,
version: number,
) => {
if (element.schemaState.tracks[track] === version) {
return element;
}
return {
...element,
schemaState: {
...element.schemaState,
tracks: {
...element.schemaState.tracks,
[track]: version,
},
},
};
};
const migrationMatchesElementType = (
migration: SchemaMigration,
element: ExcalidrawElement,
) => {
return (
migration.targetTypes === "*" ||
migration.targetTypes.includes(element.type)
);
};
export const validateSchemaMigrations = (
migrations: readonly SchemaMigration[],
) => {
const errors: string[] = [];
const seenIds = new Set<string>();
const previousVersionByTrack = new Map<string, number>();
for (const migration of migrations) {
if (!migration.id.trim()) {
errors.push("Migration id must be non-empty.");
}
if (seenIds.has(migration.id)) {
errors.push(`Duplicate schema migration id found: ${migration.id}.`);
}
seenIds.add(migration.id);
if (!migration.title.trim()) {
errors.push(`Migration "${migration.id}" title must be non-empty.`);
}
if (!migration.description.trim()) {
errors.push(
`Migration "${migration.id}" must include a non-empty description.`,
);
}
if (!Number.isInteger(migration.toVersion)) {
errors.push(`Migration "${migration.id}" must use an integer version.`);
}
if (migration.toVersion <= SCHEMA_INITIAL_TRACK_VERSION) {
errors.push(
`Migration "${migration.id}" version must be greater than ${SCHEMA_INITIAL_TRACK_VERSION}.`,
);
}
if (
migration.targetTypes !== "*" &&
(!migration.targetTypes.length ||
migration.targetTypes.some((type) => !type))
) {
errors.push(
`Migration "${migration.id}" must declare at least one target type.`,
);
}
const trackKey = `${migration.namespace}|${migration.track}`;
const previousVersion =
previousVersionByTrack.get(trackKey) ?? SCHEMA_INITIAL_TRACK_VERSION;
if (migration.toVersion <= previousVersion) {
errors.push(
`Migration "${migration.id}" must be ordered by increasing version within ${trackKey}.`,
);
}
previousVersionByTrack.set(trackKey, migration.toVersion);
if (
migration.namespace === SCHEMA_CORE_NAMESPACE &&
!migration.track.startsWith("excalidraw.")
) {
errors.push(
`Core migration "${migration.id}" must use an excalidraw.* track.`,
);
}
if (
migration.namespace === SCHEMA_CORE_NAMESPACE &&
!(migration.track in CORE_SUPPORTED_TRACKS)
) {
errors.push(
`Core migration "${migration.id}" track "${migration.track}" must be declared in CORE_SUPPORTED_TRACKS.`,
);
}
if (
migration.namespace !== SCHEMA_CORE_NAMESPACE &&
!migration.track.startsWith(`${migration.namespace}.`)
) {
errors.push(
`Host migration "${migration.id}" track must use namespace prefix ${migration.namespace}.`,
);
}
}
for (const [track, supportedVersion] of Object.entries(
CORE_SUPPORTED_TRACKS,
)) {
const migrationTrackKey = `${SCHEMA_CORE_NAMESPACE}|${track}`;
const lastDeclaredVersion =
previousVersionByTrack.get(migrationTrackKey) ??
SCHEMA_INITIAL_TRACK_VERSION;
if (lastDeclaredVersion !== supportedVersion) {
errors.push(
`Core supported track "${track}" (${supportedVersion}) must match last migration version (${lastDeclaredVersion}).`,
);
}
}
return errors;
};
export const validateSchemaPlugins = (plugins: readonly SchemaPlugin[]) => {
const errors: string[] = [];
const seenIds = new Set<string>();
for (const plugin of plugins) {
if (!plugin.id.trim()) {
errors.push("Schema plugin id must be non-empty.");
}
if (seenIds.has(plugin.id)) {
errors.push(`Duplicate schema plugin id found: ${plugin.id}.`);
}
seenIds.add(plugin.id);
for (const migration of plugin.migrations) {
if (migration.namespace === SCHEMA_CORE_NAMESPACE) {
errors.push(
`Schema plugin "${plugin.id}" cannot declare core migrations ("${migration.id}").`,
);
}
}
}
return errors;
};
const collectPluginMigrations = (plugins: readonly SchemaPlugin[]) =>
plugins.flatMap((plugin) => plugin.migrations);
/**
* Builds the registry "latest version" map:
* - core tracks come from CORE_SUPPORTED_TRACKS
* - host tracks are inferred from provided plugin migrations
*/
const getSupportedTrackVersions = (
migrations: readonly SchemaMigration[],
): Readonly<Record<string, number>> => {
const supportedTrackVersions: Record<string, number> = {
...CORE_SUPPORTED_TRACKS,
};
for (const migration of migrations) {
if (migration.namespace === SCHEMA_CORE_NAMESPACE) {
continue;
}
const currentSupportedVersion =
supportedTrackVersions[migration.track] ?? SCHEMA_INITIAL_TRACK_VERSION;
if (migration.toVersion > currentSupportedVersion) {
supportedTrackVersions[migration.track] = migration.toVersion;
}
}
return supportedTrackVersions;
};
export const createSchemaMigrationRegistry = (
plugins: readonly SchemaPlugin[] = SCHEMA_PLUGINS,
): SchemaMigrationRegistry => {
const pluginErrors = validateSchemaPlugins(plugins);
if (pluginErrors.length) {
throw new Error(
`Invalid schema plugin configuration:\n${pluginErrors.join("\n")}`,
);
}
const migrations = [
...SCHEMA_MIGRATIONS,
...collectPluginMigrations(plugins),
] as const;
const migrationErrors = validateSchemaMigrations(migrations);
if (migrationErrors.length) {
throw new Error(
`Invalid schema migration configuration:\n${migrationErrors.join("\n")}`,
);
}
return {
migrations,
supportedTrackVersions: getSupportedTrackVersions(migrations),
};
};
const CORE_SCHEMA_MIGRATION_REGISTRY = createSchemaMigrationRegistry();
/** Uses cached core config by default, recompiles when plugins are provided. */
const resolveSchemaMigrationRegistry = (
schemaMigrationRegistry: SchemaMigrationRegistry | undefined,
) => schemaMigrationRegistry || CORE_SCHEMA_MIGRATION_REGISTRY;
const migrateElement = (
element: ExcalidrawElement,
schemaMigrationRegistry: SchemaMigrationRegistry,
) => {
// Always migrate from a normalized per-element schema state.
let migratedElement = ensureElementSchemaState(element);
for (const migration of schemaMigrationRegistry.migrations) {
if (!migrationMatchesElementType(migration, migratedElement)) {
continue;
}
const currentTrackVersion = getTrackVersion(
migratedElement,
migration.track,
);
const supportedTrackVersion =
schemaMigrationRegistry.supportedTrackVersions[migration.track] ??
currentTrackVersion;
// Never re-run or downgrade.
if (currentTrackVersion >= migration.toVersion) {
continue;
}
// Preserve future data: ignore migrations newer than what this app supports.
if (migration.toVersion > supportedTrackVersion) {
continue;
}
// Apply transform, then stamp the element's track version.
migratedElement = withTrackVersion(
migration.apply(migratedElement),
migration.track,
migration.toVersion,
);
}
return migratedElement;
};
export const migrateElements = (
elements: readonly ExcalidrawElement[] | null | undefined,
opts?: {
schemaMigrationRegistry?: SchemaMigrationRegistry;
},
) => {
if (!elements) {
return elements;
}
const schemaMigrationRegistry = resolveSchemaMigrationRegistry(
opts?.schemaMigrationRegistry,
);
return elements.map((element) =>
migrateElement(element, schemaMigrationRegistry),
);
};
+1 -1
View File
@@ -30,7 +30,7 @@ export class HistoryDelta extends StoreDelta {
// as we always need to end up with a new version due to collaboration, // as we always need to end up with a new version due to collaboration,
// approaching each undo / redo as a new user action // approaching each undo / redo as a new user action
{ {
excludedProperties: new Set(["version", "versionNonce"]), excludedProperties: new Set(["version", "versionNonce", "schemaState"]),
}, },
); );
+2 -6
View File
@@ -56,6 +56,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
aiEnabled, aiEnabled,
showDeprecatedFonts, showDeprecatedFonts,
renderScrollbars, renderScrollbars,
schemaPlugins,
} = props; } = props;
const canvasActions = props.UIOptions?.canvasActions; const canvasActions = props.UIOptions?.canvasActions;
@@ -149,6 +150,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
aiEnabled={aiEnabled !== false} aiEnabled={aiEnabled !== false}
showDeprecatedFonts={showDeprecatedFonts} showDeprecatedFonts={showDeprecatedFonts}
renderScrollbars={renderScrollbars} renderScrollbars={renderScrollbars}
schemaPlugins={schemaPlugins}
> >
{children} {children}
</App> </App>
@@ -319,9 +321,3 @@ export { isElementLink } from "@excalidraw/element";
export { setCustomTextMetricsProvider } from "@excalidraw/element"; export { setCustomTextMetricsProvider } from "@excalidraw/element";
export { CommandPalette } from "./components/CommandPalette/CommandPalette"; export { CommandPalette } from "./components/CommandPalette/CommandPalette";
export {
renderSpreadsheet,
tryParseSpreadsheet,
isSpreadsheetValidForChartType,
} from "./charts";
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "لصق", "paste": "لصق",
"pasteAsPlaintext": "اللصق كنص عادي", "pasteAsPlaintext": "اللصق كنص عادي",
"pasteCharts": "لصق الرسوم البيانية", "pasteCharts": "لصق الرسوم البيانية",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "تحديد الكل", "selectAll": "تحديد الكل",
"multiSelect": "إضافة عنصر للتحديد", "multiSelect": "إضافة عنصر للتحديد",
"moveCanvas": "نقل لوح الرسم", "moveCanvas": "نقل لوح الرسم",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Crow's foot (many)", "arrowhead_crowfoot_many": "Crow's foot (many)",
"arrowhead_crowfoot_one": "Crow's foot (one)", "arrowhead_crowfoot_one": "Crow's foot (one)",
"arrowhead_crowfoot_one_or_many": "Crow's foot (one or many)", "arrowhead_crowfoot_one_or_many": "Crow's foot (one or many)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "خيارات إضافية", "more_options": "خيارات إضافية",
"cardinality": "",
"arrowtypes": "نوع السهم", "arrowtypes": "نوع السهم",
"arrowtype_sharp": "سهم حاد", "arrowtype_sharp": "سهم حاد",
"arrowtype_round": "سهم منحني", "arrowtype_round": "سهم منحني",
@@ -182,11 +171,7 @@
"linkToElement": "رابط إلى الكائن", "linkToElement": "رابط إلى الكائن",
"wrapSelectionInFrame": "تغليف التحديد في إطار", "wrapSelectionInFrame": "تغليف التحديد في إطار",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "رابط إلى الكائن", "title": "رابط إلى الكائن",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "خطأ" "title": "خطأ"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "حفظ الملف على الجهاز", "disk_title": "حفظ الملف على الجهاز",
"disk_details": "تصدير بيانات المشهد إلى ملف يمكنك الاستيراد منه لاحقًا.", "disk_details": "تصدير بيانات المشهد إلى ملف يمكنك الاستيراد منه لاحقًا.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Mermaid إلى Excalidraw", "title": "Mermaid إلى Excalidraw",
"button": "إدراج", "button": "إدراج",
"description": "", "description": "حاليًا، يتم دعم <flowchartLink>مخططات التدفق</flowchartLink>، <sequenceLink>التسلسلات</sequenceLink>، و<classLink>الفئات</classLink> فقط. سيتم عرض الأنواع الأخرى كصورة في Excalidraw.",
"syntax": "صيغة Mermaid", "syntax": "صيغة Mermaid",
"preview": "معاينة", "preview": "معاينة",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Yapışdır", "paste": "Yapışdır",
"pasteAsPlaintext": "Düz mətn kimi yapışdırın", "pasteAsPlaintext": "Düz mətn kimi yapışdırın",
"pasteCharts": "Diaqramları yapışdırın", "pasteCharts": "Diaqramları yapışdırın",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Hamısını seç", "selectAll": "Hamısını seç",
"multiSelect": "Seçimə element əlavə edin", "multiSelect": "Seçimə element əlavə edin",
"moveCanvas": "Kanvası köçürün", "moveCanvas": "Kanvası köçürün",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "" "title": ""
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "", "disk_title": "",
"disk_details": "", "disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Постави", "paste": "Постави",
"pasteAsPlaintext": "Постави като обикновен текст", "pasteAsPlaintext": "Постави като обикновен текст",
"pasteCharts": "Постави графики", "pasteCharts": "Постави графики",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Маркирай всичко", "selectAll": "Маркирай всичко",
"multiSelect": "Добави елемент към селекция", "multiSelect": "Добави елемент към селекция",
"moveCanvas": "Премести платно", "moveCanvas": "Премести платно",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "Вид стрелка", "arrowtypes": "Вид стрелка",
"arrowtype_sharp": "Остра стрелка", "arrowtype_sharp": "Остра стрелка",
"arrowtype_round": "Извита стрелка", "arrowtype_round": "Извита стрелка",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Грешка" "title": "Грешка"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Запази към диск", "disk_title": "Запази към диск",
"disk_details": "", "disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "Mermaid Синтаксис", "syntax": "Mermaid Синтаксис",
"preview": "Преглед", "preview": "Преглед",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "পেস্ট করুন", "paste": "পেস্ট করুন",
"pasteAsPlaintext": "প্লেইনটেক্সট হিসাবে পেস্ট করুন", "pasteAsPlaintext": "প্লেইনটেক্সট হিসাবে পেস্ট করুন",
"pasteCharts": "চার্ট পেস্ট করুন", "pasteCharts": "চার্ট পেস্ট করুন",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "সবটা সিলেক্ট করুন", "selectAll": "সবটা সিলেক্ট করুন",
"multiSelect": "একাধিক সিলেক্ট করুন", "multiSelect": "একাধিক সিলেক্ট করুন",
"moveCanvas": "ক্যানভাস সরান", "moveCanvas": "ক্যানভাস সরান",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "ত্রুটি" "title": "ত্রুটি"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "", "disk_title": "",
"disk_details": "", "disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "পেস্ট করুন", "paste": "পেস্ট করুন",
"pasteAsPlaintext": "প্লেইনটেক্সট হিসাবে পেস্ট করুন", "pasteAsPlaintext": "প্লেইনটেক্সট হিসাবে পেস্ট করুন",
"pasteCharts": "চার্ট পেস্ট করুন", "pasteCharts": "চার্ট পেস্ট করুন",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "সবটা সিলেক্ট করুন", "selectAll": "সবটা সিলেক্ট করুন",
"multiSelect": "একাধিক সিলেক্ট করুন", "multiSelect": "একাধিক সিলেক্ট করুন",
"moveCanvas": "ক্যানভাস সরান", "moveCanvas": "ক্যানভাস সরান",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "ত্রুটি" "title": "ত্রুটি"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "", "disk_title": "",
"disk_details": "", "disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Enganxa", "paste": "Enganxa",
"pasteAsPlaintext": "Enganxar com a text pla", "pasteAsPlaintext": "Enganxar com a text pla",
"pasteCharts": "Enganxa els diagrames", "pasteCharts": "Enganxa els diagrames",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Selecciona-ho tot", "selectAll": "Selecciona-ho tot",
"multiSelect": "Afegeix un element a la selecció", "multiSelect": "Afegeix un element a la selecció",
"moveCanvas": "Mou el llenç", "moveCanvas": "Mou el llenç",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Potes de gall (moltes)", "arrowhead_crowfoot_many": "Potes de gall (moltes)",
"arrowhead_crowfoot_one": "Potes de gall (una)", "arrowhead_crowfoot_one": "Potes de gall (una)",
"arrowhead_crowfoot_one_or_many": "Potes de gall (una o moltes)", "arrowhead_crowfoot_one_or_many": "Potes de gall (una o moltes)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Més opcions", "more_options": "Més opcions",
"cardinality": "",
"arrowtypes": "Tipus de fletxa", "arrowtypes": "Tipus de fletxa",
"arrowtype_sharp": "Fletxa Esmolada", "arrowtype_sharp": "Fletxa Esmolada",
"arrowtype_round": "Fletxa corba", "arrowtype_round": "Fletxa corba",
@@ -182,11 +171,7 @@
"linkToElement": "Enllaç a l'objecte", "linkToElement": "Enllaç a l'objecte",
"wrapSelectionInFrame": "Embolica la selecció en un marc", "wrapSelectionInFrame": "Embolica la selecció en un marc",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "Enllaç a l'objecte", "title": "Enllaç a l'objecte",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Error" "title": "Error"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Desa al disc", "disk_title": "Desa al disc",
"disk_details": "Exporta les dades de l'escena a un fitxer que després podreu importar.", "disk_details": "Exporta les dades de l'escena a un fitxer que després podreu importar.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Mermaid a Excalidraw", "title": "Mermaid a Excalidraw",
"button": "Inseriu", "button": "Inseriu",
"description": "", "description": "Actualment només s'admeten els diagrames <flowchartLink>Flowchart</flowchartLink>, <sequenceLink> Sequence, </sequenceLink> i <classLink> Class </classLink>. Els altres tipus es representaran com a imatge a Excalidraw.",
"syntax": "Sintaxi de Mermaid", "syntax": "Sintaxi de Mermaid",
"preview": "Previsualització", "preview": "Previsualització",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Vložit", "paste": "Vložit",
"pasteAsPlaintext": "Vložit jako prostý text", "pasteAsPlaintext": "Vložit jako prostý text",
"pasteCharts": "Vložit grafy", "pasteCharts": "Vložit grafy",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Vybrat vše", "selectAll": "Vybrat vše",
"multiSelect": "Přidat prvek do výběru", "multiSelect": "Přidat prvek do výběru",
"moveCanvas": "Posunout plátno", "moveCanvas": "Posunout plátno",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "Typ šipky", "arrowtypes": "Typ šipky",
"arrowtype_sharp": "Ostrá šipka", "arrowtype_sharp": "Ostrá šipka",
"arrowtype_round": "Zakřivená šipka", "arrowtype_round": "Zakřivená šipka",
@@ -182,11 +171,7 @@
"linkToElement": "Odkaz na objekt", "linkToElement": "Odkaz na objekt",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "Odkaz na objekt", "title": "Odkaz na objekt",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Chyba" "title": "Chyba"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Uložit na disk", "disk_title": "Uložit na disk",
"disk_details": "Exportovat data scény do souboru, ze kterého můžete importovat později.", "disk_details": "Exportovat data scény do souboru, ze kterého můžete importovat později.",
@@ -635,8 +616,7 @@
"syntax": "Mermaid syntaxe", "syntax": "Mermaid syntaxe",
"preview": "Náhled", "preview": "Náhled",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Indsæt", "paste": "Indsæt",
"pasteAsPlaintext": "Indsæt som klartekst", "pasteAsPlaintext": "Indsæt som klartekst",
"pasteCharts": "Indsæt diagrammer", "pasteCharts": "Indsæt diagrammer",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Marker alle", "selectAll": "Marker alle",
"multiSelect": "Tilføj element til markering", "multiSelect": "Tilføj element til markering",
"moveCanvas": "Flyt lærred", "moveCanvas": "Flyt lærred",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Kragefod (mange)", "arrowhead_crowfoot_many": "Kragefod (mange)",
"arrowhead_crowfoot_one": "Kragefod (én)", "arrowhead_crowfoot_one": "Kragefod (én)",
"arrowhead_crowfoot_one_or_many": "Kragefod (én eller mange)", "arrowhead_crowfoot_one_or_many": "Kragefod (én eller mange)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Flere muligheder", "more_options": "Flere muligheder",
"cardinality": "",
"arrowtypes": "Pile type", "arrowtypes": "Pile type",
"arrowtype_sharp": "Skarp pil", "arrowtype_sharp": "Skarp pil",
"arrowtype_round": "Buet pil", "arrowtype_round": "Buet pil",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Fejl" "title": "Fejl"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Gem til disk", "disk_title": "Gem til disk",
"disk_details": "", "disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Einfügen", "paste": "Einfügen",
"pasteAsPlaintext": "Als unformatierten Text einfügen", "pasteAsPlaintext": "Als unformatierten Text einfügen",
"pasteCharts": "Diagramme einfügen", "pasteCharts": "Diagramme einfügen",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Alle auswählen", "selectAll": "Alle auswählen",
"multiSelect": "Element zur Auswahl hinzufügen", "multiSelect": "Element zur Auswahl hinzufügen",
"moveCanvas": "Leinwand verschieben", "moveCanvas": "Leinwand verschieben",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Krähenfuß (viele)", "arrowhead_crowfoot_many": "Krähenfuß (viele)",
"arrowhead_crowfoot_one": "Krähenfuß (einer)", "arrowhead_crowfoot_one": "Krähenfuß (einer)",
"arrowhead_crowfoot_one_or_many": "Krähenfuß (einer oder viele)", "arrowhead_crowfoot_one_or_many": "Krähenfuß (einer oder viele)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Weitere Optionen", "more_options": "Weitere Optionen",
"cardinality": "",
"arrowtypes": "Pfeiltyp", "arrowtypes": "Pfeiltyp",
"arrowtype_sharp": "Scharfer Pfeil", "arrowtype_sharp": "Scharfer Pfeil",
"arrowtype_round": "Gebogener Pfeil", "arrowtype_round": "Gebogener Pfeil",
@@ -182,11 +171,7 @@
"linkToElement": "Link zum Objekt", "linkToElement": "Link zum Objekt",
"wrapSelectionInFrame": "Auswahl in Rahmen einbetten", "wrapSelectionInFrame": "Auswahl in Rahmen einbetten",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "Link zum Objekt", "title": "Link zum Objekt",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Fehler" "title": "Fehler"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Auf Festplatte speichern", "disk_title": "Auf Festplatte speichern",
"disk_details": "Exportiere die Zeichnungsdaten in eine Datei, die Du später importieren kannst.", "disk_details": "Exportiere die Zeichnungsdaten in eine Datei, die Du später importieren kannst.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Mermaid zu Excalidraw", "title": "Mermaid zu Excalidraw",
"button": "Einfügen", "button": "Einfügen",
"description": "", "description": "Derzeit werden nur <flowchartLink>Flussdiagramme</flowchartLink>, <sequenceLink>Sequenzdiagramme</sequenceLink> und <classLink>Klassendiagramme</classLink> unterstützt. Die anderen Typen werden als Bild in Excalidraw dargestellt.",
"syntax": "Mermaid-Syntax", "syntax": "Mermaid-Syntax",
"preview": "Vorschau", "preview": "Vorschau",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Einfügen", "paste": "Einfügen",
"pasteAsPlaintext": "Als unformatierten Text einfügen", "pasteAsPlaintext": "Als unformatierten Text einfügen",
"pasteCharts": "Diagramme einfügen", "pasteCharts": "Diagramme einfügen",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Alle auswählen", "selectAll": "Alle auswählen",
"multiSelect": "Element zur Auswahl hinzufügen", "multiSelect": "Element zur Auswahl hinzufügen",
"moveCanvas": "Leinwand verschieben", "moveCanvas": "Leinwand verschieben",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Krähenfuß (viele)", "arrowhead_crowfoot_many": "Krähenfuß (viele)",
"arrowhead_crowfoot_one": "Krähenfuß (einer)", "arrowhead_crowfoot_one": "Krähenfuß (einer)",
"arrowhead_crowfoot_one_or_many": "Krähenfuß (einer oder viele)", "arrowhead_crowfoot_one_or_many": "Krähenfuß (einer oder viele)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Weitere Optionen", "more_options": "Weitere Optionen",
"cardinality": "",
"arrowtypes": "Pfeiltyp", "arrowtypes": "Pfeiltyp",
"arrowtype_sharp": "Scharfer Pfeil", "arrowtype_sharp": "Scharfer Pfeil",
"arrowtype_round": "Gebogener Pfeil", "arrowtype_round": "Gebogener Pfeil",
@@ -182,11 +171,7 @@
"linkToElement": "Link zum Objekt", "linkToElement": "Link zum Objekt",
"wrapSelectionInFrame": "Auswahl in Rahmen einbetten", "wrapSelectionInFrame": "Auswahl in Rahmen einbetten",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "Link zum Objekt", "title": "Link zum Objekt",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Fehler" "title": "Fehler"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Auf Festplatte speichern", "disk_title": "Auf Festplatte speichern",
"disk_details": "Exportiere die Zeichnungsdaten in eine Datei, die Du später importieren kannst.", "disk_details": "Exportiere die Zeichnungsdaten in eine Datei, die Du später importieren kannst.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Mermaid zu Excalidraw", "title": "Mermaid zu Excalidraw",
"button": "Einfügen", "button": "Einfügen",
"description": "", "description": "Derzeit werden nur <flowchartLink>Flussdiagramme</flowchartLink>, <sequenceLink>Sequenzdiagramme</sequenceLink> und <classLink>Klassendiagramme</classLink> unterstützt. Die anderen Typen werden als Bild in Excalidraw dargestellt.",
"syntax": "Mermaid-Syntax", "syntax": "Mermaid-Syntax",
"preview": "Vorschau", "preview": "Vorschau",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Επικόλληση", "paste": "Επικόλληση",
"pasteAsPlaintext": "Επικόλληση ως απλό κείμενο", "pasteAsPlaintext": "Επικόλληση ως απλό κείμενο",
"pasteCharts": "Επικόλληση γραφημάτων", "pasteCharts": "Επικόλληση γραφημάτων",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Επιλογή όλων", "selectAll": "Επιλογή όλων",
"multiSelect": "Προσθέστε το στοιχείο στην επιλογή", "multiSelect": "Προσθέστε το στοιχείο στην επιλογή",
"moveCanvas": "Μετακίνηση καμβά", "moveCanvas": "Μετακίνηση καμβά",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "Τύπος βέλους", "arrowtypes": "Τύπος βέλους",
"arrowtype_sharp": "Αιχμηρό βέλος", "arrowtype_sharp": "Αιχμηρό βέλος",
"arrowtype_round": "Κυρτό βέλος", "arrowtype_round": "Κυρτό βέλος",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Σφάλμα" "title": "Σφάλμα"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Αποθήκευση στο δίσκο", "disk_title": "Αποθήκευση στο δίσκο",
"disk_details": "Εξαγωγή δεδομένων σκηνής σε ένα αρχείο από το οποίο μπορείτε να εισάγετε αργότερα.", "disk_details": "Εξαγωγή δεδομένων σκηνής σε ένα αρχείο από το οποίο μπορείτε να εισάγετε αργότερα.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Mermaid σε Excalidraw", "title": "Mermaid σε Excalidraw",
"button": "Εισαγωγή", "button": "Εισαγωγή",
"description": "", "description": "Επί του παρόντος υποστηρίζονται μόνο Διαγράμματα <flowchartLink>Ροής</flowchartLink>,<sequenceLink> Ακολουθίας, </sequenceLink> και <classLink>Κλάσεων</classLink>. Οι άλλοι τύποι θα αποδοθούν ως εικόνα στο Excalidraw.",
"syntax": "Σύνταξη Mermaid", "syntax": "Σύνταξη Mermaid",
"preview": "Προεπισκόπηση", "preview": "Προεπισκόπηση",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+1 -7
View File
@@ -3,10 +3,6 @@
"paste": "Paste", "paste": "Paste",
"pasteAsPlaintext": "Paste as plaintext", "pasteAsPlaintext": "Paste as plaintext",
"pasteCharts": "Paste charts", "pasteCharts": "Paste charts",
"chartType_bar": "Bar chart",
"chartType_line": "Line chart",
"chartType_radar": "Radar chart",
"chartType_plaintext": "Plain text",
"selectAll": "Select all", "selectAll": "Select all",
"multiSelect": "Add element to selection", "multiSelect": "Add element to selection",
"moveCanvas": "Move canvas", "moveCanvas": "Move canvas",
@@ -177,9 +173,7 @@
"tab": "Tab", "tab": "Tab",
"shapeSwitch": "Switch shape", "shapeSwitch": "Switch shape",
"preferences": "Preferences", "preferences": "Preferences",
"preferences_toolLock": "Tool lock", "preferences_toolLock": "Tool lock"
"arrowBinding": "Arrow binding",
"midpointSnapping": "Snap to midpoints"
}, },
"elementLink": { "elementLink": {
"title": "Link to object", "title": "Link to object",
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Pegar", "paste": "Pegar",
"pasteAsPlaintext": "Pegar como texto sin formato", "pasteAsPlaintext": "Pegar como texto sin formato",
"pasteCharts": "Pegar gráficos", "pasteCharts": "Pegar gráficos",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Seleccionar todo", "selectAll": "Seleccionar todo",
"multiSelect": "Añadir elemento a la selección", "multiSelect": "Añadir elemento a la selección",
"moveCanvas": "Mover el lienzo", "moveCanvas": "Mover el lienzo",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Pie de la corona (varios)", "arrowhead_crowfoot_many": "Pie de la corona (varios)",
"arrowhead_crowfoot_one": "Pie de la corona (uno)", "arrowhead_crowfoot_one": "Pie de la corona (uno)",
"arrowhead_crowfoot_one_or_many": "Pie de la corona (uno o varios)", "arrowhead_crowfoot_one_or_many": "Pie de la corona (uno o varios)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Más opciones", "more_options": "Más opciones",
"cardinality": "",
"arrowtypes": "Tipo de flecha", "arrowtypes": "Tipo de flecha",
"arrowtype_sharp": "Flecha Afilada", "arrowtype_sharp": "Flecha Afilada",
"arrowtype_round": "Flecha Curva", "arrowtype_round": "Flecha Curva",
@@ -182,11 +171,7 @@
"linkToElement": "Enlace al objeto", "linkToElement": "Enlace al objeto",
"wrapSelectionInFrame": "Ajustar la selección en el marco", "wrapSelectionInFrame": "Ajustar la selección en el marco",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "Enlace al objeto", "title": "Enlace al objeto",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Error" "title": "Error"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Guardar en disco", "disk_title": "Guardar en disco",
"disk_details": "Exportar los datos de la escena a un archivo desde el cual pueda importar más tarde.", "disk_details": "Exportar los datos de la escena a un archivo desde el cual pueda importar más tarde.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Mermaid a Excalidraw", "title": "Mermaid a Excalidraw",
"button": "Insertar", "button": "Insertar",
"description": "", "description": "Actualmente sólo estos tipos de <flowchartLink>diagrama de flujo</flowchartLink>,<sequenceLink> Secuencia, </sequenceLink> y <classLink>Clase </classLink> son soportados. Los otros tipos de diagramas se renderizarán como imagen en Excalidraw.",
"syntax": "Sintaxis Mermaid", "syntax": "Sintaxis Mermaid",
"preview": "Vista previa", "preview": "Vista previa",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Itsatsi", "paste": "Itsatsi",
"pasteAsPlaintext": "Itsatsi testu arrunt gisa", "pasteAsPlaintext": "Itsatsi testu arrunt gisa",
"pasteCharts": "Itsatsi grafikoak", "pasteCharts": "Itsatsi grafikoak",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Hautatu dena", "selectAll": "Hautatu dena",
"multiSelect": "Gehitu elementua hautapenera", "multiSelect": "Gehitu elementua hautapenera",
"moveCanvas": "Mugitu oihala", "moveCanvas": "Mugitu oihala",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Errorea" "title": "Errorea"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Gorde diskoan", "disk_title": "Gorde diskoan",
"disk_details": "Esportatu eszenaren datuak geroago inportatu ahal izango duzun fitxategi batan.", "disk_details": "Esportatu eszenaren datuak geroago inportatu ahal izango duzun fitxategi batan.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Mermaid-etik Excalidraw-ra", "title": "Mermaid-etik Excalidraw-ra",
"button": "Txertatu", "button": "Txertatu",
"description": "", "description": "Momentu honetan <flowchartLink>Flowchart</flowchartLink>, <sequenceLink> Sequence, </sequenceLink> eta <classLink>Class </classLink>Diagramak onartzen dira. Beste motak irudi gisa errendatuko dira Excalidrawn.",
"syntax": "Mermaid sintaxia", "syntax": "Mermaid sintaxia",
"preview": "Aurrebista", "preview": "Aurrebista",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+11 -31
View File
@@ -3,10 +3,6 @@
"paste": "جایگذاری", "paste": "جایگذاری",
"pasteAsPlaintext": "جایگذاری به عنوان متن ساده", "pasteAsPlaintext": "جایگذاری به عنوان متن ساده",
"pasteCharts": "جایگذاری نمودارها", "pasteCharts": "جایگذاری نمودارها",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "انتخاب همه", "selectAll": "انتخاب همه",
"multiSelect": "یک ایتم به انتخاب شده ها اضافه کنید", "multiSelect": "یک ایتم به انتخاب شده ها اضافه کنید",
"moveCanvas": "جابجایی بوم", "moveCanvas": "جابجایی بوم",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "پای کلاغی (بسیار)", "arrowhead_crowfoot_many": "پای کلاغی (بسیار)",
"arrowhead_crowfoot_one": "پای کلاغی (یک)", "arrowhead_crowfoot_one": "پای کلاغی (یک)",
"arrowhead_crowfoot_one_or_many": "پای کلاغی (یک یا بسیار)", "arrowhead_crowfoot_one_or_many": "پای کلاغی (یک یا بسیار)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "امکانات بیشتر", "more_options": "امکانات بیشتر",
"cardinality": "",
"arrowtypes": "نوع پیکان", "arrowtypes": "نوع پیکان",
"arrowtype_sharp": "پیکان تیز", "arrowtype_sharp": "پیکان تیز",
"arrowtype_round": "پیکان منحنی", "arrowtype_round": "پیکان منحنی",
@@ -182,11 +171,7 @@
"linkToElement": "لینک به آیتم", "linkToElement": "لینک به آیتم",
"wrapSelectionInFrame": "انتخاب را در قاب قرار دهید", "wrapSelectionInFrame": "انتخاب را در قاب قرار دهید",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "لینک به آیتم", "title": "لینک به آیتم",
@@ -211,7 +196,7 @@
"multipleResults": "نتایج", "multipleResults": "نتایج",
"placeholder": "جستجوی متن در بوم...", "placeholder": "جستجوی متن در بوم...",
"frames": "", "frames": "",
"texts": "متن" "texts": ""
}, },
"buttons": { "buttons": {
"clearReset": "پاکسازی بوم نقاشی", "clearReset": "پاکسازی بوم نقاشی",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "خطا" "title": "خطا"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "ذخیره در دیسک", "disk_title": "ذخیره در دیسک",
"disk_details": "داده های صحنه را به فایلی که بعداً می توانید از آن وارد کنید صادر کنید.", "disk_details": "داده های صحنه را به فایلی که بعداً می توانید از آن وارد کنید صادر کنید.",
@@ -590,7 +571,7 @@
} }
}, },
"colorPicker": { "colorPicker": {
"color": "رنگ", "color": "",
"mostUsedCustomColors": "رنگ های به‌تازگی به‌کار گرفته شده", "mostUsedCustomColors": "رنگ های به‌تازگی به‌کار گرفته شده",
"colors": "رنگ‌ها", "colors": "رنگ‌ها",
"shades": "جلوه‌ها", "shades": "جلوه‌ها",
@@ -631,15 +612,14 @@
"mermaid": { "mermaid": {
"title": "مرمید به excalidraw", "title": "مرمید به excalidraw",
"button": "درج", "button": "درج",
"description": "", "description": "فعلا فقط <flowchartLink> فلوچارت </flowchartLink> ، <sequenceLink> توالی </sequenceLink> و <classLink> کلاس </classLink> نمودارها پشتیبانی می شوند. انواع دیگر به صورت تصویر در Excalidraw ارائه خواهند شد.",
"syntax": "مرمید syntax", "syntax": "مرمید syntax",
"preview": "پیشنمایش", "preview": "پیشنمایش",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "خطا!" "error": ""
}, },
"chat": { "chat": {
"inputPlaceholder": "", "inputPlaceholder": "",
@@ -647,8 +627,8 @@
"generating": "", "generating": "",
"rateLimitRemaining": "", "rateLimitRemaining": "",
"role": { "role": {
"user": "شما", "user": "",
"assistant": "دستیار هوش مصنوعی", "assistant": "",
"system": "" "system": ""
}, },
"aiBeta": "", "aiBeta": "",
@@ -727,9 +707,9 @@
"alt": "", "alt": "",
"escape": "", "escape": "",
"enter": "", "enter": "",
"shift": "Shift", "shift": "",
"spacebar": "فاصله", "spacebar": "",
"delete": "حذف", "delete": "",
"mmb": "" "mmb": ""
} }
} }
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Liitä", "paste": "Liitä",
"pasteAsPlaintext": "Liitä pelkkänä tekstinä", "pasteAsPlaintext": "Liitä pelkkänä tekstinä",
"pasteCharts": "Liitä kaaviot", "pasteCharts": "Liitä kaaviot",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Valitse kaikki", "selectAll": "Valitse kaikki",
"multiSelect": "Lisää kohde valintaan", "multiSelect": "Lisää kohde valintaan",
"moveCanvas": "Siirrä piirtoaluetta", "moveCanvas": "Siirrä piirtoaluetta",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Virhe" "title": "Virhe"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Tallenna levylle", "disk_title": "Tallenna levylle",
"disk_details": "Vie työn tiedot tiedostoon, josta sen voi tuoda myöhemmin.", "disk_details": "Vie työn tiedot tiedostoon, josta sen voi tuoda myöhemmin.",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "Esikatsele", "preview": "Esikatsele",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+40 -60
View File
@@ -3,10 +3,6 @@
"paste": "Coller", "paste": "Coller",
"pasteAsPlaintext": "Coller comme texte brut", "pasteAsPlaintext": "Coller comme texte brut",
"pasteCharts": "Coller les graphiques", "pasteCharts": "Coller les graphiques",
"chartType_bar": "Graphique à barres",
"chartType_line": "Courbes",
"chartType_radar": "Diagramme en radar",
"chartType_plaintext": "Texte brut",
"selectAll": "Tout sélectionner", "selectAll": "Tout sélectionner",
"multiSelect": "Ajouter l'élément à la sélection", "multiSelect": "Ajouter l'élément à la sélection",
"moveCanvas": "Déplacer le canevas", "moveCanvas": "Déplacer le canevas",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "Pied de Corde (un)", "arrowhead_crowfoot_one": "Pied de Corde (un)",
"arrowhead_crowfoot_one_or_many": "Pied du corbeau (un ou plusieurs)", "arrowhead_crowfoot_one_or_many": "Pied du corbeau (un ou plusieurs)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Plus d'options", "more_options": "Plus d'options",
"cardinality": "",
"arrowtypes": "Type de flèche", "arrowtypes": "Type de flèche",
"arrowtype_sharp": "Flèche pointue", "arrowtype_sharp": "Flèche pointue",
"arrowtype_round": "Flèche incurvée", "arrowtype_round": "Flèche incurvée",
@@ -182,11 +171,7 @@
"linkToElement": "Lien vers un objet", "linkToElement": "Lien vers un objet",
"wrapSelectionInFrame": "Mettre la sélection dans un cadre", "wrapSelectionInFrame": "Mettre la sélection dans un cadre",
"tab": "Onglet", "tab": "Onglet",
"shapeSwitch": "Changer de forme", "shapeSwitch": "Changer de forme"
"preferences": "Préférences",
"preferences_toolLock": "Verrouillage de l'outil",
"arrowBinding": "Liaison de flèches",
"midpointSnapping": "S'accrocher aux points intermédiaires"
}, },
"elementLink": { "elementLink": {
"title": "Lien vers un objet", "title": "Lien vers un objet",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Erreur" "title": "Erreur"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Enregistrer sur le disque", "disk_title": "Enregistrer sur le disque",
"disk_details": "Exporter les données de la scène comme un fichier que vous pourrez importer ultérieurement.", "disk_details": "Exporter les données de la scène comme un fichier que vous pourrez importer ultérieurement.",
@@ -576,9 +557,9 @@
}, },
"welcomeScreen": { "welcomeScreen": {
"app": { "app": {
"center_heading": "Vos dessins sont enregistrés dans le stockage de votre navigateur.", "center_heading": "",
"center_heading_line2": "Le stockage du navigateur peut être effacé de manière inattendue.", "center_heading_line2": "",
"center_heading_line3": "Enregistrez régulièrement votre travail dans un fichier pour éviter de le perdre.", "center_heading_line3": "",
"center_heading_plus": "Vouliez-vous plutôt aller à Excalidraw+ à la place ?", "center_heading_plus": "Vouliez-vous plutôt aller à Excalidraw+ à la place ?",
"menuHint": "Exportation, préférences, langues, ..." "menuHint": "Exportation, préférences, langues, ..."
}, },
@@ -631,58 +612,57 @@
"mermaid": { "mermaid": {
"title": "De Mermaid à Excalidraw", "title": "De Mermaid à Excalidraw",
"button": "Insérer", "button": "Insérer",
"description": "", "description": "Actuellement, seuls les diagrammes <flowchartLink>Flowchart</flowchartLink>,<sequenceLink> Sequence, </sequenceLink> et <classLink>de classe </classLink>sont pris en charge. Les autres types seront rendus en tant qu'image dans Excalidraw.",
"syntax": "Syntaxe Mermaid", "syntax": "Syntaxe Mermaid",
"preview": "Prévisualisation", "preview": "Prévisualisation",
"label": "Mermaid", "label": "",
"inputPlaceholder": "Écrivez la définition du diagramme de Mermaid ici...", "inputPlaceholder": ""
"autoFixAvailable": "Correction automatique disponible"
}, },
"ttd": { "ttd": {
"error": "Erreur!" "error": ""
}, },
"chat": { "chat": {
"inputPlaceholder": "Commencez à taper votre idée de diagramme ici... ({{shortcut}} pour une nouvelle ligne)", "inputPlaceholder": "",
"inputPlaceholderWithMessages": "Continuer à affiner votre diagramme...", "inputPlaceholderWithMessages": "",
"generating": "Génération...", "generating": "",
"rateLimitRemaining": "{{count}} demandes restantes aujourd'hui", "rateLimitRemaining": "",
"role": { "role": {
"user": "Vous", "user": "",
"assistant": "Assistant IA", "assistant": "",
"system": "Système" "system": ""
}, },
"aiBeta": "Bêta IA", "aiBeta": "",
"label": "Chat", "label": "",
"menu": "Menu", "menu": "",
"newChat": "Nouveau chat", "newChat": "",
"deleteChat": "Supprimer le chat", "deleteChat": "",
"deleteMessage": "Supprimer le message", "deleteMessage": "",
"viewAsMermaid": "Voir en tant que Mermaid", "viewAsMermaid": "",
"placeholder": { "placeholder": {
"title": "Créons votre diagramme", "title": "",
"description": "Décrivez le diagramme que vous voulez créer, et nous le générerons pour vous.", "description": "",
"hint": "" "hint": ""
}, },
"preview": "Prévisualisation", "preview": "",
"insert": "Insérer", "insert": "",
"retry": "Recommencer", "retry": "",
"errors": { "errors": {
"promptTooShort": "La requête est trop courte (min {{min}} caractères)", "promptTooShort": "",
"promptTooLong": "La requête est trop longue (max {{max}} caractères)", "promptTooLong": "",
"generationFailed": "Échec de la génération", "generationFailed": "",
"invalidDiagram": "A généré un diagramme non valide :(. Vous pouvez modifier manuellement, réessayer avec la correction automatique, ou essayer une invite différente.", "invalidDiagram": "",
"fixInMermaid": "Modifier Mermaid manuellement →", "fixInMermaid": "",
"aiRepair": "Régénérer (correction automatique) →", "aiRepair": "",
"requestAborted": "Demande abandonnée", "requestAborted": "",
"requestFailed": "La requête a échoué", "requestFailed": "",
"mermaidParseError": "Erreur de syntaxe Mermaid" "mermaidParseError": ""
}, },
"rateLimit": { "rateLimit": {
"messageLimit": "Vous avez atteint votre limite d'IA sur le plan gratuit. Essayez Excalidraw+ pour plus ou revenez demain.", "messageLimit": "",
"generalRateLimit": "Tenez vos chevaux, vous êtes trop rapide pour nous! Veuillez attendre un instant avant de réessayer.", "generalRateLimit": "",
"messageLimitInputPlaceholder": "Vous avez atteint votre limite de messages" "messageLimitInputPlaceholder": ""
}, },
"upsellBtnLabel": "Devenez Premium" "upsellBtnLabel": ""
}, },
"quickSearch": { "quickSearch": {
"placeholder": "Recherche rapide" "placeholder": "Recherche rapide"
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Pegar", "paste": "Pegar",
"pasteAsPlaintext": "Pegar coma texto sen formato", "pasteAsPlaintext": "Pegar coma texto sen formato",
"pasteCharts": "Pegar gráficos", "pasteCharts": "Pegar gráficos",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Seleccionar todo", "selectAll": "Seleccionar todo",
"multiSelect": "Engadir elemento á selección", "multiSelect": "Engadir elemento á selección",
"moveCanvas": "Mover o lenzo", "moveCanvas": "Mover o lenzo",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Erro" "title": "Erro"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Gardar no disco", "disk_title": "Gardar no disco",
"disk_details": "Exporte os datos da escena a un ficheiro que poderás importar máis tarde.", "disk_details": "Exporte os datos da escena a un ficheiro que poderás importar máis tarde.",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "Vista previa", "preview": "Vista previa",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "הדבקה", "paste": "הדבקה",
"pasteAsPlaintext": "הדבקה ללא עיצוב", "pasteAsPlaintext": "הדבקה ללא עיצוב",
"pasteCharts": "הדבקת תרשימים", "pasteCharts": "הדבקת תרשימים",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "בחירה בהכול", "selectAll": "בחירה בהכול",
"multiSelect": "הוספת רכיב לבחירה", "multiSelect": "הוספת רכיב לבחירה",
"moveCanvas": "הזזת הקנבס", "moveCanvas": "הזזת הקנבס",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "רגל עורב (הרבה)", "arrowhead_crowfoot_many": "רגל עורב (הרבה)",
"arrowhead_crowfoot_one": "רגל עורב (אחת)", "arrowhead_crowfoot_one": "רגל עורב (אחת)",
"arrowhead_crowfoot_one_or_many": "רגל עורב (אחת או הרבה)", "arrowhead_crowfoot_one_or_many": "רגל עורב (אחת או הרבה)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "אפשרויות נוספות", "more_options": "אפשרויות נוספות",
"cardinality": "",
"arrowtypes": "סוג החץ", "arrowtypes": "סוג החץ",
"arrowtype_sharp": "חץ מחודד", "arrowtype_sharp": "חץ מחודד",
"arrowtype_round": "חץ מעוקל", "arrowtype_round": "חץ מעוקל",
@@ -182,11 +171,7 @@
"linkToElement": "קישור לפריט", "linkToElement": "קישור לפריט",
"wrapSelectionInFrame": "לעטוף את הבחירה במסגרת", "wrapSelectionInFrame": "לעטוף את הבחירה במסגרת",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "קישור לפריט", "title": "קישור לפריט",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "שגיאה" "title": "שגיאה"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "שמור לכונן", "disk_title": "שמור לכונן",
"disk_details": "ייצא מידע של הקנבאס לקובץ שתוכל לייבא אחר כך.", "disk_details": "ייצא מידע של הקנבאס לקובץ שתוכל לייבא אחר כך.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Mermaid ל־Excalidraw", "title": "Mermaid ל־Excalidraw",
"button": "הוספה", "button": "הוספה",
"description": "", "description": "לעת עתה נתמכים רק <flowchartLink>תרשימי זרימה</flowchartLink>, <sequenceLink>תהליכים</sequenceLink>, <classLink>ודיאגרמת מחלקה</classLink>. שאר הסוגים ייוצרו כתמונות ב-Excalidraw.",
"syntax": "תחביר Mermaid", "syntax": "תחביר Mermaid",
"preview": "תצוגה מקדימה", "preview": "תצוגה מקדימה",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "पेस्ट", "paste": "पेस्ट",
"pasteAsPlaintext": "सादे पाठ के रूप में चिपकाएं", "pasteAsPlaintext": "सादे पाठ के रूप में चिपकाएं",
"pasteCharts": "चार्ट चिपकाएँ", "pasteCharts": "चार्ट चिपकाएँ",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "सेलेक्ट ऑल", "selectAll": "सेलेक्ट ऑल",
"multiSelect": "अवयव को चयन में सम्मिलित करें", "multiSelect": "अवयव को चयन में सम्मिलित करें",
"moveCanvas": "चित्रपटल को स्थानांतरित करें", "moveCanvas": "चित्रपटल को स्थानांतरित करें",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "चिड़िया पैर (अनेक)", "arrowhead_crowfoot_many": "चिड़िया पैर (अनेक)",
"arrowhead_crowfoot_one": "चिड़िया पैर (एक)", "arrowhead_crowfoot_one": "चिड़िया पैर (एक)",
"arrowhead_crowfoot_one_or_many": "चिड़िया पैर (एक या अनेक)", "arrowhead_crowfoot_one_or_many": "चिड़िया पैर (एक या अनेक)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "और विकल्प", "more_options": "और विकल्प",
"cardinality": "",
"arrowtypes": "तीर प्रकार", "arrowtypes": "तीर प्रकार",
"arrowtype_sharp": "तीक्ष्ण तीर", "arrowtype_sharp": "तीक्ष्ण तीर",
"arrowtype_round": "गोलाकार तीर", "arrowtype_round": "गोलाकार तीर",
@@ -182,11 +171,7 @@
"linkToElement": "वस्तु की कड़ी", "linkToElement": "वस्तु की कड़ी",
"wrapSelectionInFrame": "चौकट में चुने हुवे को लपेटे", "wrapSelectionInFrame": "चौकट में चुने हुवे को लपेटे",
"tab": "", "tab": "",
"shapeSwitch": "आकार बदलें", "shapeSwitch": "आकार बदलें"
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "वस्तु की कड़ी", "title": "वस्तु की कड़ी",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "त्रुटि" "title": "त्रुटि"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "डिस्क पर सुरक्षित करे", "disk_title": "डिस्क पर सुरक्षित करे",
"disk_details": "दृश्य डेटा एक फ़ाइल में निर्यात करे, जहांसे आप उसे पुनः आयात कर सके", "disk_details": "दृश्य डेटा एक फ़ाइल में निर्यात करे, जहांसे आप उसे पुनः आयात कर सके",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "मर्मेड से एक्सकाली में", "title": "मर्मेड से एक्सकाली में",
"button": "सन्निवेश करे", "button": "सन्निवेश करे",
"description": "", "description": "वर्तमान में केवल <flowchartLink>बहाव चित्र</flowchartLink>, <sequenceLink> अनुक्रम चित्र </sequenceLink> और <classLink>वर्ग चित्र</classLink> का चित्रिकरण संभव हैं. अन्य चित्र प्रकार एक्सकाली प्रतिमा जैसे चित्रित किए जायेंगे.",
"syntax": "मर्मेड विन्यास", "syntax": "मर्मेड विन्यास",
"preview": "पूर्वावलोकन", "preview": "पूर्वावलोकन",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Beillesztés", "paste": "Beillesztés",
"pasteAsPlaintext": "Beillesztés formázatlan szövegként", "pasteAsPlaintext": "Beillesztés formázatlan szövegként",
"pasteCharts": "Grafikon beillesztése", "pasteCharts": "Grafikon beillesztése",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Összes kijelölése", "selectAll": "Összes kijelölése",
"multiSelect": "Elem hozzáadása a kijelöléshez", "multiSelect": "Elem hozzáadása a kijelöléshez",
"moveCanvas": "Vászon mozgatása", "moveCanvas": "Vászon mozgatása",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "Nyíl típus", "arrowtypes": "Nyíl típus",
"arrowtype_sharp": "Hegyes nyíl", "arrowtype_sharp": "Hegyes nyíl",
"arrowtype_round": "Ívelt nyíl", "arrowtype_round": "Ívelt nyíl",
@@ -182,11 +171,7 @@
"linkToElement": "Hivatkozás az objektumhoz", "linkToElement": "Hivatkozás az objektumhoz",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "Tab", "tab": "Tab",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "Hivatkozás az objektumhoz", "title": "Hivatkozás az objektumhoz",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Hiba" "title": "Hiba"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Mentés lemezre", "disk_title": "Mentés lemezre",
"disk_details": "Exportálja a jelenetadatokat egy fájlba, amelyből később importálhatja.", "disk_details": "Exportálja a jelenetadatokat egy fájlba, amelyből később importálhatja.",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Tempel", "paste": "Tempel",
"pasteAsPlaintext": "Tempel sebagai teks biasa", "pasteAsPlaintext": "Tempel sebagai teks biasa",
"pasteCharts": "Tempel diagram", "pasteCharts": "Tempel diagram",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Pilih semua", "selectAll": "Pilih semua",
"multiSelect": "Tambah unsur ke seleksi", "multiSelect": "Tambah unsur ke seleksi",
"moveCanvas": "Pindahkan kanvas", "moveCanvas": "Pindahkan kanvas",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Pilihan lain", "more_options": "Pilihan lain",
"cardinality": "",
"arrowtypes": "Jenis panah", "arrowtypes": "Jenis panah",
"arrowtype_sharp": "Panah runcing", "arrowtype_sharp": "Panah runcing",
"arrowtype_round": "Panah melengkung", "arrowtype_round": "Panah melengkung",
@@ -182,11 +171,7 @@
"linkToElement": "Taut ke objek", "linkToElement": "Taut ke objek",
"wrapSelectionInFrame": "Bungkus pilihan dalam bingkai", "wrapSelectionInFrame": "Bungkus pilihan dalam bingkai",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "Taut ke objek", "title": "Taut ke objek",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Kesalahan" "title": "Kesalahan"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Simpan ke disk", "disk_title": "Simpan ke disk",
"disk_details": "Ekspor data pemandangan ke file yang mana Anda dapat impor nanti.", "disk_details": "Ekspor data pemandangan ke file yang mana Anda dapat impor nanti.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Mermaid menjadi Excalidraw", "title": "Mermaid menjadi Excalidraw",
"button": "Sisipkan", "button": "Sisipkan",
"description": "", "description": "Saat ini hanya <flowchartLink>Flowchart</flowchartLink>, <sequenceLink>Sekuen, </sequenceLink>, dan <classLink>Kelas</classLink>Diagram yang didukung. Jenis lainnya akan dirender sebagai gambar di Excalidraw.",
"syntax": "Syntax Mermaid", "syntax": "Syntax Mermaid",
"preview": "Pratinjau", "preview": "Pratinjau",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+7 -27
View File
@@ -3,10 +3,6 @@
"paste": "Incolla", "paste": "Incolla",
"pasteAsPlaintext": "Incolla come testo normale", "pasteAsPlaintext": "Incolla come testo normale",
"pasteCharts": "Incolla grafici", "pasteCharts": "Incolla grafici",
"chartType_bar": "Grafico a barre",
"chartType_line": "Grafico a linee",
"chartType_radar": "Grafico radar",
"chartType_plaintext": "Testo semplice",
"selectAll": "Seleziona tutto", "selectAll": "Seleziona tutto",
"multiSelect": "Aggiungi elemento alla selezione", "multiSelect": "Aggiungi elemento alla selezione",
"moveCanvas": "Sposta tela", "moveCanvas": "Sposta tela",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Zampe di gallina (molti)", "arrowhead_crowfoot_many": "Zampe di gallina (molti)",
"arrowhead_crowfoot_one": "Zampa di gallina (una)", "arrowhead_crowfoot_one": "Zampa di gallina (una)",
"arrowhead_crowfoot_one_or_many": "Zampe di gallina (una o molte)", "arrowhead_crowfoot_one_or_many": "Zampe di gallina (una o molte)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Altre opzioni", "more_options": "Altre opzioni",
"cardinality": "",
"arrowtypes": "Tipo di freccia", "arrowtypes": "Tipo di freccia",
"arrowtype_sharp": "Freccia affilata", "arrowtype_sharp": "Freccia affilata",
"arrowtype_round": "Freccia curva", "arrowtype_round": "Freccia curva",
@@ -182,11 +171,7 @@
"linkToElement": "Link all'oggetto", "linkToElement": "Link all'oggetto",
"wrapSelectionInFrame": "Avvolgi la selezione nella cornice", "wrapSelectionInFrame": "Avvolgi la selezione nella cornice",
"tab": "Scheda", "tab": "Scheda",
"shapeSwitch": "Cambia forma", "shapeSwitch": "Cambia forma"
"preferences": "Preferenze",
"preferences_toolLock": "Blocco strumento",
"arrowBinding": "Legatura a freccia",
"midpointSnapping": "Aggancia ai punti medi"
}, },
"elementLink": { "elementLink": {
"title": "Link all'oggetto", "title": "Link all'oggetto",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Errore" "title": "Errore"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Salva su disco", "disk_title": "Salva su disco",
"disk_details": "Esporta i dati della scena su file, dal quale potrai importare in seguito.", "disk_details": "Esporta i dati della scena su file, dal quale potrai importare in seguito.",
@@ -576,9 +557,9 @@
}, },
"welcomeScreen": { "welcomeScreen": {
"app": { "app": {
"center_heading": "I tuoi disegni vengono salvati nella memoria del tuo browser.", "center_heading": "",
"center_heading_line2": "La memoria del browser potrebbe essere cancellata inaspettatamente.", "center_heading_line2": "",
"center_heading_line3": "Salva regolarmente il tuo lavoro in un file per evitare di perderlo.", "center_heading_line3": "",
"center_heading_plus": "Volevi invece andare su Excalidraw+?", "center_heading_plus": "Volevi invece andare su Excalidraw+?",
"menuHint": "Esporta, preferenze, lingue, ..." "menuHint": "Esporta, preferenze, lingue, ..."
}, },
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Da Mermaid a Excalidraw", "title": "Da Mermaid a Excalidraw",
"button": "Inserisci", "button": "Inserisci",
"description": "", "description": "Attualmente sono supportati solo diagrammi di <flowchartLink>flusso</flowchartLink>,<sequenceLink> sequenza, </sequenceLink> e <classLink>classe </classLink>. Gli altri tipi saranno rappresentati come immagini in Excalidraw.",
"syntax": "Sintassi Mermaid", "syntax": "Sintassi Mermaid",
"preview": "Anteprima", "preview": "Anteprima",
"label": "Mermaid", "label": "Mermaid",
"inputPlaceholder": "Scrivi qui la definizione del diagramma Mermaid...", "inputPlaceholder": "Scrivi qui la definizione del diagramma Mermaid..."
"autoFixAvailable": "Correzione automatica disponibile"
}, },
"ttd": { "ttd": {
"error": "Errore!" "error": "Errore!"
@@ -661,7 +641,7 @@
"placeholder": { "placeholder": {
"title": "Progettiamo il tuo diagramma", "title": "Progettiamo il tuo diagramma",
"description": "Descrivi il diagramma che vuoi creare e noi lo genereremo per te.", "description": "Descrivi il diagramma che vuoi creare e noi lo genereremo per te.",
"hint": "" "hint": "Al momento conosciamo i diagrammi di flusso, di sequenza e di classe."
}, },
"preview": "Anteprima", "preview": "Anteprima",
"insert": "Inserisci", "insert": "Inserisci",
+142 -162
View File
@@ -3,10 +3,6 @@
"paste": "貼り付け", "paste": "貼り付け",
"pasteAsPlaintext": "書式なしテキストとして貼り付け", "pasteAsPlaintext": "書式なしテキストとして貼り付け",
"pasteCharts": "チャートの貼り付け", "pasteCharts": "チャートの貼り付け",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "すべて選択", "selectAll": "すべて選択",
"multiSelect": "複数選択", "multiSelect": "複数選択",
"moveCanvas": "キャンバスを移動", "moveCanvas": "キャンバスを移動",
@@ -38,7 +34,7 @@
"opacity": "透明度", "opacity": "透明度",
"textAlign": "文字の配置", "textAlign": "文字の配置",
"edges": "角", "edges": "角",
"sharp": "シャープ", "sharp": "四角",
"round": "丸", "round": "丸",
"arrowheads": "線の終点", "arrowheads": "線の終点",
"arrowhead_none": "なし", "arrowhead_none": "なし",
@@ -50,21 +46,14 @@
"arrowhead_triangle_outline": "三角 (中抜き)", "arrowhead_triangle_outline": "三角 (中抜き)",
"arrowhead_diamond": "ひし形", "arrowhead_diamond": "ひし形",
"arrowhead_diamond_outline": "ひし形 (中抜き)", "arrowhead_diamond_outline": "ひし形 (中抜き)",
"arrowhead_crowfoot_many": "カラスの足 (*)", "arrowhead_crowfoot_many": "鳥の足記法(多対多)",
"arrowhead_crowfoot_one": "カラスの足 (1)", "arrowhead_crowfoot_one": "鳥の足記法(一対一)",
"arrowhead_crowfoot_one_or_many": "カラスの足 (1..*)", "arrowhead_crowfoot_one_or_many": "鳥の足記法(一対多)",
"arrowhead_cardinality_one": "", "more_options": "詳細設定",
"arrowhead_cardinality_many": "", "arrowtypes": "矢印の種類",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "その他のオプション",
"cardinality": "",
"arrowtypes": "矢印タイプ",
"arrowtype_sharp": "鋭い矢印", "arrowtype_sharp": "鋭い矢印",
"arrowtype_round": "曲線矢印", "arrowtype_round": "曲線矢印",
"arrowtype_elbowed": "折れ線矢印", "arrowtype_elbowed": "ひじ矢印",
"fontSize": "フォントの大きさ", "fontSize": "フォントの大きさ",
"fontFamily": "フォントの種類", "fontFamily": "フォントの種類",
"addWatermark": "\"Made with Excalidraw\"と表示", "addWatermark": "\"Made with Excalidraw\"と表示",
@@ -86,14 +75,14 @@
"right": "右寄せ", "right": "右寄せ",
"extraBold": "極太", "extraBold": "極太",
"architect": "建築家", "architect": "建築家",
"artist": "画家", "artist": "アーティスト",
"cartoonist": "漫画家", "cartoonist": "漫画家",
"fileTitle": "ファイル名", "fileTitle": "ファイル名",
"colorPicker": "カラーピッカー", "colorPicker": "カラーピッカー",
"canvasColors": "キャンバス上で使用", "canvasColors": "キャンバス上で使用",
"canvasBackground": "キャンバスの背景", "canvasBackground": "キャンバスの背景",
"drawingCanvas": "キャンバスの描画", "drawingCanvas": "キャンバスの描画",
"clearCanvas": "キャンバスを消去", "clearCanvas": "キャンバスを片付ける",
"layers": "レイヤー", "layers": "レイヤー",
"actions": "操作", "actions": "操作",
"language": "言語", "language": "言語",
@@ -112,7 +101,7 @@
"libraryLoadingMessage": "ライブラリを読み込み中…", "libraryLoadingMessage": "ライブラリを読み込み中…",
"libraries": "ライブラリを参照する", "libraries": "ライブラリを参照する",
"loadingScene": "シーンを読み込み中…", "loadingScene": "シーンを読み込み中…",
"loadScene": "ファイルからシーンを開く", "loadScene": "ファイルからシーン",
"align": "配置", "align": "配置",
"alignTop": "上揃え", "alignTop": "上揃え",
"alignBottom": "下揃え", "alignBottom": "下揃え",
@@ -171,22 +160,18 @@
"prompt": "プロンプト", "prompt": "プロンプト",
"followUs": "フォローする", "followUs": "フォローする",
"discordChat": "Discord チャット", "discordChat": "Discord チャット",
"zoomToFitViewport": "表範囲に合わせてズーム", "zoomToFitViewport": "表寿範囲に合わせてズーム",
"zoomToFitSelection": "選択範囲に合わせてズーム", "zoomToFitSelection": "選択範囲に合わせてズーム",
"zoomToFit": "すべての要素が収まるようにズーム", "zoomToFit": "すべての要素が収まるようにズーム",
"installPWA": "Excalidrawをローカルにインストール(PWA)", "installPWA": "Excalidrawをローカルにインストール(PWA)",
"autoResize": "テキストの自動サイズ変更を有効化", "autoResize": "テキストの自動サイズ変更を有効化",
"imageCropping": "画像の切り抜き", "imageCropping": "画像のトリミング",
"unCroppedDimension": "元のサイズ", "unCroppedDimension": "",
"copyElementLink": "オブジェクトへのリンクをコピー", "copyElementLink": "",
"linkToElement": "オブジェクトにリンク", "linkToElement": "オブジェクトにリンク",
"wrapSelectionInFrame": "選択範囲をフレームで囲む", "wrapSelectionInFrame": "選択範囲を枠で折り返す",
"tab": "Tab", "tab": "Tab",
"shapeSwitch": "図形形状を変更", "shapeSwitch": "図形形状を変更"
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "オブジェクトにリンク", "title": "オブジェクトにリンク",
@@ -200,15 +185,15 @@
"search": { "search": {
"inputPlaceholder": "ライブラリを検索", "inputPlaceholder": "ライブラリを検索",
"heading": "ライブラリと一致", "heading": "ライブラリと一致",
"noResults": "一致するアイテムはありません…", "noResults": "一致するアイテムが見つかりませんでした…",
"clearSearch": "検索のクリア" "clearSearch": "検索のクリア"
} }
}, },
"search": { "search": {
"title": "キャンバスで検索", "title": "キャンバスで検索",
"noMatch": "一致する結果はありません…", "noMatch": "一致なし…",
"singleResult": "", "singleResult": "結果",
"multipleResults": "", "multipleResults": "結果数",
"placeholder": "キャンバス内のテキストを検索…", "placeholder": "キャンバス内のテキストを検索…",
"frames": "フレーム", "frames": "フレーム",
"texts": "テキスト" "texts": "テキスト"
@@ -255,7 +240,7 @@
"embeddableInteractionButton": "クリックして操作" "embeddableInteractionButton": "クリックして操作"
}, },
"alerts": { "alerts": {
"clearReset": "キャンバス全体を消去します。本当によろしいですか?", "clearReset": "この操作によってキャンバス全体が消えます。よろしいですか?",
"couldNotCreateShareableLink": "共有URLを作成できませんでした。", "couldNotCreateShareableLink": "共有URLを作成できませんでした。",
"couldNotCreateShareableLinkTooBig": "共有可能なリンクを作成できませんでした: シーンが大きすぎます", "couldNotCreateShareableLinkTooBig": "共有可能なリンクを作成できませんでした: シーンが大きすぎます",
"couldNotLoadInvalidFile": "無効なファイルを読み込めませんでした。", "couldNotLoadInvalidFile": "無効なファイルを読み込めませんでした。",
@@ -264,8 +249,8 @@
"couldNotCopyToClipboard": "クリップボードにコピーできませんでした。", "couldNotCopyToClipboard": "クリップボードにコピーできませんでした。",
"decryptFailed": "データを復号できませんでした。", "decryptFailed": "データを復号できませんでした。",
"uploadedSecurly": "データのアップロードはエンドツーエンド暗号化によって保護されています。Excalidrawサーバーと第三者はデータの内容を見ることができません。", "uploadedSecurly": "データのアップロードはエンドツーエンド暗号化によって保護されています。Excalidrawサーバーと第三者はデータの内容を見ることができません。",
"loadSceneOverridePrompt": "外部の描画データを読み込むと、既存のコンテンツが置き換わります。続行しますか?", "loadSceneOverridePrompt": "外部図面を読み込むと、既存のコンテンツが置き換わります。続行しますか?",
"collabStopOverridePrompt": "セッションを停止すると、ローカルに保存されている図が上書きされます。 本当によろしいですか?\n\n(ローカルの図を保持したい場合は、セッションを停止せずにブラウザーのタブを閉じてください。)", "collabStopOverridePrompt": "セッションを停止すると、ローカルに保存されている図が上書きされます。 本当によろしいですか?\n\n(ローカルの図を保持したい場合は、セッションを停止せずにブラウザタブを閉じてください。)",
"errorAddingToLibrary": "アイテムをライブラリに追加できませんでした", "errorAddingToLibrary": "アイテムをライブラリに追加できませんでした",
"errorRemovingFromLibrary": "ライブラリからアイテムを削除できませんでした", "errorRemovingFromLibrary": "ライブラリからアイテムを削除できませんでした",
"confirmAddLibrary": "{{numShapes}} 個の図形をライブラリに追加します。よろしいですか?", "confirmAddLibrary": "{{numShapes}} 個の図形をライブラリに追加します。よろしいですか?",
@@ -276,7 +261,7 @@
"removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?", "removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?",
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。共同編集は無効化されています。", "invalidEncryptionKey": "暗号化キーは22文字でなければなりません。共同編集は無効化されています。",
"collabOfflineWarning": "インターネットに接続されていません。\n変更は保存されません!", "collabOfflineWarning": "インターネットに接続されていません。\n変更は保存されません!",
"localStorageQuotaExceeded": "ブラウザーのストレージ容量を超えました。変更は保存されません。" "localStorageQuotaExceeded": ""
}, },
"errors": { "errors": {
"unsupportedFileType": "サポートされていないファイル形式です。", "unsupportedFileType": "サポートされていないファイル形式です。",
@@ -286,13 +271,13 @@
"failedToFetchImage": "画像の読み込みに失敗しました。", "failedToFetchImage": "画像の読み込みに失敗しました。",
"cannotResolveCollabServer": "コラボレーションサーバに接続できませんでした。ページを再読み込みして、もう一度お試しください。", "cannotResolveCollabServer": "コラボレーションサーバに接続できませんでした。ページを再読み込みして、もう一度お試しください。",
"importLibraryError": "ライブラリを読み込めませんでした。", "importLibraryError": "ライブラリを読み込めませんでした。",
"saveLibraryError": "ライブラリをストレージに保存できませんでした。変更を失わないように、ローカルにファイルとして保存してください。", "saveLibraryError": "",
"collabSaveFailed": "バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。", "collabSaveFailed": "バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。",
"collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。", "collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。",
"imageToolNotSupported": "画像ツールは使用不可です。", "imageToolNotSupported": "画像ツールは使用不可です。",
"brave_measure_text_error": { "brave_measure_text_error": {
"line1": "<bold>Aggressly Block Fingerprinting</bold> 設定が有効なBraveブラウザを使用しているようです。", "line1": "<bold>Aggressly Block Fingerprinting</bold> 設定が有効なBraveブラウザを使用しているようです。",
"line2": "これにより、描画内の<bold>テキスト要素</bold>が壊れる可能性があります。", "line2": "これにより、図面の <bold>テキスト要素</bold> が壊れる可能性があります。",
"line3": "この設定を無効にすることを強く推奨します。 <link>設定手順</link> をこちらから確認できます。", "line3": "この設定を無効にすることを強く推奨します。 <link>設定手順</link> をこちらから確認できます。",
"line4": "この設定を無効にすると、テキスト要素の表示が修正されません。 GitHub で <issueLink>Issue</issueLink> を開くか、 <discordLink>Discord</discordLink> にご記入ください" "line4": "この設定を無効にすると、テキスト要素の表示が修正されません。 GitHub で <issueLink>Issue</issueLink> を開くか、 <discordLink>Discord</discordLink> にご記入ください"
}, },
@@ -307,9 +292,9 @@
}, },
"toolBar": { "toolBar": {
"selection": "選択", "selection": "選択",
"lasso": "なげなわ選択", "lasso": "",
"image": "画像を挿入", "image": "画像を挿入",
"rectangle": "長方形", "rectangle": "形",
"diamond": "ひし形", "diamond": "ひし形",
"ellipse": "楕円", "ellipse": "楕円",
"arrow": "矢印", "arrow": "矢印",
@@ -319,31 +304,31 @@
"library": "ライブラリ", "library": "ライブラリ",
"lock": "描画後も使用中のツールを選択したままにする", "lock": "描画後も使用中のツールを選択したままにする",
"penMode": "ペンモード - タッチ防止", "penMode": "ペンモード - タッチ防止",
"link": "選択した図形にリンクを追加・更新", "link": "",
"eraser": "消しゴム", "eraser": "消しゴム",
"frame": "フレーム", "frame": "フレームツール",
"magicframe": "ワイヤーフレームからコードを生成", "magicframe": "ワイヤーフレームからコードを生成",
"embeddable": "Web埋め込み", "embeddable": "Web埋め込み",
"laser": "レーザーポインター", "laser": "レーザーポインター",
"hand": "手 (パンニングツール)", "hand": "手 (パンニングツール)",
"extraTools": "その他のツール", "extraTools": "その他のツール",
"mermaidToExcalidraw": "Mermaid を Excalidraw に変換", "mermaidToExcalidraw": "Mermaid を Excalidraw に変換",
"convertElementType": "図形タイプを切り替え" "convertElementType": ""
}, },
"element": { "element": {
"rectangle": "長方形", "rectangle": "",
"diamond": "ひし形", "diamond": "",
"ellipse": "楕円", "ellipse": "楕円",
"arrow": "矢印", "arrow": "",
"line": "", "line": "",
"freedraw": "フリーハンド", "freedraw": "",
"text": "文字", "text": "文字",
"image": "画像", "image": "画像",
"group": "グループ", "group": "グループ",
"frame": "フレーム", "frame": "フレーム",
"magicframe": "ワイヤーフレームからコードを生成", "magicframe": "ワイヤーフレームからコードを生成",
"embeddable": "Web埋め込み", "embeddable": "",
"selection": "選択", "selection": "",
"iframe": "IFrame" "iframe": "IFrame"
}, },
"headings": { "headings": {
@@ -352,34 +337,34 @@
"shapes": "図形" "shapes": "図形"
}, },
"hints": { "hints": {
"dismissSearch": "{{shortcut}} で検索を閉じる", "dismissSearch": "",
"canvasPanning": "キャンバスを移動するには、{{shortcut_1}} か {{shortcut_2}} を押しながらドラッグ、または手のひらツールを使用", "canvasPanning": "",
"linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線", "linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線",
"arrowTool": "クリックで複数の点を追加、ドラッグで直線。{{shortcut}} を再度押すと矢印タイプが変更されます。", "arrowTool": "",
"arrowBindModifiers": "{{shortcut_1}} を押し続けるとバインドを無効化、{{shortcut_2}} を押し続けると固定点にバインド", "arrowBindModifiers": "",
"freeDraw": "クリックしてドラッグします。離すと終了します", "freeDraw": "クリックしてドラッグします。離すと終了します",
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます", "text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
"embeddable": "クリックしてドラッグし、ウェブサイトを埋め込む", "embeddable": "クリックしてドラッグし、ウェブサイトを埋め込む",
"text_selected": "ダブルクリック、または {{shortcut}} を押してテキストを編集", "text_selected": "",
"text_editing": "{{shortcut_1}} または {{shortcut_2}} を押して編集を終了", "text_editing": "",
"linearElementMulti": "最後の点をクリック、または {{shortcut_1}} か {{shortcut_2}} を押して完了", "linearElementMulti": "",
"lockAngle": "{{shortcut}} を押し続けて角度を固定", "lockAngle": "",
"resize": "{{shortcut_1}} を押し続けて縦横比を維持、\n{{shortcut_2}} を押し続けて中心からリサイズ", "resize": "",
"resizeImage": "{{shortcut_1}} を押し続けて自由にリサイズ、\n{{shortcut_2}} を押し続けて中心からリサイズ", "resizeImage": "",
"rotate": "{{shortcut}} を押し続けて角度を固定して回転", "rotate": "",
"lineEditor_info": "{{shortcut_1}} を押しながらダブルクリック、または {{shortcut_2}} を押して点を編集", "lineEditor_info": "",
"lineEditor_line_info": "ダブルクリック、または {{shortcut}} を押して点を編集", "lineEditor_line_info": "",
"lineEditor_pointSelected": "{{shortcut_1}} で点を削除、\n{{shortcut_2}} で複製、またはドラッグで移動", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "点を選択して編集 ({{shortcut_1}} を押し続けて複数選択)、\nまたは {{shortcut_2}} を押しながらクリックで新しい点を追加", "lineEditor_nothingSelected": "",
"publishLibrary": "自分のライブラリを公開", "publishLibrary": "自分のライブラリを公開",
"bindTextToElement": "{{shortcut}} でテキストを追加", "bindTextToElement": "",
"createFlowchart": "{{shortcut}} でフローチャートを作成", "createFlowchart": "",
"deepBoxSelect": "{{shortcut}} を押し続けて詳細選択・ドラッグ防止", "deepBoxSelect": "",
"eraserRevert": "{{shortcut}} を押し続けて削除マークを取り消し", "eraserRevert": "",
"firefox_clipboard_write": "この機能は、\"dom.events.asyncClipboard.clipboardItem\" フラグを \"true\" に設定することで有効になる可能性があります。Firefox でブラウザーの設定を変更するには、\"about:config\" ページを参照してください。", "firefox_clipboard_write": "この機能は、\"dom.events.asyncClipboard.clipboardItem\" フラグを \"true\" に設定することで有効になる可能性があります。Firefox でブラウザーの設定を変更するには、\"about:config\" ページを参照してください。",
"disableSnapping": "{{shortcut}} を押し続けてスナップを無効化", "disableSnapping": "",
"enterCropEditor": "画像をダブルクリック、または {{shortcut}} を押して切り抜き", "enterCropEditor": "",
"leaveCropEditor": "画像の外をクリック、または {{shortcut_1}} か {{shortcut_2}} を押して切り抜きを終了" "leaveCropEditor": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "プレビューを表示できません", "cannotShowPreview": "プレビューを表示できません",
@@ -388,17 +373,17 @@
}, },
"errorSplash": { "errorSplash": {
"headingMain": "エラーが発生しました。もう一度やり直してください。 <button>ページを再読み込みする。</button>", "headingMain": "エラーが発生しました。もう一度やり直してください。 <button>ページを再読み込みする。</button>",
"clearCanvasMessage": "再読み込みがうまくいかない場合は、<button>キャンバスを消去</button>してみてください。", "clearCanvasMessage": "再読み込みがうまくいかない場合は、 <button>キャンバスを消去しています</button>",
"clearCanvasCaveat": " これにより作業が失われます ", "clearCanvasCaveat": " これにより作業が失われます ",
"trackedToSentry": "エラー (識別子: {{eventId}}) はシステムに記録されました。", "trackedToSentry": "識別子のエラー {{eventId}} が我々のシステムで追跡されました。",
"openIssueMessage": "エラーにシーン情報が含まれないよう配慮しています。シーンが非公開でなければ、<button>バグ報告</button>へのご協力をお願いします。以下の情報をコピーしてGitHub Issueに貼り付けてください。", "openIssueMessage": "エラーに関するシーン情報を含めないように非常に慎重に設定しました。もしあなたのシーンがプライベートでない場合は、私たちのフォローアップを検討してください。 <button>バグ報告</button> GitHub Issueに以下の情報をコピーして貼り付けてください。",
"sceneContent": "シーンの内容:" "sceneContent": "シーンの内容:"
}, },
"shareDialog": { "shareDialog": {
"or": "または" "or": "または"
}, },
"roomDialog": { "roomDialog": {
"desc_intro": "他の人を描画の共同編集に招待しましょう。", "desc_intro": "図面にコラボレーションするよう人々を招待しま。",
"desc_privacy": "心配しないでください、セッションはエンドツーエンドで暗号化されており、完全にプライベートです。私たちのサーバーでさえも、あなたが描いたものを見ることができません。", "desc_privacy": "心配しないでください、セッションはエンドツーエンドで暗号化されており、完全にプライベートです。私たちのサーバーでさえも、あなたが描いたものを見ることができません。",
"button_startSession": "セッションを開始する", "button_startSession": "セッションを開始する",
"button_stopSession": "セッションを終了する", "button_stopSession": "セッションを終了する",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "エラー" "title": "エラー"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "ディスクに保存", "disk_title": "ディスクに保存",
"disk_details": "シーンデータを後からインポートできるファイルにエクスポートします。", "disk_details": "シーンデータを後からインポートできるファイルにエクスポートします。",
@@ -430,8 +411,8 @@
"click": "クリック", "click": "クリック",
"deepSelect": "深い選択", "deepSelect": "深い選択",
"deepBoxSelect": "ボックス内の深い選択、およびドラッグの抑止", "deepBoxSelect": "ボックス内の深い選択、およびドラッグの抑止",
"createFlowchart": "汎用要素からフローチャートを作成", "createFlowchart": "",
"navigateFlowchart": "フローチャート内を移動", "navigateFlowchart": "",
"curvedArrow": "カーブした矢印", "curvedArrow": "カーブした矢印",
"curvedLine": "曲線", "curvedLine": "曲線",
"documentation": "ドキュメント", "documentation": "ドキュメント",
@@ -455,8 +436,8 @@
"toggleElementLock": "選択したアイテムをロック/ロック解除", "toggleElementLock": "選択したアイテムをロック/ロック解除",
"movePageUpDown": "ページを上下に移動", "movePageUpDown": "ページを上下に移動",
"movePageLeftRight": "ページを左右に移動", "movePageLeftRight": "ページを左右に移動",
"cropStart": "画像を切り抜き", "cropStart": "",
"cropFinish": "画像の切り抜きを終了" "cropFinish": ""
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "キャンバスを消去" "title": "キャンバスを消去"
@@ -482,10 +463,10 @@
"required": "必須項目", "required": "必須項目",
"website": "有効な URL を入力してください" "website": "有効な URL を入力してください"
}, },
"noteDescription": "あなたのライブラリを<link>公開ライブラリリポジトリ</link>に投稿して、他の人が作図に使えるようにしましょう。", "noteDescription": "以下に含めるライブラリを提出してください <link>公開ライブラリリポジトリ</link>他の人が作図に使えるようにするためです",
"noteGuidelines": "投稿されたライブラリは担当者が審査します。投稿前に<link>ガイドライン</link>をお読みください。修正をお願いすることがあるため、GitHubアカウントがあると便利ですが、必須ではありません。", "noteGuidelines": "最初にライブラリを手動で承認する必要があります。次をお読みください <link>ガイドライン</link> 送信する前に、GitHubアカウントが必要になりますが、必須ではありません。",
"noteLicense": "投稿すること、ライブラリが<link>MITライセンス</link>で公開されることに同意したものとみなします。これは誰でも自由に利用できることを意味します。", "noteLicense": "提出することにより、ライブラリが次の下で公開されることに同意します: <link>MIT ライセンス </link>つまり誰でも制限なく使えるということです",
"noteItems": "各アイテムにはフィルタリング用に固有の名前が必要です。以下のアイテムが含まれます:", "noteItems": "各ライブラリ項目は、フィルタリングのために独自の名前を持つ必要があります。以下のライブラリアイテムが含まれます:",
"atleastOneLibItem": "開始するには少なくとも1つのライブラリ項目を選択してください", "atleastOneLibItem": "開始するには少なくとも1つのライブラリ項目を選択してください",
"republishWarning": "注意: 選択された項目の中には、すでに公開/投稿済みと表示されているものがあります。既存のライブラリや投稿を更新する場合のみ、アイテムを再投稿してください。" "republishWarning": "注意: 選択された項目の中には、すでに公開/投稿済みと表示されているものがあります。既存のライブラリや投稿を更新する場合のみ、アイテムを再投稿してください。"
}, },
@@ -527,15 +508,15 @@
}, },
"stats": { "stats": {
"angle": "角度", "angle": "角度",
"shapes": "図形", "shapes": "",
"height": "高さ", "height": "高さ",
"scene": "シーン", "scene": "シーン",
"selected": "選択済み", "selected": "選択済み",
"storage": "ストレージ", "storage": "ストレージ",
"fullTitle": "キャンバス・図形のプロパティ", "fullTitle": "",
"title": "プロパティ", "title": "プロパティ",
"generalStats": "全般", "generalStats": "",
"elementProperties": "図形のプロパティ", "elementProperties": "",
"total": "合計", "total": "合計",
"version": "バージョン", "version": "バージョン",
"versionCopy": "クリックしてコピー", "versionCopy": "クリックしてコピー",
@@ -547,7 +528,7 @@
"copyStyles": "スタイルをコピーしました。", "copyStyles": "スタイルをコピーしました。",
"copyToClipboard": "クリップボードにコピー", "copyToClipboard": "クリップボードにコピー",
"copyToClipboardAsPng": "{{exportSelection}} を PNG 形式でクリップボードにコピーしました\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} を PNG 形式でクリップボードにコピーしました\n({{exportColorScheme}})",
"copyToClipboardAsSvg": "{{exportSelection}} を SVG 形式でクリップボードにコピーしました\n({{exportColorScheme}})", "copyToClipboardAsSvg": "",
"fileSaved": "ファイルを保存しました", "fileSaved": "ファイルを保存しました",
"fileSavedToFilename": "{filename} に保存しました", "fileSavedToFilename": "{filename} に保存しました",
"canvas": "キャンバス", "canvas": "キャンバス",
@@ -555,7 +536,7 @@
"pasteAsSingleElement": "{{shortcut}} を使用して単一の要素として貼り付けるか、\n既存のテキストエディタに貼り付け", "pasteAsSingleElement": "{{shortcut}} を使用して単一の要素として貼り付けるか、\n既存のテキストエディタに貼り付け",
"unableToEmbed": "この URL の埋め込みは現在許可されていません。URL のホワイトリストへの追加をリクエストするには、GitHub で Issue を上げてください。", "unableToEmbed": "この URL の埋め込みは現在許可されていません。URL のホワイトリストへの追加をリクエストするには、GitHub で Issue を上げてください。",
"unrecognizedLinkFormat": "埋め込もうとしたリンクは期待するフォーマットと一致しません。埋め込み元のサイトで提供される「embed」の文字列を貼り付けてください。", "unrecognizedLinkFormat": "埋め込もうとしたリンクは期待するフォーマットと一致しません。埋め込み元のサイトで提供される「embed」の文字列を貼り付けてください。",
"elementLinkCopied": "リンクをクリップボードにコピーしました" "elementLinkCopied": ""
}, },
"colors": { "colors": {
"transparent": "透明", "transparent": "透明",
@@ -576,9 +557,9 @@
}, },
"welcomeScreen": { "welcomeScreen": {
"app": { "app": {
"center_heading": "描画データはブラウザーのストレージに保存されます。", "center_heading": "",
"center_heading_line2": "ブラウザーのストレージは予期せず消去されることがあります。", "center_heading_line2": "",
"center_heading_line3": "作業を失わないよう、定期的にファイルへ保存してください。", "center_heading_line3": "",
"center_heading_plus": "代わりにExcalidraw+を開きますか?", "center_heading_plus": "代わりにExcalidraw+を開きますか?",
"menuHint": "エクスポート、設定、言語..." "menuHint": "エクスポート、設定、言語..."
}, },
@@ -590,7 +571,7 @@
} }
}, },
"colorPicker": { "colorPicker": {
"color": "", "color": "",
"mostUsedCustomColors": "最も使用されているカスタム色", "mostUsedCustomColors": "最も使用されているカスタム色",
"colors": "色", "colors": "色",
"shades": "影", "shades": "影",
@@ -619,77 +600,76 @@
"loadFromFile": { "loadFromFile": {
"title": "ファイルからロード", "title": "ファイルからロード",
"button": "ファイルからロード", "button": "ファイルからロード",
"description": "ファイルからの読み込みは、<bold>現在の描画内容を置き換えます</bold>。<br></br>その前に、以下の選択肢のいずれかにより描画内容を保存できます。" "description": "ファイルからのロードは、<bold>現在の描画内容を置き換えます</bold>。<br></br>その前に、以下の選択肢のいずれかにより描画内容を保存できます。"
}, },
"shareableLink": { "shareableLink": {
"title": "リンクから読み込み", "title": "リンクからロード",
"button": "描画内容を置き換える", "button": "描画内容を置き換える",
"description": "外部の描画データの読み込みは、<bold>現在の描画内容を置き換えます</bold>。<br></br>その前に、以下の選択肢のいずれかにより描画内容を保存できます。" "description": "外部図面のロードは、<bold>現在の描画内容を置き換えます</bold>。<br></br>その前に、以下の選択肢のいずれかにより描画内容を保存できます。"
} }
} }
}, },
"mermaid": { "mermaid": {
"title": "Mermaid を Excalidraw に変換", "title": "Mermaid を Excalidraw に変換",
"button": "挿入", "button": "挿入",
"description": "", "description": "現在、<flowchartLink>Flowchart</flowchartLink>、<sequenceLink>Sequence</sequenceLink>、<classLink>Class</classLink> のダイアグラムのみに対応しています。その他の種類は、Excalidraw では画像として描画されます。",
"syntax": "Mermaid 構文", "syntax": "Mermaid 構文",
"preview": "プレビュー", "preview": "プレビュー",
"label": "Mermaid", "label": "",
"inputPlaceholder": "Mermaid のダイアグラム定義をここに入力...", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "エラー!" "error": ""
}, },
"chat": { "chat": {
"inputPlaceholder": "ダイアグラムのアイデアを入力... ({{shortcut}} で改行)", "inputPlaceholder": "",
"inputPlaceholderWithMessages": "ダイアグラムを修正...", "inputPlaceholderWithMessages": "",
"generating": "生成中...", "generating": "",
"rateLimitRemaining": "本日の残りリクエスト回数: {{count}}", "rateLimitRemaining": "",
"role": { "role": {
"user": "あなた", "user": "",
"assistant": "AIアシスタント", "assistant": "",
"system": "システム" "system": ""
}, },
"aiBeta": "AI ベータ版", "aiBeta": "",
"label": "チャット", "label": "",
"menu": "メニュー", "menu": "",
"newChat": "新しいチャット", "newChat": "",
"deleteChat": "チャットを削除", "deleteChat": "",
"deleteMessage": "メッセージを削除", "deleteMessage": "",
"viewAsMermaid": "Mermaidで表示", "viewAsMermaid": "",
"placeholder": { "placeholder": {
"title": "ダイアグラムをデザインしよう", "title": "",
"description": "作りたいダイアグラムを説明してください。AIが生成します。", "description": "",
"hint": "" "hint": ""
}, },
"preview": "プレビュー", "preview": "",
"insert": "挿入", "insert": "",
"retry": "再試行", "retry": "",
"errors": { "errors": {
"promptTooShort": "プロンプトが短すぎます ({{min}} 文字以上)", "promptTooShort": "",
"promptTooLong": "プロンプトが長すぎます ({{max}} 文字以下)", "promptTooLong": "",
"generationFailed": "生成に失敗しました", "generationFailed": "",
"invalidDiagram": "無効なダイアグラムが生成されました。手動で編集、自動修正で再試行、または別のプロンプトをお試しください。", "invalidDiagram": "",
"fixInMermaid": "Mermaidで手動編集 →", "fixInMermaid": "",
"aiRepair": "再生成 (自動修正) →", "aiRepair": "",
"requestAborted": "リクエストが中断されました", "requestAborted": "",
"requestFailed": "リクエストに失敗しました", "requestFailed": "",
"mermaidParseError": "Mermaid構文エラー" "mermaidParseError": ""
}, },
"rateLimit": { "rateLimit": {
"messageLimit": "無料プランのAI利用上限に達しました。Excalidraw+をお試しいただくか、また明日ご利用ください。", "messageLimit": "",
"generalRateLimit": "操作が速すぎたようです。もう少し待ってから再度お試しください。", "generalRateLimit": "",
"messageLimitInputPlaceholder": "メッセージの送信上限に達しました" "messageLimitInputPlaceholder": ""
}, },
"upsellBtnLabel": "Plusにアップグレード" "upsellBtnLabel": ""
}, },
"quickSearch": { "quickSearch": {
"placeholder": "クイック検索" "placeholder": ""
}, },
"fontList": { "fontList": {
"badge": { "badge": {
"old": "" "old": ""
}, },
"sceneFonts": "このシーン内", "sceneFonts": "このシーン内",
"availableFonts": "利用可能フォント", "availableFonts": "利用可能フォント",
@@ -700,36 +680,36 @@
"hint": { "hint": {
"text": "ユーザーをクリックしてフォロー", "text": "ユーザーをクリックしてフォロー",
"followStatus": "現在このユーザーをフォローしています", "followStatus": "現在このユーザーをフォローしています",
"inCall": "ユーザーは音声通話中です", "inCall": "",
"micMuted": "ユーザーのマイクはミュート中です", "micMuted": "",
"isSpeaking": "ユーザーは発話中です" "isSpeaking": ""
} }
}, },
"commandPalette": { "commandPalette": {
"title": "コマンドパレット", "title": "",
"shortcuts": { "shortcuts": {
"select": "選択", "select": "選択",
"confirm": "確認", "confirm": "確認",
"close": "閉じる" "close": "閉じる"
}, },
"recents": "最近使ったもの", "recents": "",
"search": { "search": {
"placeholder": "メニュー、コマンドを検索して便利な機能を見つけよう", "placeholder": "",
"noMatch": "一致するコマンドはありません..." "noMatch": ""
}, },
"itemNotAvailable": "コマンドを利用できません...", "itemNotAvailable": "",
"shortcutHint": "コマンドパレットには{{shortcut}}を使用" "shortcutHint": "コマンドパレットには{{shortcut}}を使用"
}, },
"keys": { "keys": {
"ctrl": "Ctrl", "ctrl": "",
"option": "Option", "option": "",
"cmd": "Cmd", "cmd": "",
"alt": "Alt", "alt": "",
"escape": "Esc", "escape": "",
"enter": "Enter", "enter": "",
"shift": "Shift", "shift": "",
"spacebar": "スペース", "spacebar": "",
"delete": "Delete", "delete": "",
"mmb": "スクロールホイール" "mmb": ""
} }
} }
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Qoyıw", "paste": "Qoyıw",
"pasteAsPlaintext": "Ápiwayı tekst retinde qoyıw", "pasteAsPlaintext": "Ápiwayı tekst retinde qoyıw",
"pasteCharts": "Diagrammalardı qoyıw", "pasteCharts": "Diagrammalardı qoyıw",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Barlıǵın tańlaw", "selectAll": "Barlıǵın tańlaw",
"multiSelect": "", "multiSelect": "",
"moveCanvas": "", "moveCanvas": "",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Qátelik" "title": "Qátelik"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Diskke saqlaw", "disk_title": "Diskke saqlaw",
"disk_details": "", "disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Senṭeḍ", "paste": "Senṭeḍ",
"pasteAsPlaintext": "", "pasteAsPlaintext": "",
"pasteCharts": "Senṭeḍ udlifen", "pasteCharts": "Senṭeḍ udlifen",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Fren akk", "selectAll": "Fren akk",
"multiSelect": "Rnu aferdis ɣer tefrayt", "multiSelect": "Rnu aferdis ɣer tefrayt",
"moveCanvas": "Smutti taɣzut n usuneɣ", "moveCanvas": "Smutti taɣzut n usuneɣ",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Tuccḍa" "title": "Tuccḍa"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Sekles deg uḍebsi", "disk_title": "Sekles deg uḍebsi",
"disk_details": "Sekles isefka n usayes deg ufaylu ansi ara tizmireḍ ad d-tketreḍ areḍqal.", "disk_details": "Sekles isefka n usayes deg ufaylu ansi ara tizmireḍ ad d-tketreḍ areḍqal.",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Қою", "paste": "Қою",
"pasteAsPlaintext": "", "pasteAsPlaintext": "",
"pasteCharts": "Диаграммаларды қою", "pasteCharts": "Диаграммаларды қою",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Бәрін таңдау", "selectAll": "Бәрін таңдау",
"multiSelect": "", "multiSelect": "",
"moveCanvas": "", "moveCanvas": "",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Қате" "title": "Қате"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "", "disk_title": "",
"disk_details": "Сахна деректерін кейін қайта импорттауға болатын файлға экспорттаңыз.", "disk_details": "Сахна деректерін кейін қайта импорттауға болатын файлға экспорттаңыз.",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "បិទភ្ជាប់", "paste": "បិទភ្ជាប់",
"pasteAsPlaintext": "បិទភ្ជាប់ជាអត្ថបទធម្មតា", "pasteAsPlaintext": "បិទភ្ជាប់ជាអត្ថបទធម្មតា",
"pasteCharts": "បិទភ្ជាប់តារាង", "pasteCharts": "បិទភ្ជាប់តារាង",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "ជ្រើសរើស​ទាំងអស់", "selectAll": "ជ្រើសរើស​ទាំងអស់",
"multiSelect": "បន្ថែមធាតុទៅលើការជ្រើសរើស", "multiSelect": "បន្ថែមធាតុទៅលើការជ្រើសរើស",
"moveCanvas": "ផ្លាស់ទីបាវ", "moveCanvas": "ផ្លាស់ទីបាវ",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "មានកំហុស" "title": "មានកំហុស"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "រក្សាទុកទៅថាស", "disk_title": "រក្សាទុកទៅថាស",
"disk_details": "នាំចេញទិន្នន័យរបស់ស៊ីនជាឯកសារដែលអ្នកអាចនាំចូលនៅពេលក្រោយ។", "disk_details": "នាំចេញទិន្នន័យរបស់ស៊ីនជាឯកសារដែលអ្នកអាចនាំចូលនៅពេលក្រោយ។",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "붙여넣기", "paste": "붙여넣기",
"pasteAsPlaintext": "일반 텍스트로 붙여넣기", "pasteAsPlaintext": "일반 텍스트로 붙여넣기",
"pasteCharts": "차트 붙여넣기", "pasteCharts": "차트 붙여넣기",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "전체 선택", "selectAll": "전체 선택",
"multiSelect": "선택 영역에 추가하기", "multiSelect": "선택 영역에 추가하기",
"moveCanvas": "캔버스 이동", "moveCanvas": "캔버스 이동",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "화살표 모양", "arrowtypes": "화살표 모양",
"arrowtype_sharp": "뾰족한 화살표", "arrowtype_sharp": "뾰족한 화살표",
"arrowtype_round": "곡선 화살표", "arrowtype_round": "곡선 화살표",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "오류" "title": "오류"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "디스크에 저장", "disk_title": "디스크에 저장",
"disk_details": "나중에 다시 불러올 수 있도록 화면 데이터를 내보냅니다.", "disk_details": "나중에 다시 불러올 수 있도록 화면 데이터를 내보냅니다.",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "دانانەوە", "paste": "دانانەوە",
"pasteAsPlaintext": "دایبنێ وەک دەقی سادە", "pasteAsPlaintext": "دایبنێ وەک دەقی سادە",
"pasteCharts": "دانانەوەی خشتەکان", "pasteCharts": "دانانەوەی خشتەکان",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "دیاریکردنی هەموو", "selectAll": "دیاریکردنی هەموو",
"multiSelect": "زیادکردنی بۆ دیاریکراوەکان", "multiSelect": "زیادکردنی بۆ دیاریکراوەکان",
"moveCanvas": "تابلۆ بجوڵێنە", "moveCanvas": "تابلۆ بجوڵێنە",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "هه‌ڵه‌ ڕوویدا" "title": "هه‌ڵه‌ ڕوویدا"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "پاشەکەوت بکە لە دیسک", "disk_title": "پاشەکەوت بکە لە دیسک",
"disk_details": "هەناردەکردنی داتای دیمەنەکە بۆ فایلێک کە دواتر دەتوانیت لێی هاوردە بکەیت.", "disk_details": "هەناردەکردنی داتای دیمەنەکە بۆ فایلێک کە دواتر دەتوانیت لێی هاوردە بکەیت.",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Įklijuoti", "paste": "Įklijuoti",
"pasteAsPlaintext": "Įklijuoti kaip paprastą tekstą", "pasteAsPlaintext": "Įklijuoti kaip paprastą tekstą",
"pasteCharts": "Įklijuoti diagramas", "pasteCharts": "Įklijuoti diagramas",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Pažymėti viską", "selectAll": "Pažymėti viską",
"multiSelect": "Pridėkite elementą prie pasirinktų", "multiSelect": "Pridėkite elementą prie pasirinktų",
"moveCanvas": "Judinti drobę", "moveCanvas": "Judinti drobę",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Klaida" "title": "Klaida"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Įrašyti į diską", "disk_title": "Įrašyti į diską",
"disk_details": "", "disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Ielīmēt", "paste": "Ielīmēt",
"pasteAsPlaintext": "Ielīmēt kā vienkāršu tekstu", "pasteAsPlaintext": "Ielīmēt kā vienkāršu tekstu",
"pasteCharts": "Ielīmēt grafikus", "pasteCharts": "Ielīmēt grafikus",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Atlasīt visu", "selectAll": "Atlasīt visu",
"multiSelect": "Pievienot elementu atlasei", "multiSelect": "Pievienot elementu atlasei",
"moveCanvas": "Pārvietot tāfeli", "moveCanvas": "Pārvietot tāfeli",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Kļūda" "title": "Kļūda"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Saglabāt diskā", "disk_title": "Saglabāt diskā",
"disk_details": "Eksportēt ainas datus datnē, ko vēlāk varēsiet importēt.", "disk_details": "Eksportēt ainas datus datnē, ko vēlāk varēsiet importēt.",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "चिटकवा", "paste": "चिटकवा",
"pasteAsPlaintext": "साधा मजकूर च्या रुपात पेस्ट करा", "pasteAsPlaintext": "साधा मजकूर च्या रुपात पेस्ट करा",
"pasteCharts": "चार्ट चिकटवा", "pasteCharts": "चार्ट चिकटवा",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "समस्त निवडा", "selectAll": "समस्त निवडा",
"multiSelect": "निवडित तत्व जोडा", "multiSelect": "निवडित तत्व जोडा",
"moveCanvas": "पटल हलवा", "moveCanvas": "पटल हलवा",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "चिमणि पाय (अनेक)", "arrowhead_crowfoot_many": "चिमणि पाय (अनेक)",
"arrowhead_crowfoot_one": "चिमणि पाय (एक)", "arrowhead_crowfoot_one": "चिमणि पाय (एक)",
"arrowhead_crowfoot_one_or_many": "चिमणि पाय (एक अथवा अनेक)", "arrowhead_crowfoot_one_or_many": "चिमणि पाय (एक अथवा अनेक)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "आणिक विकल्प", "more_options": "आणिक विकल्प",
"cardinality": "",
"arrowtypes": "बाणाचे प्रकार", "arrowtypes": "बाणाचे प्रकार",
"arrowtype_sharp": "तीक्ष्ण तीर", "arrowtype_sharp": "तीक्ष्ण तीर",
"arrowtype_round": "वक्राकार तीर", "arrowtype_round": "वक्राकार तीर",
@@ -182,11 +171,7 @@
"linkToElement": "वस्तू ह्याचा दुवा", "linkToElement": "वस्तू ह्याचा दुवा",
"wrapSelectionInFrame": "चौकटित निवडलेले गुंडाळा", "wrapSelectionInFrame": "चौकटित निवडलेले गुंडाळा",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "वस्तू ह्याचा दुवा", "title": "वस्तू ह्याचा दुवा",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "त्रुटि" "title": "त्रुटि"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "डिस्क मधे जतन करा", "disk_title": "डिस्क मधे जतन करा",
"disk_details": "सीन डेटा बाहेर एक फ़ाइल मधे जतन करा, त्या फ़ाइल मधुम तो डेटा नंतर परत आणु शकता.", "disk_details": "सीन डेटा बाहेर एक फ़ाइल मधे जतन करा, त्या फ़ाइल मधुम तो डेटा नंतर परत आणु शकता.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "मर्मेड पासून एक्सकाली मधे", "title": "मर्मेड पासून एक्सकाली मधे",
"button": "शिरवा", "button": "शिरवा",
"description": "", "description": "सध्या फक्त <flowchartLink> प्रवाह चित्र (फ़्लो चार्ट) </flowchartLink> आणि <sequenceLink> क्रम चित्र (सिकवेंस ड़ायग्राम) </sequenceLink> करता येतात. बाक़ीचे चित्र प्रकार एक्सकाली चित्र पद्धति नी चित्रित होतील.",
"syntax": "मर्मेड संरचना नियम", "syntax": "मर्मेड संरचना नियम",
"preview": "पूर्वावलोकन", "preview": "पूर्वावलोकन",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "ထား", "paste": "ထား",
"pasteAsPlaintext": "", "pasteAsPlaintext": "",
"pasteCharts": "", "pasteCharts": "",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "အကုန်ရွေး", "selectAll": "အကုန်ရွေး",
"multiSelect": "ရွေးထားသည့်ထဲပုံထည့်", "multiSelect": "ရွေးထားသည့်ထဲပုံထည့်",
"moveCanvas": "ကားချပ်ရွှေ့", "moveCanvas": "ကားချပ်ရွှေ့",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "ချို့ယွင်းချက်" "title": "ချို့ယွင်းချက်"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "", "disk_title": "",
"disk_details": "", "disk_details": "",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Lim inn", "paste": "Lim inn",
"pasteAsPlaintext": "Lim inn som klartekst", "pasteAsPlaintext": "Lim inn som klartekst",
"pasteCharts": "Lim inn diagrammer", "pasteCharts": "Lim inn diagrammer",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Velg alt", "selectAll": "Velg alt",
"multiSelect": "Legg til element i utvalg", "multiSelect": "Legg til element i utvalg",
"moveCanvas": "Flytt lerretet", "moveCanvas": "Flytt lerretet",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Kråkefot (mange)", "arrowhead_crowfoot_many": "Kråkefot (mange)",
"arrowhead_crowfoot_one": "Kråkefot (én)", "arrowhead_crowfoot_one": "Kråkefot (én)",
"arrowhead_crowfoot_one_or_many": "Kråkefot (én eller mange)", "arrowhead_crowfoot_one_or_many": "Kråkefot (én eller mange)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Flere alternativer", "more_options": "Flere alternativer",
"cardinality": "",
"arrowtypes": "Type pil", "arrowtypes": "Type pil",
"arrowtype_sharp": "Skarp pil", "arrowtype_sharp": "Skarp pil",
"arrowtype_round": "Buet pil", "arrowtype_round": "Buet pil",
@@ -182,11 +171,7 @@
"linkToElement": "Lenke til objekt", "linkToElement": "Lenke til objekt",
"wrapSelectionInFrame": "Brekk om utvalg i ramme", "wrapSelectionInFrame": "Brekk om utvalg i ramme",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "Lenke til objekt", "title": "Lenke til objekt",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Feil" "title": "Feil"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Lagre til disk", "disk_title": "Lagre til disk",
"disk_details": "Eksporter scene-dataene til en fil som du kan importere fra senere.", "disk_details": "Eksporter scene-dataene til en fil som du kan importere fra senere.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Mermaid til Excalidraw", "title": "Mermaid til Excalidraw",
"button": "Sett inn", "button": "Sett inn",
"description": "", "description": "Foreløpig er bare <flowchartLink>Flowchart</flowchartLink>-,<sequenceLink> Sequence</sequenceLink>- og <classLink>klasse </classLink>-diagrammer støttet. De andre typene vil bli gjengitt som bilde i Excalidraw.",
"syntax": "Mermaid-syntaks", "syntax": "Mermaid-syntaks",
"preview": "Forhåndsvisning", "preview": "Forhåndsvisning",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+9 -29
View File
@@ -3,10 +3,6 @@
"paste": "Plakken", "paste": "Plakken",
"pasteAsPlaintext": "Plakken als platte tekst", "pasteAsPlaintext": "Plakken als platte tekst",
"pasteCharts": "Grafieken plakken", "pasteCharts": "Grafieken plakken",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Alles selecteren", "selectAll": "Alles selecteren",
"multiSelect": "Voeg element toe aan selectie", "multiSelect": "Voeg element toe aan selectie",
"moveCanvas": "Canvas verplaatsen", "moveCanvas": "Canvas verplaatsen",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "Kraaienpoot (veel)", "arrowhead_crowfoot_many": "Kraaienpoot (veel)",
"arrowhead_crowfoot_one": "Kraaienpoot (enkel)", "arrowhead_crowfoot_one": "Kraaienpoot (enkel)",
"arrowhead_crowfoot_one_or_many": "Kraaienpoot (enkel of veel)", "arrowhead_crowfoot_one_or_many": "Kraaienpoot (enkel of veel)",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "Meer opties", "more_options": "Meer opties",
"cardinality": "",
"arrowtypes": "Pijl type", "arrowtypes": "Pijl type",
"arrowtype_sharp": "Scherpe pijl", "arrowtype_sharp": "Scherpe pijl",
"arrowtype_round": "Gebogen pijl", "arrowtype_round": "Gebogen pijl",
@@ -182,11 +171,7 @@
"linkToElement": "Link naar object", "linkToElement": "Link naar object",
"wrapSelectionInFrame": "Selectie omslaan in frame", "wrapSelectionInFrame": "Selectie omslaan in frame",
"tab": "Tab", "tab": "Tab",
"shapeSwitch": "Verander vorm", "shapeSwitch": "Verander vorm"
"preferences": "Voorkeuren",
"preferences_toolLock": "Tool vergrendelen",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "Link naar object", "title": "Link naar object",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Fout" "title": "Fout"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Opslaan op schijf", "disk_title": "Opslaan op schijf",
"disk_details": "De scènegegevens exporteren naar een bestand waaruit u later kunt importeren.", "disk_details": "De scènegegevens exporteren naar een bestand waaruit u later kunt importeren.",
@@ -576,9 +557,9 @@
}, },
"welcomeScreen": { "welcomeScreen": {
"app": { "app": {
"center_heading": "Je tekeningen worden bewaard in de opslag van je browser.", "center_heading": "",
"center_heading_line2": "Browser opslag kan onverwacht gewist worden.", "center_heading_line2": "",
"center_heading_line3": "Sla je werk regelmatig op in een bestand om te voorkomen dat je het kwijtraakt.", "center_heading_line3": "",
"center_heading_plus": "Wil je in plaats daarvan naar Excalidraw+ gaan?", "center_heading_plus": "Wil je in plaats daarvan naar Excalidraw+ gaan?",
"menuHint": "Exporteren, voorkeuren en meer, ..." "menuHint": "Exporteren, voorkeuren en meer, ..."
}, },
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "Mermaid naar Excalidraw", "title": "Mermaid naar Excalidraw",
"button": "Invoegen", "button": "Invoegen",
"description": "", "description": "Momenteel worden alleen <flowchartLink>Flowchart</flowchartLink>-, <sequenceLink>Sequence</sequenceLink>- en <classLink>Class</classLink>-diagrammen ondersteund. De andere types worden als afbeelding weergegeven in Excalidraw.",
"syntax": "Mermaid Syntaxis", "syntax": "Mermaid Syntaxis",
"preview": "Voorbeeld", "preview": "Voorbeeld",
"label": "Mermaid", "label": "Mermaid",
"inputPlaceholder": "Noteer Mermaid diagram omschrijving hier...", "inputPlaceholder": "Noteer Mermaid diagram omschrijving hier..."
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "Fout!" "error": "Fout!"
@@ -644,7 +624,7 @@
"chat": { "chat": {
"inputPlaceholder": "Begin je diagram idee hier in te typen... ({{shortcut}} voor een nieuwe lijn)", "inputPlaceholder": "Begin je diagram idee hier in te typen... ({{shortcut}} voor een nieuwe lijn)",
"inputPlaceholderWithMessages": "Ga door met het verfijnen van je diagram...", "inputPlaceholderWithMessages": "Ga door met het verfijnen van je diagram...",
"generating": "Genereren...", "generating": "",
"rateLimitRemaining": "{{count}} verzoeken over vandaag", "rateLimitRemaining": "{{count}} verzoeken over vandaag",
"role": { "role": {
"user": "Jij", "user": "Jij",
@@ -661,7 +641,7 @@
"placeholder": { "placeholder": {
"title": "Laten we jouw diagram ontwerpen", "title": "Laten we jouw diagram ontwerpen",
"description": "Beschrijf het diagram dat je wilt aanmaken, en we genereren het voor je.", "description": "Beschrijf het diagram dat je wilt aanmaken, en we genereren het voor je.",
"hint": "" "hint": "Op dit moment kennen we Flowchart, Sequence, en Class diagrammen."
}, },
"preview": "Voorbeeld", "preview": "Voorbeeld",
"insert": "Invoegen", "insert": "Invoegen",
@@ -675,7 +655,7 @@
"aiRepair": "Opnieuw genereren (automatisch repareren) →", "aiRepair": "Opnieuw genereren (automatisch repareren) →",
"requestAborted": "Verzoek gestopt", "requestAborted": "Verzoek gestopt",
"requestFailed": "Verzoek mislukt", "requestFailed": "Verzoek mislukt",
"mermaidParseError": "Mermaid syntax fout" "mermaidParseError": ""
}, },
"rateLimit": { "rateLimit": {
"messageLimit": "Je hebt je AI limiet bereikt op het gratis abonnement. Probeer Excalidraw+ uit voor meer of kom morgen terug.", "messageLimit": "Je hebt je AI limiet bereikt op het gratis abonnement. Probeer Excalidraw+ uit voor meer of kom morgen terug.",
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "Lim inn", "paste": "Lim inn",
"pasteAsPlaintext": "", "pasteAsPlaintext": "",
"pasteCharts": "Lim inn diagram", "pasteCharts": "Lim inn diagram",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Vel alt", "selectAll": "Vel alt",
"multiSelect": "Legg til element i utval", "multiSelect": "Legg til element i utval",
"moveCanvas": "Flytt lerretet", "moveCanvas": "Flytt lerretet",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Feil" "title": "Feil"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Lagre til disk", "disk_title": "Lagre til disk",
"disk_details": "Eksporter scenedataa til ei fil du kan importere seinare.", "disk_details": "Eksporter scenedataa til ei fil du kan importere seinare.",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+3 -23
View File
@@ -3,10 +3,6 @@
"paste": "Pegar", "paste": "Pegar",
"pasteAsPlaintext": "Pegar en tèxt brut", "pasteAsPlaintext": "Pegar en tèxt brut",
"pasteCharts": "Pegar los grafics", "pasteCharts": "Pegar los grafics",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "Tot seleccionar", "selectAll": "Tot seleccionar",
"multiSelect": "Apondre un element a la seleccion", "multiSelect": "Apondre un element a la seleccion",
"moveCanvas": "Desplaçar lo canabàs", "moveCanvas": "Desplaçar lo canabàs",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "Ligam cap a lobjècte", "linkToElement": "Ligam cap a lobjècte",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "Onglet", "tab": "Onglet",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "Error" "title": "Error"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "Salvar al disc", "disk_title": "Salvar al disc",
"disk_details": "Exportar las donadas de la scèna cap a un fichièr que podètz importar mai tard.", "disk_details": "Exportar las donadas de la scèna cap a un fichièr que podètz importar mai tard.",
@@ -631,12 +612,11 @@
"mermaid": { "mermaid": {
"title": "De Mermaid cap a Excalidraw", "title": "De Mermaid cap a Excalidraw",
"button": "Inserir", "button": "Inserir",
"description": "", "description": "Actualament, sonque los diagramas <flowchartLink>logics</flowchartLink>,<sequenceLink> de sequéncia</sequenceLink> e <classLink>de classa </classLink>son preses en carga. Los autres tipes seràn afichats coma imatge dins Excalidraw.",
"syntax": "Sintaxi Mermaid", "syntax": "Sintaxi Mermaid",
"preview": "Apercebut", "preview": "Apercebut",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+2 -22
View File
@@ -3,10 +3,6 @@
"paste": "ਪੇਸਟ ਕਰੋ", "paste": "ਪੇਸਟ ਕਰੋ",
"pasteAsPlaintext": "ਸਾਦੇ ਪਾਠ ਵਜੋਂ ਪੇਸਟ ਕਰੋ", "pasteAsPlaintext": "ਸਾਦੇ ਪਾਠ ਵਜੋਂ ਪੇਸਟ ਕਰੋ",
"pasteCharts": "ਚਾਰਟ ਪੇਸਟ ਕਰੋ", "pasteCharts": "ਚਾਰਟ ਪੇਸਟ ਕਰੋ",
"chartType_bar": "",
"chartType_line": "",
"chartType_radar": "",
"chartType_plaintext": "",
"selectAll": "ਸਾਰੇ ਚੁਣੋ", "selectAll": "ਸਾਰੇ ਚੁਣੋ",
"multiSelect": "ਐਲੀਮੈਂਟ ਨੂੰ ਚੋਣ ਵਿੱਚ ਜੋੜੋ", "multiSelect": "ਐਲੀਮੈਂਟ ਨੂੰ ਚੋਣ ਵਿੱਚ ਜੋੜੋ",
"moveCanvas": "ਕੈਨਵਸ ਹਿਲਾਓ", "moveCanvas": "ਕੈਨਵਸ ਹਿਲਾਓ",
@@ -53,14 +49,7 @@
"arrowhead_crowfoot_many": "", "arrowhead_crowfoot_many": "",
"arrowhead_crowfoot_one": "", "arrowhead_crowfoot_one": "",
"arrowhead_crowfoot_one_or_many": "", "arrowhead_crowfoot_one_or_many": "",
"arrowhead_cardinality_one": "",
"arrowhead_cardinality_many": "",
"arrowhead_cardinality_one_or_many": "",
"arrowhead_cardinality_exactly_one": "",
"arrowhead_cardinality_zero_or_one": "",
"arrowhead_cardinality_zero_or_many": "",
"more_options": "", "more_options": "",
"cardinality": "",
"arrowtypes": "", "arrowtypes": "",
"arrowtype_sharp": "", "arrowtype_sharp": "",
"arrowtype_round": "", "arrowtype_round": "",
@@ -182,11 +171,7 @@
"linkToElement": "", "linkToElement": "",
"wrapSelectionInFrame": "", "wrapSelectionInFrame": "",
"tab": "", "tab": "",
"shapeSwitch": "", "shapeSwitch": ""
"preferences": "",
"preferences_toolLock": "",
"arrowBinding": "",
"midpointSnapping": ""
}, },
"elementLink": { "elementLink": {
"title": "", "title": "",
@@ -410,10 +395,6 @@
"errorDialog": { "errorDialog": {
"title": "ਗਲਤੀ" "title": "ਗਲਤੀ"
}, },
"progressDialog": {
"title": "",
"defaultMessage": ""
},
"exportDialog": { "exportDialog": {
"disk_title": "ਡਿਸਕ ਵਿੱਚ ਸਾਂਭੋ", "disk_title": "ਡਿਸਕ ਵਿੱਚ ਸਾਂਭੋ",
"disk_details": "ਦ੍ਰਿਸ਼ ਦਾ ਡਾਟਾ ਫਾਈਲ ਵਿੱਚ ਨਿਰਯਾਤ ਕਰੋ ਜਿੱਥੋਂ ਤੁਸੀਂ ਇਸਨੂੰ ਬਾਅਦ ਵਿੱਚ ਆਯਾਤ ਕਰ ਸਕਦੇ ਹੋ।", "disk_details": "ਦ੍ਰਿਸ਼ ਦਾ ਡਾਟਾ ਫਾਈਲ ਵਿੱਚ ਨਿਰਯਾਤ ਕਰੋ ਜਿੱਥੋਂ ਤੁਸੀਂ ਇਸਨੂੰ ਬਾਅਦ ਵਿੱਚ ਆਯਾਤ ਕਰ ਸਕਦੇ ਹੋ।",
@@ -635,8 +616,7 @@
"syntax": "", "syntax": "",
"preview": "", "preview": "",
"label": "", "label": "",
"inputPlaceholder": "", "inputPlaceholder": ""
"autoFixAvailable": ""
}, },
"ttd": { "ttd": {
"error": "" "error": ""
+52 -52
View File
@@ -1,59 +1,59 @@
{ {
"ar-SA": 82, "ar-SA": 84,
"az-AZ": 24, "az-AZ": 25,
"bg-BG": 63, "bg-BG": 65,
"bn-BD": 38, "bn-BD": 39,
"bn-IN": 38, "bn-IN": 39,
"ca-ES": 82, "ca-ES": 84,
"cs-CZ": 74, "cs-CZ": 76,
"da-DK": 28, "da-DK": 28,
"de-CH": 83, "de-CH": 85,
"de-DE": 83, "de-DE": 85,
"el-GR": 77, "el-GR": 80,
"en": 100, "en": 100,
"es-ES": 82, "es-ES": 85,
"eu-ES": 70, "eu-ES": 72,
"fa-IR": 83, "fa-IR": 84,
"fi-FI": 60, "fi-FI": 62,
"fr-FR": 97, "fr-FR": 92,
"gl-ES": 61, "gl-ES": 63,
"he-IL": 82, "he-IL": 84,
"hi-IN": 83, "hi-IN": 86,
"hu-HU": 65, "hu-HU": 67,
"id-ID": 81, "id-ID": 84,
"it-IT": 98, "it-IT": 99,
"ja-JP": 96, "ja-JP": 81,
"kaa": 21, "kaa": 22,
"kab-KAB": 54, "kab-KAB": 56,
"kk-KZ": 13, "kk-KZ": 13,
"km-KH": 54, "km-KH": 56,
"ko-KR": 73, "ko-KR": 75,
"ku-TR": 58, "ku-TR": 59,
"lt-LT": 33, "lt-LT": 35,
"lv-LV": 51, "lv-LV": 53,
"mr-IN": 82, "mr-IN": 84,
"my-MM": 24, "my-MM": 25,
"nb-NO": 82, "nb-NO": 84,
"nl-NL": 96, "nl-NL": 99,
"nn-NO": 43, "nn-NO": 45,
"oc-FR": 76, "oc-FR": 79,
"pa-IN": 52, "pa-IN": 54,
"pl-PL": 90, "pl-PL": 93,
"pt-BR": 86, "pt-BR": 84,
"pt-PT": 82, "pt-PT": 84,
"ro-RO": 98, "ro-RO": 100,
"ru-RU": 98, "ru-RU": 99,
"si-LK": 67, "si-LK": 70,
"sk-SK": 90, "sk-SK": 93,
"sl-SI": 82, "sl-SI": 84,
"sv-SE": 82, "sv-SE": 85,
"ta-IN": 78, "ta-IN": 81,
"th-TH": 65, "th-TH": 59,
"tr-TR": 92, "tr-TR": 88,
"uk-UA": 86, "uk-UA": 84,
"uz-UZ": 0, "uz-UZ": 0,
"vi-VN": 68, "vi-VN": 70,
"zh-CN": 91, "zh-CN": 93,
"zh-HK": 16, "zh-HK": 16,
"zh-TW": 98 "zh-TW": 84
} }

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