From ae7ff761268989131d4e5ff45759d838418dde59 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 6 Jun 2023 14:36:18 +0530 Subject: [PATCH 01/27] fix: cleanup textWysiwyg and getAdjustedDimensions (#6520) * fix: cleanup textWysiwyg and getAdjustedDimensions * fix lint * fix test --- src/element/newElement.ts | 28 +--------------- src/element/textWysiwyg.test.tsx | 48 +++++++++++++++++---------- src/element/textWysiwyg.tsx | 57 +++++--------------------------- 3 files changed, 41 insertions(+), 92 deletions(-) diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 4922a5b4ee..7a19f6b37e 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -20,15 +20,13 @@ import { isTestEnv, } from "../utils"; import { randomInteger, randomId } from "../random"; -import { bumpVersion, mutateElement, newElementWith } from "./mutateElement"; +import { bumpVersion, newElementWith } from "./mutateElement"; import { getNewGroupIdsForDuplication } from "../groups"; import { AppState } from "../types"; import { getElementAbsoluteCoords } from "."; import { adjustXYWithRotation } from "../math"; import { getResizedElementAbsoluteCoords } from "./bounds"; import { - getBoundTextElementOffset, - getContainerDims, getContainerElement, measureText, normalizeText, @@ -44,7 +42,6 @@ import { DEFAULT_VERTICAL_ALIGN, VERTICAL_ALIGN, } from "../constants"; -import { isArrowElement } from "./typeChecks"; import { MarkOptional, Merge, Mutable } from "../utility-types"; type ElementConstructorOpts = MarkOptional< @@ -211,8 +208,6 @@ const getAdjustedDimensions = ( height: number; baseline: number; } => { - const container = getContainerElement(element); - const { width: nextWidth, height: nextHeight, @@ -268,27 +263,6 @@ const getAdjustedDimensions = ( ); } - // make sure container dimensions are set properly when - // text editor overflows beyond viewport dimensions - if (container) { - const boundTextElementPadding = getBoundTextElementOffset(element); - - const containerDims = getContainerDims(container); - let height = containerDims.height; - let width = containerDims.width; - if (nextHeight > height - boundTextElementPadding * 2) { - height = nextHeight + boundTextElementPadding * 2; - } - if (nextWidth > width - boundTextElementPadding * 2) { - width = nextWidth + boundTextElementPadding * 2; - } - if ( - !isArrowElement(container) && - (height !== containerDims.height || width !== containerDims.width) - ) { - mutateElement(container, { height, width }); - } - } return { width: nextWidth, height: nextHeight, diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index 294c132636..0a5bbf3202 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -1213,32 +1213,46 @@ describe("textWysiwyg", () => { }); it("should restore original container height and clear cache once text is unbind", async () => { - const originalRectHeight = rectangle.height; - expect(rectangle.height).toBe(originalRectHeight); - - Keyboard.keyPress(KEYS.ENTER); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; - await new Promise((r) => setTimeout(r, 0)); - - fireEvent.change(editor, { - target: { value: "Online whiteboard collaboration made easy" }, + const container = API.createElement({ + type: "rectangle", + height: 75, + width: 90, }); - editor.blur(); - expect(rectangle.height).toBe(185); - mouse.select(rectangle); + const originalRectHeight = container.height; + expect(container.height).toBe(originalRectHeight); + + const text = API.createElement({ + type: "text", + text: "Online whiteboard collaboration made easy", + }); + h.elements = [container, text]; + API.setSelectedElements([container, text]); + fireEvent.contextMenu(GlobalTestState.canvas, { button: 2, clientX: 20, clientY: 30, }); - const contextMenu = document.querySelector(".context-menu"); + let contextMenu = document.querySelector(".context-menu"); + + fireEvent.click( + queryByText(contextMenu as HTMLElement, "Bind text to the container")!, + ); + + expect((h.elements[1] as ExcalidrawTextElementWithContainer).text).toBe( + "Online \nwhitebo\nard \ncollabo\nration \nmade \neasy", + ); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 20, + clientY: 30, + }); + contextMenu = document.querySelector(".context-menu"); fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!); expect(h.elements[0].boundElements).toEqual([]); - expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null); + expect(getOriginalContainerHeightFromCache(container.id)).toBe(null); - expect(rectangle.height).toBe(originalRectHeight); + expect(container.height).toBe(originalRectHeight); }); it("should reset the container height cache when resizing", async () => { diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index 5b94ab3e8a..9105ba700a 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -11,7 +11,7 @@ import { isBoundToContainer, isTextElement, } from "./typeChecks"; -import { CLASSES, isSafari, VERTICAL_ALIGN } from "../constants"; +import { CLASSES, isSafari } from "../constants"; import { ExcalidrawElement, ExcalidrawLinearElement, @@ -23,12 +23,10 @@ import { AppState } from "../types"; import { mutateElement } from "./mutateElement"; import { getBoundTextElementId, - getContainerCoords, getContainerDims, getContainerElement, getTextElementAngle, getTextWidth, - measureText, normalizeText, redrawTextBoundingBox, wrapText, @@ -36,6 +34,7 @@ import { getBoundTextMaxWidth, computeContainerDimensionForBoundText, detectLineHeight, + computeBoundTextPosition, } from "./textElement"; import { actionDecreaseFontSize, @@ -162,7 +161,7 @@ export const textWysiwyg = ({ let textElementWidth = updatedTextElement.width; // Set to element height by default since that's // what is going to be used for unbounded text - let textElementHeight = updatedTextElement.height; + const textElementHeight = updatedTextElement.height; if (container && updatedTextElement.containerId) { if (isArrowElement(container)) { @@ -179,15 +178,6 @@ export const textWysiwyg = ({ editable, ); const containerDims = getContainerDims(container); - // using editor.style.height to get the accurate height of text editor - const editorHeight = Number(editable.style.height.slice(0, -2)); - if (editorHeight > 0) { - textElementHeight = editorHeight; - } - if (propertiesUpdated) { - // update height of the editor after properties updated - textElementHeight = updatedTextElement.height; - } let originalContainerData; if (propertiesUpdated) { @@ -232,22 +222,12 @@ export const textWysiwyg = ({ container.type, ); mutateElement(container, { height: targetContainerHeight }); - } - // Start pushing text upward until a diff of 30px (padding) - // is reached - else { - const containerCoords = getContainerCoords(container); - - // vertically center align the text - if (verticalAlign === VERTICAL_ALIGN.MIDDLE) { - if (!isArrowElement(container)) { - coordY = - containerCoords.y + maxHeight / 2 - textElementHeight / 2; - } - } - if (verticalAlign === VERTICAL_ALIGN.BOTTOM) { - coordY = containerCoords.y + (maxHeight - textElementHeight); - } + } else { + const { y } = computeBoundTextPosition( + container, + updatedTextElement as ExcalidrawTextElementWithContainer, + ); + coordY = y; } } const [viewportX, viewportY] = getViewportCoords(coordX, coordY); @@ -388,25 +368,6 @@ export const textWysiwyg = ({ }; editable.oninput = () => { - const updatedTextElement = Scene.getScene(element)?.getElement( - id, - ) as ExcalidrawTextElement; - const font = getFontString(updatedTextElement); - if (isBoundToContainer(element)) { - const container = getContainerElement(element); - const wrappedText = wrapText( - normalizeText(editable.value), - font, - getBoundTextMaxWidth(container!), - ); - const { width, height } = measureText( - wrappedText, - font, - updatedTextElement.lineHeight, - ); - editable.style.width = `${width}px`; - editable.style.height = `${height}px`; - } onChange(normalizeText(editable.value)); }; } From 84bd9bd4ffc079d73b009ea80b34f678f9146e64 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 6 Jun 2023 22:04:06 +0200 Subject: [PATCH 02/27] fix: creating text while color picker open (#6651) Co-authored-by: Aakansha Doshi --- src/components/App.tsx | 17 +- src/components/ColorPicker/ColorPicker.tsx | 11 +- src/element/textWysiwyg.test.tsx | 260 +++++++-------------- src/utils.ts | 7 + 4 files changed, 111 insertions(+), 184 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index f4c7689bb5..87f94496e0 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -825,6 +825,14 @@ class App extends React.Component { if (typeof this.props.name !== "undefined") { name = this.props.name; } + + editingElement = + editingElement || actionResult.appState?.editingElement || null; + + if (editingElement?.isDeleted) { + editingElement = null; + } + this.setState( (state) => { // using Object.assign instead of spread to fool TS 4.2.2+ into @@ -835,8 +843,7 @@ class App extends React.Component { // or programmatically from the host, so it will need to be // rewritten later contextMenu: null, - editingElement: - editingElement || actionResult.appState?.editingElement || null, + editingElement, viewModeEnabled, zenModeEnabled, gridSize, @@ -1347,6 +1354,12 @@ class App extends React.Component { }); } + // failsafe in case the state is being updated in incorrect order resulting + // in the editingElement being now a deleted element + if (this.state.editingElement?.isDeleted) { + this.setState({ editingElement: null }); + } + if ( this.state.selectedLinearElement && !this.state.selectedElementIds[this.state.selectedLinearElement.elementId] diff --git a/src/components/ColorPicker/ColorPicker.tsx b/src/components/ColorPicker/ColorPicker.tsx index cec9ef9a98..d2f89c81eb 100644 --- a/src/components/ColorPicker/ColorPicker.tsx +++ b/src/components/ColorPicker/ColorPicker.tsx @@ -1,4 +1,4 @@ -import { isTransparent, isWritableElement } from "../../utils"; +import { isInteractive, isTransparent, isWritableElement } from "../../utils"; import { ExcalidrawElement } from "../../element/types"; import { AppState } from "../../types"; import { TopPicks } from "./TopPicks"; @@ -121,11 +121,14 @@ const ColorPickerPopupContent = ({ } }} onCloseAutoFocus={(e) => { - e.preventDefault(); e.stopPropagation(); + // prevents focusing the trigger + e.preventDefault(); - // return focus to excalidraw container - if (container) { + // return focus to excalidraw container unless + // user focuses an interactive element, such as a button, or + // enters the text editor by clicking on canvas with the text tool + if (container && !isInteractive(document.activeElement)) { container.focus(); } diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index 0a5bbf3202..3892d7030e 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -26,6 +26,17 @@ ReactDOM.unmountComponentAtNode(document.getElementById("root")!); const tab = " "; const mouse = new Pointer("mouse"); +const getTextEditor = () => { + return document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; +}; + +const updateTextEditor = (editor: HTMLTextAreaElement, value: string) => { + fireEvent.change(editor, { target: { value } }); + editor.dispatchEvent(new Event("input")); +}; + describe("textWysiwyg", () => { describe("start text editing", () => { const { h } = window; @@ -190,9 +201,7 @@ describe("textWysiwyg", () => { mouse.clickAt(text.x + 50, text.y + 50); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); expect(editor).not.toBe(null); expect(h.state.editingElement?.id).toBe(text.id); @@ -214,9 +223,7 @@ describe("textWysiwyg", () => { mouse.doubleClickAt(text.x + 50, text.y + 50); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); expect(editor).not.toBe(null); expect(h.state.editingElement?.id).toBe(text.id); @@ -243,9 +250,7 @@ describe("textWysiwyg", () => { textElement = UI.createElement("text"); mouse.clickOn(textElement); - textarea = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - )!; + textarea = getTextEditor(); }); afterAll(() => { @@ -455,17 +460,11 @@ describe("textWysiwyg", () => { UI.clickTool("text"); mouse.clickAt(750, 300); - textarea = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - )!; - fireEvent.change(textarea, { - target: { - value: - "Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!", - }, - }); - - textarea.dispatchEvent(new Event("input")); + textarea = getTextEditor(); + updateTextEditor( + textarea, + "Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!", + ); await new Promise((cb) => setTimeout(cb, 0)); textarea.blur(); expect(textarea.style.width).toBe("792px"); @@ -513,11 +512,9 @@ describe("textWysiwyg", () => { { id: text.id, type: "text" }, ]); mouse.down(); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); - fireEvent.change(editor, { target: { value: "Hello World!" } }); + updateTextEditor(editor, "Hello World!"); await new Promise((r) => setTimeout(r, 0)); editor.blur(); @@ -543,11 +540,9 @@ describe("textWysiwyg", () => { ]); expect(text.angle).toBe(rectangle.angle); mouse.down(); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); - fireEvent.change(editor, { target: { value: "Hello World!" } }); + updateTextEditor(editor, "Hello World!"); await new Promise((r) => setTimeout(r, 0)); editor.blur(); @@ -572,9 +567,7 @@ describe("textWysiwyg", () => { API.setSelectedElements([diamond]); Keyboard.keyPress(KEYS.ENTER); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); const value = new Array(1000).fill("1").join("\n"); @@ -587,9 +580,7 @@ describe("textWysiwyg", () => { expect(diamond.height).toBe(50020); // Clearing text to simulate height decrease - expect(() => - fireEvent.input(editor, { target: { value: "" } }), - ).not.toThrow(); + expect(() => updateTextEditor(editor, "")).not.toThrow(); expect(diamond.height).toBe(70); }); @@ -611,9 +602,7 @@ describe("textWysiwyg", () => { expect(text.type).toBe("text"); expect(text.containerId).toBe(null); mouse.down(); - let editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + let editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); editor.blur(); @@ -628,11 +617,9 @@ describe("textWysiwyg", () => { expect(text.containerId).toBe(rectangle.id); mouse.down(); - editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + editor = getTextEditor(); - fireEvent.change(editor, { target: { value: "Hello World!" } }); + updateTextEditor(editor, "Hello World!"); await new Promise((r) => setTimeout(r, 0)); editor.blur(); @@ -652,13 +639,11 @@ describe("textWysiwyg", () => { const text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.type).toBe("text"); expect(text.containerId).toBe(rectangle.id); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello World!" } }); + updateTextEditor(editor, "Hello World!"); editor.blur(); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, @@ -689,11 +674,8 @@ describe("textWysiwyg", () => { { id: text.id, type: "text" }, ]); mouse.down(); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; - - fireEvent.change(editor, { target: { value: "Hello World!" } }); + const editor = getTextEditor(); + updateTextEditor(editor, "Hello World!"); await new Promise((r) => setTimeout(r, 0)); editor.blur(); @@ -717,17 +699,9 @@ describe("textWysiwyg", () => { freedraw.y + freedraw.height / 2, ); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; - - fireEvent.change(editor, { - target: { - value: "Hello World!", - }, - }); + const editor = getTextEditor(); + updateTextEditor(editor, "Hello World!"); fireEvent.keyDown(editor, { key: KEYS.ESCAPE }); - editor.dispatchEvent(new Event("input")); expect(freedraw.boundElements).toBe(null); expect(h.elements[1].type).toBe("text"); @@ -759,11 +733,9 @@ describe("textWysiwyg", () => { expect(text.type).toBe("text"); expect(text.containerId).toBe(null); mouse.down(); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); - fireEvent.change(editor, { target: { value: "Hello World!" } }); + updateTextEditor(editor, "Hello World!"); await new Promise((r) => setTimeout(r, 0)); editor.blur(); @@ -776,17 +748,12 @@ describe("textWysiwyg", () => { UI.clickTool("text"); mouse.clickAt(20, 30); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); - fireEvent.change(editor, { - target: { - value: "Excalidraw is an opensource virtual collaborative whiteboard", - }, - }); - - editor.dispatchEvent(new Event("input")); + updateTextEditor( + editor, + "Excalidraw is an opensource virtual collaborative whiteboard", + ); await new Promise((cb) => setTimeout(cb, 0)); expect(h.elements.length).toBe(2); expect(h.elements[1].type).toBe("text"); @@ -826,12 +793,10 @@ describe("textWysiwyg", () => { mouse.down(); const text = h.elements[1] as ExcalidrawTextElementWithContainer; - let editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + let editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello World!" } }); + updateTextEditor(editor, "Hello World!"); editor.blur(); expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil); UI.clickTool("text"); @@ -841,9 +806,7 @@ describe("textWysiwyg", () => { rectangle.y + rectangle.height / 2, ); mouse.down(); - editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + editor = getTextEditor(); editor.select(); fireEvent.click(screen.getByTitle(/code/i)); @@ -876,17 +839,9 @@ describe("textWysiwyg", () => { Keyboard.keyDown(KEYS.ENTER); let text = h.elements[1] as ExcalidrawTextElementWithContainer; - let editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + let editor = getTextEditor(); - fireEvent.change(editor, { - target: { - value: "Hello World!", - }, - }); - - editor.dispatchEvent(new Event("input")); + updateTextEditor(editor, "Hello World!"); await new Promise((cb) => setTimeout(cb, 0)); editor.blur(); @@ -905,17 +860,8 @@ describe("textWysiwyg", () => { mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); - editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; - - fireEvent.change(editor, { - target: { - value: "Hello", - }, - }); - - editor.dispatchEvent(new Event("input")); + editor = getTextEditor(); + updateTextEditor(editor, "Hello"); await new Promise((r) => setTimeout(r, 0)); @@ -943,13 +889,11 @@ describe("textWysiwyg", () => { const text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.containerId).toBe(rectangle.id); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello World!" } }); + updateTextEditor(editor, "Hello World!"); editor.blur(); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, @@ -982,11 +926,9 @@ describe("textWysiwyg", () => { // Bind first text const text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.containerId).toBe(rectangle.id); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello World!" } }); + updateTextEditor(editor, "Hello World!"); editor.blur(); expect(rectangle.boundElements).toStrictEqual([ { id: text.id, type: "text" }, @@ -1005,11 +947,9 @@ describe("textWysiwyg", () => { it("should respect text alignment when resizing", async () => { Keyboard.keyPress(KEYS.ENTER); - let editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + let editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello" } }); + updateTextEditor(editor, "Hello"); editor.blur(); // should center align horizontally and vertically by default @@ -1024,9 +964,7 @@ describe("textWysiwyg", () => { mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); - editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + editor = getTextEditor(); editor.select(); @@ -1049,9 +987,7 @@ describe("textWysiwyg", () => { mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); - editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + editor = getTextEditor(); editor.select(); @@ -1089,11 +1025,9 @@ describe("textWysiwyg", () => { expect(text.type).toBe("text"); expect(text.containerId).toBe(rectangle.id); mouse.down(); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); - fireEvent.change(editor, { target: { value: "Hello World!" } }); + updateTextEditor(editor, "Hello World!"); await new Promise((r) => setTimeout(r, 0)); editor.blur(); @@ -1106,11 +1040,9 @@ describe("textWysiwyg", () => { it("should scale font size correctly when resizing using shift", async () => { Keyboard.keyPress(KEYS.ENTER); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello" } }); + updateTextEditor(editor, "Hello"); editor.blur(); const textElement = h.elements[1] as ExcalidrawTextElement; expect(rectangle.width).toBe(90); @@ -1128,11 +1060,9 @@ describe("textWysiwyg", () => { it("should bind text correctly when container duplicated with alt-drag", async () => { Keyboard.keyPress(KEYS.ENTER); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello" } }); + updateTextEditor(editor, "Hello"); editor.blur(); expect(h.elements.length).toBe(2); @@ -1162,11 +1092,9 @@ describe("textWysiwyg", () => { it("undo should work", async () => { Keyboard.keyPress(KEYS.ENTER); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello" } }); + updateTextEditor(editor, "Hello"); editor.blur(); expect(rectangle.boundElements).toStrictEqual([ { id: h.elements[1].id, type: "text" }, @@ -1201,12 +1129,10 @@ describe("textWysiwyg", () => { it("should not allow bound text with only whitespaces", async () => { Keyboard.keyPress(KEYS.ENTER); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: " " } }); + updateTextEditor(editor, " "); editor.blur(); expect(rectangle.boundElements).toStrictEqual([]); expect(h.elements[1].isDeleted).toBe(true); @@ -1225,9 +1151,9 @@ describe("textWysiwyg", () => { type: "text", text: "Online whiteboard collaboration made easy", }); + h.elements = [container, text]; API.setSelectedElements([container, text]); - fireEvent.contextMenu(GlobalTestState.canvas, { button: 2, clientX: 20, @@ -1258,11 +1184,9 @@ describe("textWysiwyg", () => { it("should reset the container height cache when resizing", async () => { Keyboard.keyPress(KEYS.ENTER); expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75); - let editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + let editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello" } }); + updateTextEditor(editor, "Hello"); editor.blur(); resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]); @@ -1272,9 +1196,7 @@ describe("textWysiwyg", () => { mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); - editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + editor = getTextEditor(); await new Promise((r) => setTimeout(r, 0)); editor.blur(); @@ -1287,12 +1209,8 @@ describe("textWysiwyg", () => { Keyboard.keyPress(KEYS.ENTER); expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; - - await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello World!" } }); + const editor = getTextEditor(); + updateTextEditor(editor, "Hello World!"); editor.blur(); mouse.select(rectangle); @@ -1316,12 +1234,8 @@ describe("textWysiwyg", () => { Keyboard.keyPress(KEYS.ENTER); expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; - - await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello World!" } }); + const editor = getTextEditor(); + updateTextEditor(editor, "Hello World!"); editor.blur(); expect( (h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight, @@ -1352,17 +1266,12 @@ describe("textWysiwyg", () => { beforeEach(async () => { Keyboard.keyPress(KEYS.ENTER); - editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; - await new Promise((r) => setTimeout(r, 0)); - fireEvent.change(editor, { target: { value: "Hello" } }); + editor = getTextEditor(); + updateTextEditor(editor, "Hello"); editor.blur(); mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); - editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + editor = getTextEditor(); editor.select(); }); @@ -1473,17 +1382,12 @@ describe("textWysiwyg", () => { it("should wrap text in a container when wrap text in container triggered from context menu", async () => { UI.clickTool("text"); mouse.clickAt(20, 30); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = getTextEditor(); - fireEvent.change(editor, { - target: { - value: "Excalidraw is an opensource virtual collaborative whiteboard", - }, - }); - - editor.dispatchEvent(new Event("input")); + updateTextEditor( + editor, + "Excalidraw is an opensource virtual collaborative whiteboard", + ); await new Promise((cb) => setTimeout(cb, 0)); editor.select(); diff --git a/src/utils.ts b/src/utils.ts index b5a65cae21..98dfb48d71 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -60,6 +60,13 @@ export const isInputLike = ( target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement; +export const isInteractive = (target: Element | EventTarget | null) => { + return ( + isInputLike(target) || + (target instanceof Element && !!target.closest("label, button")) + ); +}; + export const isWritableElement = ( target: Element | EventTarget | null, ): target is From a39640ead1d90e7c4b5f4d30fdf07735bf5f3f3e Mon Sep 17 00:00:00 2001 From: WBbug <1056342711@qq.com> Date: Thu, 8 Jun 2023 17:41:22 +0800 Subject: [PATCH 03/27] fix: delete setCursor when resize (#6660) --- src/components/App.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 87f94496e0..ad41e72899 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -4155,12 +4155,6 @@ class App extends React.Component { ); } if (pointerDownState.resize.handleType) { - setCursor( - this.canvas, - getCursorForResizingElement({ - transformHandleType: pointerDownState.resize.handleType, - }), - ); pointerDownState.resize.isResizing = true; pointerDownState.resize.offset = tupleToCoors( getResizeOffsetXY( From b4abfad6381e99ab0e1b73f91f3846055e43b8bf Mon Sep 17 00:00:00 2001 From: Alex Kim <45559664+alex-kim-dev@users.noreply.github.com> Date: Fri, 9 Jun 2023 16:22:40 +0500 Subject: [PATCH 04/27] fix: bound arrows not updated when rotating multiple elements (#6662) --- src/element/resizeElements.ts | 42 +++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index 3610d7577c..6e49b7e09d 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -890,26 +890,34 @@ const rotateMultipleElements = ( centerY, centerAngle + origAngle - element.angle, ); - mutateElement(element, { - x: element.x + (rotatedCX - cx), - y: element.y + (rotatedCY - cy), - angle: normalizeAngle(centerAngle + origAngle), - }); - const boundTextElementId = getBoundTextElementId(element); - if (boundTextElementId) { - const textElement = - Scene.getScene(element)?.getElement( - boundTextElementId, - ); - if (textElement && !isArrowElement(element)) { - mutateElement(textElement, { - x: textElement.x + (rotatedCX - cx), - y: textElement.y + (rotatedCY - cy), + + mutateElement( + element, + { + x: element.x + (rotatedCX - cx), + y: element.y + (rotatedCY - cy), + angle: normalizeAngle(centerAngle + origAngle), + }, + false, + ); + + updateBoundElements(element, { simultaneouslyUpdated: elements }); + + const boundText = getBoundTextElement(element); + if (boundText && !isArrowElement(element)) { + mutateElement( + boundText, + { + x: boundText.x + (rotatedCX - cx), + y: boundText.y + (rotatedCY - cy), angle: normalizeAngle(centerAngle + origAngle), - }); - } + }, + false, + ); } }); + + Scene.getScene(elements[0])?.informMutation(); }; export const getResizeOffsetXY = ( From 5ca3613cc30f207e32558af9a8b9c35e3bb95d49 Mon Sep 17 00:00:00 2001 From: Arnost Pleskot Date: Mon, 12 Jun 2023 15:43:14 +0200 Subject: [PATCH 05/27] feat: redesigned collab cursors (#6659) Co-authored-by: dwelle --- src/actions/actionNavigate.tsx | 7 +- src/clients.ts | 52 ++++++------- src/components/Avatar.scss | 5 +- src/components/Avatar.tsx | 1 - src/renderer/renderScene.ts | 131 +++++++++++++++++---------------- 5 files changed, 100 insertions(+), 96 deletions(-) diff --git a/src/actions/actionNavigate.tsx b/src/actions/actionNavigate.tsx index 1b808e4a7b..126e547ae2 100644 --- a/src/actions/actionNavigate.tsx +++ b/src/actions/actionNavigate.tsx @@ -1,4 +1,4 @@ -import { getClientColors } from "../clients"; +import { getClientColor } from "../clients"; import { Avatar } from "../components/Avatar"; import { centerScrollOn } from "../scene/scroll"; import { Collaborator } from "../types"; @@ -31,15 +31,14 @@ export const actionGoToCollaborator = register({ commitToHistory: false, }; }, - PanelComponent: ({ appState, updateData, data }) => { + PanelComponent: ({ updateData, data }) => { const [clientId, collaborator] = data as [string, Collaborator]; - const { background, stroke } = getClientColors(clientId, appState); + const background = getClientColor(clientId); return ( updateData(collaborator.pointer)} name={collaborator.username || ""} src={collaborator.avatarUrl} diff --git a/src/clients.ts b/src/clients.ts index 604936e334..3540989184 100644 --- a/src/clients.ts +++ b/src/clients.ts @@ -1,31 +1,31 @@ -import { - DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX, - DEFAULT_ELEMENT_STROKE_COLOR_INDEX, - getAllColorsSpecificShade, -} from "./colors"; -import { AppState } from "./types"; - -const BG_COLORS = getAllColorsSpecificShade( - DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX, -); -const STROKE_COLORS = getAllColorsSpecificShade( - DEFAULT_ELEMENT_STROKE_COLOR_INDEX, -); - -export const getClientColors = (clientId: string, appState: AppState) => { - if (appState?.collaborators) { - const currentUser = appState.collaborators.get(clientId); - if (currentUser?.color) { - return currentUser.color; - } +function hashToInteger(id: string) { + let hash = 0; + if (id.length === 0) { + return hash; } - // Naive way of getting an integer out of the clientId - const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0); + for (let i = 0; i < id.length; i++) { + const char = id.charCodeAt(i); + hash = (hash << 5) - hash + char; + } + return hash; +} - return { - background: BG_COLORS[sum % BG_COLORS.length], - stroke: STROKE_COLORS[sum % STROKE_COLORS.length], - }; +export const getClientColor = ( + /** + * any uniquely identifying key, such as user id or socket id + */ + id: string, +) => { + // to get more even distribution in case `id` is not uniformly distributed to + // begin with, we hash it + const hash = Math.abs(hashToInteger(id)); + // we want to get a multiple of 10 number in the range of 0-360 (in other + // words a hue value of step size 10). There are 37 such values including 0. + const hue = (hash % 37) * 10; + const saturation = 100; + const lightness = 83; + + return `hsl(${hue}, ${saturation}%, ${lightness}%)`; }; /** diff --git a/src/components/Avatar.scss b/src/components/Avatar.scss index 86b9c42388..c0c66f0a2c 100644 --- a/src/components/Avatar.scss +++ b/src/components/Avatar.scss @@ -10,10 +10,9 @@ display: flex; justify-content: center; align-items: center; - color: $oc-white; cursor: pointer; - font-size: 0.625rem; - font-weight: 500; + font-size: 0.75rem; + font-weight: 800; line-height: 1; &-img { diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 20dc7b9f2b..8b4624b7f4 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -6,7 +6,6 @@ import { getNameInitial } from "../clients"; type AvatarProps = { onClick: (e: React.MouseEvent) => void; color: string; - border: string; name: string; src?: string; }; diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index c8b64b47b5..53c4f387f8 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -30,7 +30,7 @@ import { import { getSelectedElements } from "../scene/selection"; import { renderElement, renderElementToSvg } from "./renderElement"; -import { getClientColors } from "../clients"; +import { getClientColor } from "../clients"; import { LinearElementEditor } from "../element/linearElementEditor"; import { isSelectedViaGroup, @@ -48,11 +48,7 @@ import { TransformHandles, TransformHandleType, } from "../element/transformHandles"; -import { - viewportCoordsToSceneCoords, - supportsEmoji, - throttleRAF, -} from "../utils"; +import { viewportCoordsToSceneCoords, throttleRAF } from "../utils"; import { UserIdleState } from "../types"; import { THEME_FILTER } from "../constants"; import { @@ -61,7 +57,6 @@ import { } from "../element/Hyperlink"; import { isLinearElement } from "../element/typeChecks"; -const hasEmojiSupport = supportsEmoji(); export const DEFAULT_SPACING = 2; const strokeRectWithRotation = ( @@ -159,7 +154,6 @@ const strokeGrid = ( const renderSingleLinearPoint = ( context: CanvasRenderingContext2D, - appState: AppState, renderConfig: RenderConfig, point: Point, radius: number, @@ -206,14 +200,7 @@ const renderLinearPointHandles = ( const isSelected = !!appState.editingLinearElement?.selectedPointsIndices?.includes(idx); - renderSingleLinearPoint( - context, - appState, - renderConfig, - point, - radius, - isSelected, - ); + renderSingleLinearPoint(context, renderConfig, point, radius, isSelected); }); //Rendering segment mid points @@ -237,7 +224,6 @@ const renderLinearPointHandles = ( if (appState.editingLinearElement) { renderSingleLinearPoint( context, - appState, renderConfig, segmentMidPoint, radius, @@ -248,7 +234,6 @@ const renderLinearPointHandles = ( highlightPoint(segmentMidPoint, context, renderConfig); renderSingleLinearPoint( context, - appState, renderConfig, segmentMidPoint, radius, @@ -258,7 +243,6 @@ const renderLinearPointHandles = ( } else if (appState.editingLinearElement || points.length === 2) { renderSingleLinearPoint( context, - appState, renderConfig, segmentMidPoint, POINT_HANDLE_SIZE / 2, @@ -527,7 +511,7 @@ export const _renderScene = ({ selectionColors.push( ...renderConfig.remoteSelectedElementIds[element.id].map( (socketId) => { - const { background } = getClientColors(socketId, appState); + const background = getClientColor(socketId); return background; }, ), @@ -647,7 +631,7 @@ export const _renderScene = ({ x -= appState.offsetLeft; y -= appState.offsetTop; - const width = 9; + const width = 11; const height = 14; const isOutOfBounds = @@ -661,15 +645,20 @@ export const _renderScene = ({ y = Math.max(y, 0); y = Math.min(y, normalizedCanvasHeight - height); - const { background, stroke } = getClientColors(clientId, appState); + const background = getClientColor(clientId); context.save(); - context.strokeStyle = stroke; + context.strokeStyle = background; context.fillStyle = background; const userState = renderConfig.remotePointerUserStates[clientId]; - if (isOutOfBounds || userState === UserIdleState.AWAY) { - context.globalAlpha = 0.48; + const isInactive = + isOutOfBounds || + userState === UserIdleState.IDLE || + userState === UserIdleState.AWAY; + + if (isInactive) { + context.globalAlpha = 0.3; } if ( @@ -686,73 +675,91 @@ export const _renderScene = ({ context.beginPath(); context.arc(x, y, 15, 0, 2 * Math.PI, false); context.lineWidth = 1; - context.strokeStyle = stroke; + context.strokeStyle = background; context.stroke(); context.closePath(); } + // Background (white outline) for arrow + context.fillStyle = oc.white; + context.strokeStyle = oc.white; + context.lineWidth = 6; + context.lineJoin = "round"; context.beginPath(); context.moveTo(x, y); - context.lineTo(x + 1, y + 14); + context.lineTo(x + 0, y + 14); context.lineTo(x + 4, y + 9); - context.lineTo(x + 9, y + 10); - context.lineTo(x, y); - context.fill(); + context.lineTo(x + 11, y + 8); + context.closePath(); context.stroke(); + context.fill(); - const username = renderConfig.remotePointerUsernames[clientId]; - - let idleState = ""; - if (userState === UserIdleState.AWAY) { - idleState = hasEmojiSupport ? "⚫️" : ` (${UserIdleState.AWAY})`; - } else if (userState === UserIdleState.IDLE) { - idleState = hasEmojiSupport ? "💤" : ` (${UserIdleState.IDLE})`; + // Arrow + context.fillStyle = background; + context.strokeStyle = background; + context.lineWidth = 2; + context.lineJoin = "round"; + context.beginPath(); + if (isInactive) { + context.moveTo(x - 1, y - 1); + context.lineTo(x - 1, y + 15); + context.lineTo(x + 5, y + 10); + context.lineTo(x + 12, y + 9); + context.closePath(); + context.fill(); + } else { + context.moveTo(x, y); + context.lineTo(x + 0, y + 14); + context.lineTo(x + 4, y + 9); + context.lineTo(x + 11, y + 8); + context.closePath(); + context.fill(); + context.stroke(); } - const usernameAndIdleState = `${username || ""}${ - idleState ? ` ${idleState}` : "" - }`; + const username = renderConfig.remotePointerUsernames[clientId] || ""; - if (!isOutOfBounds && usernameAndIdleState) { - const offsetX = x + width; - const offsetY = y + height; - const paddingHorizontal = 4; - const paddingVertical = 4; - const measure = context.measureText(usernameAndIdleState); + if (!isOutOfBounds && username) { + context.font = "600 12px sans-serif"; // font has to be set before context.measureText() + + const offsetX = x + width / 2; + const offsetY = y + height + 2; + const paddingHorizontal = 5; + const paddingVertical = 3; + const measure = context.measureText(username); const measureHeight = measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent; + const finalHeight = Math.max(measureHeight, 12); const boxX = offsetX - 1; const boxY = offsetY - 1; - const boxWidth = measure.width + 2 * paddingHorizontal + 2; - const boxHeight = measureHeight + 2 * paddingVertical + 2; + const boxWidth = measure.width + 2 + paddingHorizontal * 2 + 2; + const boxHeight = finalHeight + 2 + paddingVertical * 2 + 2; if (context.roundRect) { context.beginPath(); - context.roundRect( - boxX, - boxY, - boxWidth, - boxHeight, - 4 / renderConfig.zoom.value, - ); + context.roundRect(boxX, boxY, boxWidth, boxHeight, 8); context.fillStyle = background; context.fill(); - context.fillStyle = stroke; + context.strokeStyle = oc.white; context.stroke(); } else { // Border - context.fillStyle = stroke; + context.fillStyle = oc.white; context.fillRect(boxX, boxY, boxWidth, boxHeight); // Background context.fillStyle = background; context.fillRect(offsetX, offsetY, boxWidth - 2, boxHeight - 2); } - context.fillStyle = oc.white; + context.fillStyle = oc.black; context.fillText( - usernameAndIdleState, - offsetX + paddingHorizontal, - offsetY + paddingVertical + measure.actualBoundingBoxAscent, + username, + offsetX + paddingHorizontal + 1, + offsetY + + paddingVertical + + measure.actualBoundingBoxAscent + + Math.floor((finalHeight - measureHeight) / 2) + + 1, ); } @@ -1145,7 +1152,7 @@ export const renderSceneToSvg = ( return; } // render elements - elements.forEach((element, index) => { + elements.forEach((element) => { if (!element.isDeleted) { try { renderElementToSvg( From 16c7945ca07b6e21dd70ee869c3cda51b38db7cd Mon Sep 17 00:00:00 2001 From: Arnost Pleskot Date: Mon, 12 Jun 2023 16:05:07 +0200 Subject: [PATCH 06/27] feat: assign random user name when not set (#6663) --- package.json | 1 + src/excalidraw-app/collab/Collab.tsx | 7 +++++++ yarn.lock | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/package.json b/package.json index 91a4400b6e..b5432d37bf 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ ] }, "dependencies": { + "@excalidraw/random-username": "1.0.0", "@radix-ui/react-popover": "1.0.3", "@radix-ui/react-tabs": "1.0.2", "@sentry/browser": "6.2.5", diff --git a/src/excalidraw-app/collab/Collab.tsx b/src/excalidraw-app/collab/Collab.tsx index eb12952dd9..76d07bb9f3 100644 --- a/src/excalidraw-app/collab/Collab.tsx +++ b/src/excalidraw-app/collab/Collab.tsx @@ -380,6 +380,13 @@ class Collab extends PureComponent { startCollaboration = async ( existingRoomLinkData: null | { roomId: string; roomKey: string }, ): Promise => { + if (!this.state.username) { + import("@excalidraw/random-username").then(({ getRandomUsername }) => { + const username = getRandomUsername(); + this.onUsernameChange(username); + }); + } + if (this.portal.socket) { return null; } diff --git a/yarn.lock b/yarn.lock index 720e61a3ef..7d385cce71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1254,6 +1254,11 @@ resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65" integrity sha512-rFIq8+A8WvkEzBsF++Rw6gzxE+hU3ZNkdg8foI+Upz2y/rOC/gUpWJaggPbCkoH3nlREVU59axQjZ1+F6ePRGg== +"@excalidraw/random-username@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@excalidraw/random-username/-/random-username-1.0.0.tgz#6d5293148aee6cd08dcdfcadc0c91276572f4499" + integrity sha512-pd4VapWahQ7PIyThGq32+C+JUS73mf3RSdC7BmQiXzhQsCTU4RHc8y9jBi+pb1CFV0iJXvjJRXnVdLCbTj3+HA== + "@firebase/analytics-types@0.4.0": version "0.4.0" resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.4.0.tgz#d6716f9fa36a6e340bc0ecfe68af325aa6f60508" From ce9acfbc550586154d90e5a4e2c1a122ac1df216 Mon Sep 17 00:00:00 2001 From: Excalidraw Bot <77840495+excalibot@users.noreply.github.com> Date: Mon, 12 Jun 2023 16:08:28 +0200 Subject: [PATCH 07/27] chore: Update translations from Crowdin (#6641) --- src/locales/az-AZ.json | 449 +++++++++++++++++++++++++++++++++++ src/locales/cs-CZ.json | 266 ++++++++++----------- src/locales/de-DE.json | 2 +- src/locales/hi-IN.json | 2 +- src/locales/ko-KR.json | 2 +- src/locales/mr-IN.json | 2 +- src/locales/nb-NO.json | 2 +- src/locales/percentages.json | 29 +-- src/locales/pl-PL.json | 2 +- src/locales/pt-PT.json | 2 +- src/locales/ro-RO.json | 2 +- src/locales/ru-RU.json | 2 +- src/locales/sk-SK.json | 2 +- src/locales/sl-SI.json | 2 +- src/locales/sv-SE.json | 2 +- src/locales/ta-IN.json | 20 +- src/locales/zh-CN.json | 40 ++-- src/locales/zh-TW.json | 2 +- 18 files changed, 640 insertions(+), 190 deletions(-) create mode 100644 src/locales/az-AZ.json diff --git a/src/locales/az-AZ.json b/src/locales/az-AZ.json new file mode 100644 index 0000000000..b42f3b422b --- /dev/null +++ b/src/locales/az-AZ.json @@ -0,0 +1,449 @@ +{ + "labels": { + "paste": "Yapışdır", + "pasteAsPlaintext": "Düz mətn kimi yapışdırın", + "pasteCharts": "Diaqramları yapışdırın", + "selectAll": "Hamısını seç", + "multiSelect": "Seçimə element əlavə edin", + "moveCanvas": "Kanvası köçürün", + "cut": "Kəs", + "copy": "Kopyala", + "copyAsPng": "PNG olaraq panoya kopyala", + "copyAsSvg": "SVG olaraq panoya kopyala", + "copyText": "Mətn olaraq panoya kopyala", + "bringForward": "Önə daşı", + "sendToBack": "Geriyə göndərin", + "bringToFront": "Önə gətirin", + "sendBackward": "Geriyə göndərin", + "delete": "Sil", + "copyStyles": "Stilləri kopyalayın", + "pasteStyles": "Stilləri yapışdırın", + "stroke": "Strok rəngi", + "background": "Arxa fon", + "fill": "Doldur", + "strokeWidth": "Strok eni", + "strokeStyle": "Strok stili", + "strokeStyle_solid": "Solid", + "strokeStyle_dashed": "Kəsik", + "strokeStyle_dotted": "Nöqtəli", + "sloppiness": "Səliqəsizlik", + "opacity": "Şəffaflıq", + "textAlign": "Mətni uyğunlaşdır", + "edges": "Kənarlar", + "sharp": "Kəskin", + "round": "Dəyirmi", + "arrowheads": "Ox ucları", + "arrowhead_none": "Heç biri", + "arrowhead_arrow": "Ox", + "arrowhead_bar": "Çubuq", + "arrowhead_dot": "Nöqtə", + "arrowhead_triangle": "Üçbucaq", + "fontSize": "Şrift ölçüsü", + "fontFamily": "Şrift qrupu", + "addWatermark": "\"Made with Excalidraw\" əlavə et", + "handDrawn": "Əllə çəkilmiş", + "normal": "Normal", + "code": "Kod", + "small": "Kiçik", + "medium": "Orta", + "large": "Böyük", + "veryLarge": "Çox böyük", + "solid": "Solid", + "hachure": "Ştrix", + "zigzag": "Ziqzaq", + "crossHatch": "Çarpaz dəlik", + "thin": "İncə", + "bold": "Qalın", + "left": "Sol", + "center": "Mərkəz", + "right": "Sağ", + "extraBold": "Ekstra qalın", + "architect": "Memar", + "artist": "Rəssam", + "cartoonist": "Karikaturaçı", + "fileTitle": "Fayl adı", + "colorPicker": "Rəng seçən", + "canvasColors": "Kanvas üzərində istifadə olunur", + "canvasBackground": "Kanvas arxa fonu", + "drawingCanvas": "Kanvas çəkmək", + "layers": "Qatlar", + "actions": "Hərəkətlər", + "language": "Dil", + "liveCollaboration": "Canlı əməkdaşlıq...", + "duplicateSelection": "Dublikat", + "untitled": "Başlıqsız", + "name": "Ad", + "yourName": "Adınız", + "madeWithExcalidraw": "Excalidraw ilə hazırlanmışdır", + "group": "Qrup şəklində seçim", + "ungroup": "Qrupsuz seçim", + "collaborators": "", + "showGrid": "", + "addToLibrary": "", + "removeFromLibrary": "", + "libraryLoadingMessage": "", + "libraries": "", + "loadingScene": "", + "align": "", + "alignTop": "", + "alignBottom": "", + "alignLeft": "", + "alignRight": "", + "centerVertically": "", + "centerHorizontally": "", + "distributeHorizontally": "", + "distributeVertically": "", + "flipHorizontal": "", + "flipVertical": "", + "viewMode": "", + "share": "", + "showStroke": "", + "showBackground": "", + "toggleTheme": "", + "personalLib": "", + "excalidrawLib": "", + "decreaseFontSize": "", + "increaseFontSize": "", + "unbindText": "", + "bindText": "", + "createContainerFromText": "", + "link": { + "edit": "", + "create": "", + "label": "" + }, + "lineEditor": { + "edit": "", + "exit": "" + }, + "elementLock": { + "lock": "", + "unlock": "", + "lockAll": "", + "unlockAll": "" + }, + "statusPublished": "", + "sidebarLock": "", + "eyeDropper": "" + }, + "library": { + "noItems": "", + "hint_emptyLibrary": "", + "hint_emptyPrivateLibrary": "" + }, + "buttons": { + "clearReset": "", + "exportJSON": "", + "exportImage": "", + "export": "", + "copyToClipboard": "", + "save": "", + "saveAs": "", + "load": "", + "getShareableLink": "", + "close": "", + "selectLanguage": "", + "scrollBackToContent": "", + "zoomIn": "", + "zoomOut": "", + "resetZoom": "", + "menu": "", + "done": "", + "edit": "", + "undo": "", + "redo": "", + "resetLibrary": "", + "createNewRoom": "", + "fullScreen": "", + "darkMode": "", + "lightMode": "", + "zenMode": "", + "exitZenMode": "", + "cancel": "", + "clear": "", + "remove": "", + "publishLibrary": "", + "submit": "", + "confirm": "" + }, + "alerts": { + "clearReset": "", + "couldNotCreateShareableLink": "", + "couldNotCreateShareableLinkTooBig": "", + "couldNotLoadInvalidFile": "", + "importBackendFailed": "", + "cannotExportEmptyCanvas": "", + "couldNotCopyToClipboard": "", + "decryptFailed": "", + "uploadedSecurly": "", + "loadSceneOverridePrompt": "", + "collabStopOverridePrompt": "", + "errorAddingToLibrary": "", + "errorRemovingFromLibrary": "", + "confirmAddLibrary": "", + "imageDoesNotContainScene": "", + "cannotRestoreFromImage": "", + "invalidSceneUrl": "", + "resetLibrary": "", + "removeItemsFromsLibrary": "", + "invalidEncryptionKey": "", + "collabOfflineWarning": "" + }, + "errors": { + "unsupportedFileType": "", + "imageInsertError": "", + "fileTooBig": "", + "svgImageInsertError": "", + "invalidSVGString": "", + "cannotResolveCollabServer": "", + "importLibraryError": "", + "collabSaveFailed": "", + "collabSaveFailed_sizeExceeded": "", + "brave_measure_text_error": { + "line1": "", + "line2": "", + "line3": "", + "line4": "" + } + }, + "toolBar": { + "selection": "", + "image": "", + "rectangle": "", + "diamond": "", + "ellipse": "", + "arrow": "", + "line": "", + "freedraw": "", + "text": "", + "library": "", + "lock": "", + "penMode": "", + "link": "", + "eraser": "", + "hand": "" + }, + "headings": { + "canvasActions": "", + "selectedShapeActions": "", + "shapes": "" + }, + "hints": { + "canvasPanning": "", + "linearElement": "", + "freeDraw": "", + "text": "", + "text_selected": "", + "text_editing": "", + "linearElementMulti": "", + "lockAngle": "", + "resize": "", + "resizeImage": "", + "rotate": "", + "lineEditor_info": "", + "lineEditor_pointSelected": "", + "lineEditor_nothingSelected": "", + "placeImage": "", + "publishLibrary": "", + "bindTextToElement": "", + "deepBoxSelect": "", + "eraserRevert": "", + "firefox_clipboard_write": "" + }, + "canvasError": { + "cannotShowPreview": "", + "canvasTooBig": "", + "canvasTooBigTip": "" + }, + "errorSplash": { + "headingMain": "", + "clearCanvasMessage": "", + "clearCanvasCaveat": "", + "trackedToSentry": "", + "openIssueMessage": "", + "sceneContent": "" + }, + "roomDialog": { + "desc_intro": "", + "desc_privacy": "", + "button_startSession": "", + "button_stopSession": "", + "desc_inProgressIntro": "", + "desc_shareLink": "", + "desc_exitSession": "", + "shareTitle": "" + }, + "errorDialog": { + "title": "" + }, + "exportDialog": { + "disk_title": "", + "disk_details": "", + "disk_button": "", + "link_title": "", + "link_details": "", + "link_button": "", + "excalidrawplus_description": "", + "excalidrawplus_button": "", + "excalidrawplus_exportError": "" + }, + "helpDialog": { + "blog": "", + "click": "", + "deepSelect": "", + "deepBoxSelect": "", + "curvedArrow": "", + "curvedLine": "", + "documentation": "", + "doubleClick": "", + "drag": "", + "editor": "", + "editLineArrowPoints": "", + "editText": "", + "github": "", + "howto": "", + "or": "", + "preventBinding": "", + "tools": "", + "shortcuts": "", + "textFinish": "", + "textNewLine": "", + "title": "", + "view": "", + "zoomToFit": "", + "zoomToSelection": "", + "toggleElementLock": "", + "movePageUpDown": "", + "movePageLeftRight": "" + }, + "clearCanvasDialog": { + "title": "" + }, + "publishDialog": { + "title": "", + "itemName": "", + "authorName": "", + "githubUsername": "", + "twitterUsername": "", + "libraryName": "", + "libraryDesc": "", + "website": "", + "placeholder": { + "authorName": "", + "libraryName": "", + "libraryDesc": "", + "githubHandle": "", + "twitterHandle": "", + "website": "" + }, + "errors": { + "required": "", + "website": "" + }, + "noteDescription": "", + "noteGuidelines": "", + "noteLicense": "", + "noteItems": "", + "atleastOneLibItem": "", + "republishWarning": "" + }, + "publishSuccessDialog": { + "title": "", + "content": "" + }, + "confirmDialog": { + "resetLibrary": "", + "removeItemsFromLib": "" + }, + "imageExportDialog": { + "header": "", + "label": { + "withBackground": "", + "onlySelected": "", + "darkMode": "", + "embedScene": "", + "scale": "", + "padding": "" + }, + "tooltip": { + "embedScene": "" + }, + "title": { + "exportToPng": "", + "exportToSvg": "", + "copyPngToClipboard": "" + }, + "button": { + "exportToPng": "", + "exportToSvg": "", + "copyPngToClipboard": "" + } + }, + "encrypted": { + "tooltip": "", + "link": "" + }, + "stats": { + "angle": "", + "element": "", + "elements": "", + "height": "", + "scene": "", + "selected": "", + "storage": "", + "title": "", + "total": "", + "version": "", + "versionCopy": "", + "versionNotAvailable": "", + "width": "" + }, + "toast": { + "addedToLibrary": "", + "copyStyles": "", + "copyToClipboard": "", + "copyToClipboardAsPng": "", + "fileSaved": "", + "fileSavedToFilename": "", + "canvas": "", + "selection": "", + "pasteAsSingleElement": "" + }, + "colors": { + "transparent": "", + "black": "", + "white": "", + "red": "", + "pink": "", + "grape": "", + "violet": "", + "gray": "", + "blue": "", + "cyan": "", + "teal": "", + "green": "", + "yellow": "", + "orange": "", + "bronze": "" + }, + "welcomeScreen": { + "app": { + "center_heading": "", + "center_heading_plus": "", + "menuHint": "" + }, + "defaults": { + "menuHint": "", + "center_heading": "", + "toolbarHint": "", + "helpHint": "" + } + }, + "colorPicker": { + "mostUsedCustomColors": "", + "colors": "", + "shades": "", + "hexCode": "", + "noShades": "" + } +} diff --git a/src/locales/cs-CZ.json b/src/locales/cs-CZ.json index bafa253742..73dcadeaed 100644 --- a/src/locales/cs-CZ.json +++ b/src/locales/cs-CZ.json @@ -1,7 +1,7 @@ { "labels": { "paste": "Vložit", - "pasteAsPlaintext": "", + "pasteAsPlaintext": "Vložit jako prostý text", "pasteCharts": "Vložit grafy", "selectAll": "Vybrat vše", "multiSelect": "Přidat prvek do výběru", @@ -49,9 +49,9 @@ "large": "Velké", "veryLarge": "Velmi velké", "solid": "Plný", - "hachure": "", - "zigzag": "", - "crossHatch": "", + "hachure": "Hachure", + "zigzag": "Klikatě", + "crossHatch": "Křížový šrafování", "thin": "Tenký", "bold": "Tlustý", "left": "Vlevo", @@ -60,7 +60,7 @@ "extraBold": "Extra tlustý", "architect": "Architekt", "artist": "Umělec", - "cartoonist": "", + "cartoonist": "Kartoonista", "fileTitle": "Název souboru", "colorPicker": "Výběr barvy", "canvasColors": "Použito na plátně", @@ -106,7 +106,7 @@ "increaseFontSize": "Zvětšit písmo", "unbindText": "Zrušit vazbu textu", "bindText": "Vázat text s kontejnerem", - "createContainerFromText": "", + "createContainerFromText": "Zabalit text do kontejneru", "link": { "edit": "Upravit odkaz", "create": "Vytvořit odkaz", @@ -124,7 +124,7 @@ }, "statusPublished": "Zveřejněno", "sidebarLock": "Ponechat postranní panel otevřený", - "eyeDropper": "" + "eyeDropper": "Vyberte barvu z plátna" }, "library": { "noItems": "Dosud neexistují žádné položky...", @@ -177,38 +177,38 @@ "decryptFailed": "Nelze dešifrovat data.", "uploadedSecurly": "Nahrávání je zabezpečeno koncovým šifrováním, což znamená, že server Excalidraw ani třetí strany nemohou obsah přečíst.", "loadSceneOverridePrompt": "Načítání externího výkresu nahradí váš existující obsah. Přejete si pokračovat?", - "collabStopOverridePrompt": "", + "collabStopOverridePrompt": "Zastavení relace přepíše vaše předchozí, lokálně uložené kresby. Jste si jisti?\n\n(Pokud chcete zachovat místní kresbu, jednoduše zavřete kartu prohlížeče)", "errorAddingToLibrary": "Položku nelze přidat do knihovny", "errorRemovingFromLibrary": "Položku nelze odstranit z knihovny", "confirmAddLibrary": "Tímto přidáte {{numShapes}} tvarů do tvé knihovny. Jste si jisti?", "imageDoesNotContainScene": "Zdá se, že tento obrázek neobsahuje žádná data o scéně. Zapnuli jste při exportu vkládání scény?", - "cannotRestoreFromImage": "", - "invalidSceneUrl": "", - "resetLibrary": "", - "removeItemsFromsLibrary": "", - "invalidEncryptionKey": "", - "collabOfflineWarning": "" + "cannotRestoreFromImage": "Scénu nelze obnovit z tohoto souboru obrázku", + "invalidSceneUrl": "Nelze importovat scénu z zadané URL. Je buď poškozená, nebo neobsahuje platná JSON data Excalidraw.", + "resetLibrary": "Tímto vymažete vaši knihovnu. Jste si jisti?", + "removeItemsFromsLibrary": "Smazat {{count}} položek z knihovny?", + "invalidEncryptionKey": "Šifrovací klíč musí mít 22 znaků. Live spolupráce je zakázána.", + "collabOfflineWarning": "Není k dispozici žádné internetové připojení.\nVaše změny nebudou uloženy!" }, "errors": { - "unsupportedFileType": "", - "imageInsertError": "", - "fileTooBig": "", - "svgImageInsertError": "", - "invalidSVGString": "", - "cannotResolveCollabServer": "", - "importLibraryError": "", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "", + "unsupportedFileType": "Nepodporovaný typ souboru.", + "imageInsertError": "Nelze vložit obrázek. Zkuste to později...", + "fileTooBig": "Soubor je příliš velký. Maximální povolená velikost je {{maxSize}}.", + "svgImageInsertError": "Nelze vložit SVG obrázek. Značení SVG je neplatné.", + "invalidSVGString": "Neplatný SVG.", + "cannotResolveCollabServer": "Nelze se připojit ke sdílenému serveru. Prosím obnovte stránku a zkuste to znovu.", + "importLibraryError": "Nelze načíst knihovnu", + "collabSaveFailed": "Nelze uložit do databáze na serveru. Pokud problémy přetrvávají, měli byste uložit soubor lokálně, abyste se ujistili, že neztratíte svou práci.", + "collabSaveFailed_sizeExceeded": "Nelze uložit do databáze na serveru, plátno se zdá být příliš velké. Měli byste uložit soubor lokálně, abyste se ujistili, že neztratíte svou práci.", "brave_measure_text_error": { - "line1": "", - "line2": "", - "line3": "", - "line4": "" + "line1": "Vypadá to, že používáte Brave prohlížeč s povoleným nastavením Aggressively Block Fingerprinting.", + "line2": "To by mohlo vést k narušení Textových elementů ve vašich výkresech.", + "line3": "Důrazně doporučujeme zakázat toto nastavení. Můžete sledovat tyto kroky jak to udělat.", + "line4": "Pokud vypnutí tohoto nastavení neopravuje zobrazení textových prvků, prosím, otevřete problém na našem GitHubu, nebo nám napište na Discord" } }, "toolBar": { "selection": "Výběr", - "image": "", + "image": "Vložit obrázek", "rectangle": "Obdélník", "diamond": "Diamant", "ellipse": "Elipsa", @@ -216,69 +216,69 @@ "line": "Čára", "freedraw": "Kreslení", "text": "Text", - "library": "", - "lock": "", - "penMode": "", - "link": "", + "library": "Knihovna", + "lock": "Po kreslení ponechat vybraný nástroj aktivní", + "penMode": "Režim Pera - zabránit dotyku", + "link": "Přidat/aktualizovat odkaz pro vybraný tvar", "eraser": "Guma", - "hand": "" + "hand": "Ruka (nástroj pro posouvání)" }, "headings": { - "canvasActions": "", - "selectedShapeActions": "", + "canvasActions": "Akce plátna", + "selectedShapeActions": "Akce vybraného tvaru", "shapes": "Tvary" }, "hints": { - "canvasPanning": "", - "linearElement": "", - "freeDraw": "", - "text": "", - "text_selected": "", - "text_editing": "", - "linearElementMulti": "", - "lockAngle": "", - "resize": "", - "resizeImage": "", - "rotate": "", - "lineEditor_info": "", - "lineEditor_pointSelected": "", - "lineEditor_nothingSelected": "", - "placeImage": "", - "publishLibrary": "", - "bindTextToElement": "", - "deepBoxSelect": "", - "eraserRevert": "", - "firefox_clipboard_write": "" + "canvasPanning": "Chcete-li přesunout plátno, podržte kolečko nebo mezerník při tažení nebo použijte nástroj Ruka", + "linearElement": "Kliknutím pro více bodů, táhnutím pro jednu čáru", + "freeDraw": "Klikněte a táhněte, pro ukončení pusťte", + "text": "Tip: Text můžete také přidat dvojitým kliknutím kdekoli pomocí nástroje pro výběr", + "text_selected": "Dvojklikem nebo stisknutím klávesy ENTER upravíte text", + "text_editing": "Stiskněte Escape nebo Ctrl/Cmd+ENTER pro dokončení úprav", + "linearElementMulti": "Klikněte na poslední bod nebo stiskněte Escape anebo Enter pro dokončení", + "lockAngle": "Úhel můžete omezit podržením SHIFT", + "resize": "Můžete omezit proporce podržením SHIFT při změně velikosti,\npodržte ALT pro změnu velikosti od středu", + "resizeImage": "Můžete volně změnit velikost podržením SHIFT,\npodržením klávesy ALT změníte velikosti od středu", + "rotate": "Úhly můžete omezit podržením SHIFT při otáčení", + "lineEditor_info": "Podržte Ctrl/Cmd a dvakrát klikněte nebo stiskněte Ctrl/Cmd + Enter pro úpravu bodů", + "lineEditor_pointSelected": "Stisknutím tlačítka Delete odstraňte bod(y),\nCtrl/Cmd+D pro duplicitu nebo táhnutím pro přesun", + "lineEditor_nothingSelected": "Vyberte bod, který chcete upravit (podržením klávesy SHIFT vyberete více položek),\nnebo podržením klávesy Alt a kliknutím přidáte nové body", + "placeImage": "Kliknutím umístěte obrázek, nebo klepnutím a přetažením ručně nastavíte jeho velikost", + "publishLibrary": "Publikovat vlastní knihovnu", + "bindTextToElement": "Stiskněte Enter pro přidání textu", + "deepBoxSelect": "Podržte Ctrl/Cmd pro hluboký výběr a pro zabránění táhnutí", + "eraserRevert": "Podržením klávesy Alt vrátíte prvky označené pro smazání", + "firefox_clipboard_write": "Tato funkce může být povolena nastavením vlajky \"dom.events.asyncClipboard.clipboardItem\" na \"true\". Chcete-li změnit vlajky prohlížeče ve Firefoxu, navštivte stránku \"about:config\"." }, "canvasError": { - "cannotShowPreview": "", - "canvasTooBig": "", - "canvasTooBigTip": "" + "cannotShowPreview": "Náhled nelze zobrazit", + "canvasTooBig": "Plátno je možná příliš velké.", + "canvasTooBigTip": "Tip: zkus posunout nejvzdálenější prvky trochu blíže k sobě." }, "errorSplash": { - "headingMain": "", - "clearCanvasMessage": "", - "clearCanvasCaveat": "", + "headingMain": "Chyba. Zkuste .", + "clearCanvasMessage": "Pokud opětovné načtení nefunguje, zkuste .", + "clearCanvasCaveat": " To povede ke ztrátě dat ", "trackedToSentry": "Chyba identifikátoru {{eventId}} byl zaznamenán v našem systému.", - "openIssueMessage": "", - "sceneContent": "" + "openIssueMessage": "Byli jsme velmi opatrní, abychom neuváděli informace o Vaší scéně. Pokud vaše scéna není soukromá, zvažte prosím sledování na našem . Uveďte prosím níže uvedené informace kopírováním a vložením do problému na GitHubu.", + "sceneContent": "Obsah scény:" }, "roomDialog": { - "desc_intro": "", - "desc_privacy": "", - "button_startSession": "", - "button_stopSession": "", - "desc_inProgressIntro": "", - "desc_shareLink": "", - "desc_exitSession": "", - "shareTitle": "" + "desc_intro": "Můžete pozvat lidi na vaši aktuální scénu ke spolupráci s vámi.", + "desc_privacy": "Nebojte se, relace používá end-to-end šifrování, takže cokoliv nakreslíte zůstane soukromé. Ani náš server nebude schopen vidět, s čím budete pracovat.", + "button_startSession": "Zahájit relaci", + "button_stopSession": "Ukončit relaci", + "desc_inProgressIntro": "Živá spolupráce právě probíhá.", + "desc_shareLink": "Sdílejte tento odkaz s každým, s kým chcete spolupracovat:", + "desc_exitSession": "Zastavením relace se odpojíte od místnosti, ale budete moci pokračovat v práci s touto scénou lokálně. Všimněte si, že to nebude mít vliv na ostatní lidi a budou stále moci spolupracovat na jejich verzi.", + "shareTitle": "Připojte se k aktivní spolupráci na Excalidraw" }, "errorDialog": { - "title": "" + "title": "Chyba" }, "exportDialog": { - "disk_title": "", - "disk_details": "", + "disk_title": "Uložit na disk", + "disk_details": "Exportovat data scény do souboru, ze kterého můžete importovat později.", "disk_button": "Uložit do souboru", "link_title": "Odkaz pro sdílení", "link_details": "Exportovat jako odkaz pouze pro čtení.", @@ -290,18 +290,18 @@ "helpDialog": { "blog": "Přečtěte si náš blog", "click": "kliknutí", - "deepSelect": "", - "deepBoxSelect": "", + "deepSelect": "Hluboký výběr", + "deepBoxSelect": "Hluboký výběr uvnitř boxu a zabránění táhnnutí", "curvedArrow": "Zakřivená šipka", "curvedLine": "Zakřivená čára", "documentation": "Dokumentace", "doubleClick": "dvojklik", "drag": "tažení", "editor": "Editor", - "editLineArrowPoints": "", - "editText": "", - "github": "", - "howto": "", + "editLineArrowPoints": "Upravit body linií/šipek", + "editText": "Upravit text / přidat popis", + "github": "Našel jsi problém? Nahlaš ho", + "howto": "Sledujte naše návody", "or": "nebo", "preventBinding": "Zabránit vázání šipky", "tools": "Nástroje", @@ -313,8 +313,8 @@ "zoomToFit": "Přiblížit na zobrazení všech prvků", "zoomToSelection": "Přiblížit na výběr", "toggleElementLock": "Zamknout/odemknout výběr", - "movePageUpDown": "", - "movePageLeftRight": "" + "movePageUpDown": "Posunout stránku nahoru/dolů", + "movePageLeftRight": "Přesunout stránku doleva/doprava" }, "clearCanvasDialog": { "title": "Vymazat plátno" @@ -342,46 +342,46 @@ }, "noteDescription": "Odešlete svou knihovnu, pro zařazení do veřejného úložiště knihoven, odkud ji budou moci při kreslení využít i ostatní uživatelé.", "noteGuidelines": "Knihovna musí být nejdříve ručně schválena. Přečtěte si prosím pokyny", - "noteLicense": "", - "noteItems": "", - "atleastOneLibItem": "", - "republishWarning": "" + "noteLicense": "Odesláním souhlasíte s tím, že knihovna bude zveřejněna pod MIT licencí, stručně řečeno, kdokoli ji může používat bez omezení.", + "noteItems": "Každá položka knihovny musí mít svůj vlastní název, aby byla filtrovatelná. Následující položky knihovny budou zahrnuty:", + "atleastOneLibItem": "Vyberte alespoň jednu položku knihovny, kterou chcete začít", + "republishWarning": "Poznámka: některé z vybraných položek jsou označeny jako již zveřejněné/odeslané. Položky byste měli znovu odeslat pouze při aktualizaci existující knihovny nebo podání." }, "publishSuccessDialog": { "title": "Knihovna byla odeslána", - "content": "" + "content": "Děkujeme vám {{authorName}}. Vaše knihovna byla odeslána k posouzení. Stav můžete sledovat zde" }, "confirmDialog": { - "resetLibrary": "", - "removeItemsFromLib": "" + "resetLibrary": "Resetovat knihovnu", + "removeItemsFromLib": "Odstranit vybrané položky z knihovny" }, "imageExportDialog": { - "header": "", + "header": "Exportovat obrázek", "label": { - "withBackground": "", - "onlySelected": "", - "darkMode": "", - "embedScene": "", - "scale": "", - "padding": "" + "withBackground": "Pozadí", + "onlySelected": "Pouze vybrané", + "darkMode": "Tmavý režim", + "embedScene": "Vložit scénu", + "scale": "Měřítko", + "padding": "Odsazení" }, "tooltip": { - "embedScene": "" + "embedScene": "Data scény budou uložena do exportovaného souboru PNG/SVG tak, aby z něj mohla být scéna obnovena.\nZvýší se velikost exportovaného souboru." }, "title": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "Exportovat do PNG", + "exportToSvg": "Exportovat do SVG", + "copyPngToClipboard": "Kopírovat PNG do schránky" }, "button": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "PNG", + "exportToSvg": "SVG", + "copyPngToClipboard": "Kopírovat do schránky" } }, "encrypted": { - "tooltip": "", - "link": "" + "tooltip": "Vaše kresby jsou end-to-end šifrované, takže servery Excalidraw je nikdy neuvidí.", + "link": "Blog příspěvek na end-to-end šifrování v Excalidraw" }, "stats": { "angle": "Úhel", @@ -402,48 +402,48 @@ "addedToLibrary": "Přidáno do knihovny", "copyStyles": "Styly byly zkopírovány.", "copyToClipboard": "Zkopírováno do schránky.", - "copyToClipboardAsPng": "", + "copyToClipboardAsPng": "{{exportSelection}} zkopírován do schránky jako PNG\n({{exportColorScheme}})", "fileSaved": "Soubor byl uložen.", "fileSavedToFilename": "Uloženo do {filename}", "canvas": "plátno", "selection": "výběr", - "pasteAsSingleElement": "" + "pasteAsSingleElement": "Pomocí {{shortcut}} vložte jako jeden prvek,\nnebo vložte do existujícího textového editoru" }, "colors": { "transparent": "Průhledná", - "black": "", - "white": "", - "red": "", - "pink": "", - "grape": "", - "violet": "", - "gray": "", - "blue": "", - "cyan": "", - "teal": "", - "green": "", - "yellow": "", - "orange": "", - "bronze": "" + "black": "Černá", + "white": "Bílá", + "red": "Červená", + "pink": "Růžová", + "grape": "Vínová", + "violet": "Fialová", + "gray": "Šedá", + "blue": "Modrá", + "cyan": "Azurová", + "teal": "Modrozelená", + "green": "Zelená", + "yellow": "Žlutá", + "orange": "Oranžová", + "bronze": "Bronzová" }, "welcomeScreen": { "app": { - "center_heading": "", - "center_heading_plus": "", - "menuHint": "" + "center_heading": "Všechna vaše data jsou uložena lokálně ve vašem prohlížeči.", + "center_heading_plus": "Chcete místo toho přejít na Excalidraw+?", + "menuHint": "Export, nastavení, jazyky, ..." }, "defaults": { - "menuHint": "", - "center_heading": "", - "toolbarHint": "", - "helpHint": "" + "menuHint": "Export, nastavení a další...", + "center_heading": "Diagramy. Vytvořeny. Jednoduše.", + "toolbarHint": "Vyberte nástroj a začněte kreslit!", + "helpHint": "Zkratky a pomoc" } }, "colorPicker": { - "mostUsedCustomColors": "", - "colors": "", - "shades": "", - "hexCode": "", - "noShades": "" + "mostUsedCustomColors": "Nejpoužívanější vlastní barvy", + "colors": "Barvy", + "shades": "Stíny", + "hexCode": "Hex kód", + "noShades": "Pro tuto barvu nejsou k dispozici žádné odstíny" } } diff --git a/src/locales/de-DE.json b/src/locales/de-DE.json index 6832b3aa50..cbb0e7b530 100644 --- a/src/locales/de-DE.json +++ b/src/locales/de-DE.json @@ -124,7 +124,7 @@ }, "statusPublished": "Veröffentlicht", "sidebarLock": "Seitenleiste offen lassen", - "eyeDropper": "" + "eyeDropper": "Farbe von der Zeichenfläche auswählen" }, "library": { "noItems": "Noch keine Elemente hinzugefügt...", diff --git a/src/locales/hi-IN.json b/src/locales/hi-IN.json index 49f7ca702f..981af6556c 100644 --- a/src/locales/hi-IN.json +++ b/src/locales/hi-IN.json @@ -124,7 +124,7 @@ }, "statusPublished": "प्रकाशित", "sidebarLock": "साइडबार खुला रखे.", - "eyeDropper": "" + "eyeDropper": "चित्रफलक से रंग चुने" }, "library": { "noItems": "अभी तक कोई आइटम जोडा नहीं गया.", diff --git a/src/locales/ko-KR.json b/src/locales/ko-KR.json index 22a3ed1e8e..c63a3e9271 100644 --- a/src/locales/ko-KR.json +++ b/src/locales/ko-KR.json @@ -124,7 +124,7 @@ }, "statusPublished": "게시됨", "sidebarLock": "사이드바 유지", - "eyeDropper": "" + "eyeDropper": "캔버스에서 색상 고르기" }, "library": { "noItems": "추가된 아이템 없음", diff --git a/src/locales/mr-IN.json b/src/locales/mr-IN.json index 5dcebfcef5..018071e1de 100644 --- a/src/locales/mr-IN.json +++ b/src/locales/mr-IN.json @@ -124,7 +124,7 @@ }, "statusPublished": "प्रकाशित करा", "sidebarLock": "साइडबार उघडं ठेवा", - "eyeDropper": "" + "eyeDropper": "चित्रफलकातून रंग निवडा" }, "library": { "noItems": "अजून कोणतेही आइटम जोडलेले नाही...", diff --git a/src/locales/nb-NO.json b/src/locales/nb-NO.json index 5ba5f903d8..6f1dbb181d 100644 --- a/src/locales/nb-NO.json +++ b/src/locales/nb-NO.json @@ -124,7 +124,7 @@ }, "statusPublished": "Publisert", "sidebarLock": "Holde sidemenyen åpen", - "eyeDropper": "" + "eyeDropper": "Velg farge fra lerretet" }, "library": { "noItems": "Ingen elementer lagt til ennå...", diff --git a/src/locales/percentages.json b/src/locales/percentages.json index b4aaaa4433..da40808b65 100644 --- a/src/locales/percentages.json +++ b/src/locales/percentages.json @@ -1,11 +1,12 @@ { "ar-SA": 80, + "az-AZ": 20, "bg-BG": 54, "bn-BD": 60, "ca-ES": 88, - "cs-CZ": 64, + "cs-CZ": 100, "da-DK": 33, - "de-DE": 99, + "de-DE": 100, "el-GR": 93, "en": 100, "es-ES": 89, @@ -24,32 +25,32 @@ "kab-KAB": 88, "kk-KZ": 21, "km-KH": 95, - "ko-KR": 99, + "ko-KR": 100, "ku-TR": 90, "lt-LT": 56, "lv-LV": 89, - "mr-IN": 95, + "mr-IN": 96, "my-MM": 41, - "nb-NO": 99, + "nb-NO": 100, "nl-NL": 83, "nn-NO": 77, "oc-FR": 87, "pa-IN": 90, - "pl-PL": 99, + "pl-PL": 100, "pt-BR": 99, "pt-PT": 95, - "ro-RO": 99, - "ru-RU": 99, + "ro-RO": 100, + "ru-RU": 100, "si-LK": 9, - "sk-SK": 99, - "sl-SI": 99, - "sv-SE": 99, - "ta-IN": 83, + "sk-SK": 100, + "sl-SI": 100, + "sv-SE": 100, + "ta-IN": 85, "th-TH": 39, "tr-TR": 87, "uk-UA": 97, "vi-VN": 56, - "zh-CN": 95, + "zh-CN": 100, "zh-HK": 26, - "zh-TW": 99 + "zh-TW": 100 } diff --git a/src/locales/pl-PL.json b/src/locales/pl-PL.json index bd691da7e3..a323bc606c 100644 --- a/src/locales/pl-PL.json +++ b/src/locales/pl-PL.json @@ -124,7 +124,7 @@ }, "statusPublished": "Opublikowano", "sidebarLock": "Panel boczny zawsze otwarty", - "eyeDropper": "" + "eyeDropper": "Wybierz kolor z płótna" }, "library": { "noItems": "Nie dodano jeszcze żadnych elementów...", diff --git a/src/locales/pt-PT.json b/src/locales/pt-PT.json index a5f3cd223d..5f41bdc361 100644 --- a/src/locales/pt-PT.json +++ b/src/locales/pt-PT.json @@ -363,7 +363,7 @@ "darkMode": "", "embedScene": "Cena embutida", "scale": "", - "padding": "" + "padding": "Espaçamento" }, "tooltip": { "embedScene": "" diff --git a/src/locales/ro-RO.json b/src/locales/ro-RO.json index 84e79a67d9..bbc7f70b1f 100644 --- a/src/locales/ro-RO.json +++ b/src/locales/ro-RO.json @@ -124,7 +124,7 @@ }, "statusPublished": "Publicat", "sidebarLock": "Păstrează deschisă bara laterală", - "eyeDropper": "" + "eyeDropper": "Alegere culoare din pânză" }, "library": { "noItems": "Niciun element adăugat încă...", diff --git a/src/locales/ru-RU.json b/src/locales/ru-RU.json index 80aad867d7..a87d715e75 100644 --- a/src/locales/ru-RU.json +++ b/src/locales/ru-RU.json @@ -124,7 +124,7 @@ }, "statusPublished": "Опубликовано", "sidebarLock": "Держать боковую панель открытой", - "eyeDropper": "" + "eyeDropper": "Взять образец цвета с холста" }, "library": { "noItems": "Пока ничего не добавлено...", diff --git a/src/locales/sk-SK.json b/src/locales/sk-SK.json index 00afadaae8..3a2bad6900 100644 --- a/src/locales/sk-SK.json +++ b/src/locales/sk-SK.json @@ -124,7 +124,7 @@ }, "statusPublished": "Zverejnené", "sidebarLock": "Nechať bočný panel otvorený", - "eyeDropper": "" + "eyeDropper": "Vybrať farbu z plátna" }, "library": { "noItems": "Zatiaľ neboli pridané žiadne položky...", diff --git a/src/locales/sl-SI.json b/src/locales/sl-SI.json index d9888a0110..3074132cac 100644 --- a/src/locales/sl-SI.json +++ b/src/locales/sl-SI.json @@ -124,7 +124,7 @@ }, "statusPublished": "Objavljeno", "sidebarLock": "Obdrži stransko vrstico odprto", - "eyeDropper": "" + "eyeDropper": "Izberi barvo s platna" }, "library": { "noItems": "Dodan še ni noben element...", diff --git a/src/locales/sv-SE.json b/src/locales/sv-SE.json index 64b2f1acf1..cbade17213 100644 --- a/src/locales/sv-SE.json +++ b/src/locales/sv-SE.json @@ -124,7 +124,7 @@ }, "statusPublished": "Publicerad", "sidebarLock": "Håll sidofältet öppet", - "eyeDropper": "" + "eyeDropper": "Välj färg från canvas" }, "library": { "noItems": "Inga objekt tillagda ännu...", diff --git a/src/locales/ta-IN.json b/src/locales/ta-IN.json index 89cc827168..8fa74c801d 100644 --- a/src/locales/ta-IN.json +++ b/src/locales/ta-IN.json @@ -50,7 +50,7 @@ "veryLarge": "மிகப் பெரிய", "solid": "திடமான", "hachure": "மலைக்குறிக்கோடு", - "zigzag": "", + "zigzag": "கோணல்மாணல்", "crossHatch": "குறுக்குகதவு", "thin": "மெல்லிய", "bold": "பட்டை", @@ -105,8 +105,8 @@ "decreaseFontSize": "எழுத்துரு அளவைக் குறை", "increaseFontSize": "எழுத்துரு அளவை அதிகரி", "unbindText": "உரையைப் பிணைவவிழ்", - "bindText": "", - "createContainerFromText": "", + "bindText": "உரையைக் கொள்கலனுக்குப் பிணை", + "createContainerFromText": "உரையைக் கொள்கலனுள் சுருட்டு", "link": { "edit": "தொடுப்பைத் திருத்து", "create": "தொடுப்பைப் படை", @@ -114,7 +114,7 @@ }, "lineEditor": { "edit": "தொடுப்பைத் திருத்து", - "exit": "" + "exit": "வரி திருத்தியிலிருந்து வெளியேறு" }, "elementLock": { "lock": "பூட்டு", @@ -124,7 +124,7 @@ }, "statusPublished": "வெளியிடப்பட்டது", "sidebarLock": "பக்கப்பட்டையைத் திறந்தே வை", - "eyeDropper": "" + "eyeDropper": "கித்தானிலிருந்து நிறம் தேர்ந்தெடு" }, "library": { "noItems": "இதுவரை உருப்படிகள் சேரக்கப்படவில்லை...", @@ -134,7 +134,7 @@ "buttons": { "clearReset": "கித்தானை அகரமாக்கு", "exportJSON": "கோப்புக்கு ஏற்றுமதிசெய்", - "exportImage": "", + "exportImage": "படத்தை ஏற்றுமதிசெய்...", "export": "இதில் சேமி...", "copyToClipboard": "நகலகத்திற்கு நகலெடு", "save": "தற்போதைய கோப்புக்குச் சேமி", @@ -187,7 +187,7 @@ "resetLibrary": "இது உங்கள் நுலகத்தைத் துடைக்கும். நீங்கள் உறுதியா?", "removeItemsFromsLibrary": "{{count}} உருப்படி(கள்)-ஐ உம் நூலகத்திலிருந்து அழிக்கவா?", "invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது.", - "collabOfflineWarning": "" + "collabOfflineWarning": "இணைய இணைப்பு இல்லை.\nஉமது மாற்றங்கள் சேமிக்கப்படா!" }, "errors": { "unsupportedFileType": "ஆதரிக்கப்படா கோப்பு வகை.", @@ -195,10 +195,10 @@ "fileTooBig": "கோப்பு மிகப்பெரிது. அனுமதிக்கப்பட்ட அதிகபட்ச அளவு {{maxSize}}.", "svgImageInsertError": "எஸ்விஜி படத்தைப் புகுத்தவியலா. எஸ்விஜியின் மார்க்அப் செல்லாததாக தெரிகிறது.", "invalidSVGString": "செல்லாத SVG.", - "cannotResolveCollabServer": "", + "cannotResolveCollabServer": "கூட்டுப்பணிச் சேவையகத்துடன் இணைக்க முடியவில்லை. பக்கத்தை மீளேற்றி மீண்டும் முயலவும்.", "importLibraryError": "நூலகத்தை ஏற்ற முடியவில்லை", - "collabSaveFailed": "", - "collabSaveFailed_sizeExceeded": "", + "collabSaveFailed": "பின்முனை தரவுத்தளத்தில் சேமிக்க முடியவில்லை. சிக்கல்கள் நீடித்தால், உமது வேலைகளை இழக்காமலிருப்பதை உறுதிசெய்ய உமது கோப்பை உள்ளகத்தில் சேமிக்க வேண்டும்.", + "collabSaveFailed_sizeExceeded": "பின்முனை தரவுத்தளத்தில் சேமிக்க முடியவில்லை, கித்தான் மிகப்பெரிதாகத் தெரிகிறது. உமது வேலைகளை இழக்காமலிருப்பதை உறுதிசெய்ய உமது கோப்பை உள்ளகத்தில் சேமிக்க வேண்டும்.", "brave_measure_text_error": { "line1": "", "line2": "", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 0cb91c6601..9a8c3db370 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -124,7 +124,7 @@ }, "statusPublished": "已发布", "sidebarLock": "侧边栏常驻", - "eyeDropper": "" + "eyeDropper": "从画布上取色" }, "library": { "noItems": "尚未添加任何项目……", @@ -266,12 +266,12 @@ "roomDialog": { "desc_intro": "你可以邀请其他人到目前的画面中与你协作。", "desc_privacy": "别担心,该会话使用端到端加密,无论绘制什么都将保持私密,甚至连我们的服务器也无法查看。", - "button_startSession": "启动会议", - "button_stopSession": "结束会议", - "desc_inProgressIntro": "实时协作会议正在进行。", + "button_startSession": "开始会话", + "button_stopSession": "结束会话", + "desc_inProgressIntro": "实时协作会话进行中。", "desc_shareLink": "分享此链接给你要协作的用户", - "desc_exitSession": "停止会话将中断您在与房间的连接,但您依然可以在本地继续使用画布. 请注意,这不会影响到其他用户, 他们仍可以在他们的版本上继续协作。", - "shareTitle": "加入 Excalidraw 实时协作会议" + "desc_exitSession": "停止会话将中断您与房间的连接,但您依然可以在本地继续使用画布。请注意,这不会影响到其他用户,他们仍可以在他们的版本上继续协作。", + "shareTitle": "加入 Excalidraw 实时协作会话" }, "errorDialog": { "title": "错误" @@ -356,27 +356,27 @@ "removeItemsFromLib": "从素材库中删除选中的项目" }, "imageExportDialog": { - "header": "", + "header": "导出图片", "label": { - "withBackground": "", - "onlySelected": "", - "darkMode": "", - "embedScene": "", - "scale": "", - "padding": "" + "withBackground": "背景", + "onlySelected": "仅选中", + "darkMode": "深色模式", + "embedScene": "包含画布数据", + "scale": "缩放比例", + "padding": "内边距" }, "tooltip": { - "embedScene": "" + "embedScene": "画布数据将被保存到导出的 PNG/SVG 文件,以便恢复。\n将会增加导出文件的大小。" }, "title": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "导出为 PNG", + "exportToSvg": "导出为 SVG", + "copyPngToClipboard": "复制 PNG 到剪切板" }, "button": { - "exportToPng": "", - "exportToSvg": "", - "copyPngToClipboard": "" + "exportToPng": "PNG", + "exportToSvg": "SVG", + "copyPngToClipboard": "复制到剪贴板" } }, "encrypted": { diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index f0528e546f..547ba247dc 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -124,7 +124,7 @@ }, "statusPublished": "已發布", "sidebarLock": "側欄維持開啟", - "eyeDropper": "" + "eyeDropper": "從畫布中選取顏色" }, "library": { "noItems": "尚未加入任何物件...", From 74d2fc6406c930df2f5823de0318346c10a8c7a5 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Mon, 12 Jun 2023 17:43:31 +0200 Subject: [PATCH 08/27] fix: collab username style fixes (#6668) --- src/renderer/renderScene.ts | 9 ++------- src/renderer/roundRect.ts | 4 ++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 53c4f387f8..07c5649c08 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -743,12 +743,7 @@ export const _renderScene = ({ context.strokeStyle = oc.white; context.stroke(); } else { - // Border - context.fillStyle = oc.white; - context.fillRect(boxX, boxY, boxWidth, boxHeight); - // Background - context.fillStyle = background; - context.fillRect(offsetX, offsetY, boxWidth - 2, boxHeight - 2); + roundRect(context, boxX, boxY, boxWidth, boxHeight, 8, oc.white); } context.fillStyle = oc.black; @@ -759,7 +754,7 @@ export const _renderScene = ({ paddingVertical + measure.actualBoundingBoxAscent + Math.floor((finalHeight - measureHeight) / 2) + - 1, + 2, ); } diff --git a/src/renderer/roundRect.ts b/src/renderer/roundRect.ts index be842a5228..bbb98306d1 100644 --- a/src/renderer/roundRect.ts +++ b/src/renderer/roundRect.ts @@ -15,6 +15,7 @@ export const roundRect = ( width: number, height: number, radius: number, + strokeColor?: string, ) => { context.beginPath(); context.moveTo(x + radius, y); @@ -33,5 +34,8 @@ export const roundRect = ( context.quadraticCurveTo(x, y, x + radius, y); context.closePath(); context.fill(); + if (strokeColor) { + context.strokeStyle = strokeColor; + } context.stroke(); }; From 3bd5d87cac803098a2e33237a51e5f805b34be5a Mon Sep 17 00:00:00 2001 From: Arnost Pleskot Date: Mon, 12 Jun 2023 17:44:31 +0200 Subject: [PATCH 09/27] feat: disable collab feature when running in iframe (#6646) Co-authored-by: dwelle --- src/excalidraw-app/collab/Collab.tsx | 3 +- src/excalidraw-app/components/AppMainMenu.tsx | 11 ++++--- .../components/AppWelcomeScreen.tsx | 9 +++-- src/excalidraw-app/index.tsx | 33 ++++++++++++------- src/utils.ts | 2 ++ 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/excalidraw-app/collab/Collab.tsx b/src/excalidraw-app/collab/Collab.tsx index 76d07bb9f3..ec0c2a3483 100644 --- a/src/excalidraw-app/collab/Collab.tsx +++ b/src/excalidraw-app/collab/Collab.tsx @@ -157,6 +157,8 @@ class Collab extends PureComponent { window.addEventListener("offline", this.onOfflineStatusToggle); window.addEventListener(EVENT.UNLOAD, this.onUnload); + this.onOfflineStatusToggle(); + const collabAPI: CollabAPI = { isCollaborating: this.isCollaborating, onPointerUpdate: this.onPointerUpdate, @@ -168,7 +170,6 @@ class Collab extends PureComponent { }; appJotaiStore.set(collabAPIAtom, collabAPI); - this.onOfflineStatusToggle(); if ( process.env.NODE_ENV === ENV.TEST || diff --git a/src/excalidraw-app/components/AppMainMenu.tsx b/src/excalidraw-app/components/AppMainMenu.tsx index b1c0771e54..7b562f8b7d 100644 --- a/src/excalidraw-app/components/AppMainMenu.tsx +++ b/src/excalidraw-app/components/AppMainMenu.tsx @@ -6,6 +6,7 @@ import { LanguageList } from "./LanguageList"; export const AppMainMenu: React.FC<{ setCollabDialogShown: (toggle: boolean) => any; isCollaborating: boolean; + isCollabEnabled: boolean; }> = React.memo((props) => { return ( @@ -13,10 +14,12 @@ export const AppMainMenu: React.FC<{ - props.setCollabDialogShown(true)} - /> + {props.isCollabEnabled && ( + props.setCollabDialogShown(true)} + /> + )} diff --git a/src/excalidraw-app/components/AppWelcomeScreen.tsx b/src/excalidraw-app/components/AppWelcomeScreen.tsx index 1e34fa8192..80fedf9faf 100644 --- a/src/excalidraw-app/components/AppWelcomeScreen.tsx +++ b/src/excalidraw-app/components/AppWelcomeScreen.tsx @@ -6,6 +6,7 @@ import { isExcalidrawPlusSignedUser } from "../app_constants"; export const AppWelcomeScreen: React.FC<{ setCollabDialogShown: (toggle: boolean) => any; + isCollabEnabled: boolean; }> = React.memo((props) => { const { t } = useI18n(); let headingContent; @@ -46,9 +47,11 @@ export const AppWelcomeScreen: React.FC<{ - props.setCollabDialogShown(true)} - /> + {props.isCollabEnabled && ( + props.setCollabDialogShown(true)} + /> + )} {!isExcalidrawPlusSignedUser && ( { const [errorMessage, setErrorMessage] = useState(""); const [langCode, setLangCode] = useAtom(appLangCodeAtom); + const isCollabDisabled = isRunningInIframe(); // initial state // --------------------------------------------------------------------------- @@ -272,7 +274,7 @@ const ExcalidrawWrapper = () => { }); useEffect(() => { - if (!collabAPI || !excalidrawAPI) { + if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) { return; } @@ -283,7 +285,7 @@ const ExcalidrawWrapper = () => { if (!data.scene) { return; } - if (collabAPI.isCollaborating()) { + if (collabAPI?.isCollaborating()) { if (data.scene.elements) { collabAPI .fetchImageFilesFromFirebase({ @@ -353,7 +355,7 @@ const ExcalidrawWrapper = () => { const libraryUrlTokens = parseLibraryTokensFromUrl(); if (!libraryUrlTokens) { if ( - collabAPI.isCollaborating() && + collabAPI?.isCollaborating() && !isCollaborationLink(window.location.href) ) { collabAPI.stopCollaboration(false); @@ -382,7 +384,10 @@ const ExcalidrawWrapper = () => { if (isTestEnv()) { return; } - if (!document.hidden && !collabAPI.isCollaborating()) { + if ( + !document.hidden && + ((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled) + ) { // don't sync if local state is newer or identical to browser state if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) { const localDataState = importFromLocalStorage(); @@ -398,7 +403,7 @@ const ExcalidrawWrapper = () => { excalidrawAPI.updateLibrary({ libraryItems: getLibraryItemsFromStorage(), }); - collabAPI.setUsername(username || ""); + collabAPI?.setUsername(username || ""); } if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) { @@ -466,7 +471,7 @@ const ExcalidrawWrapper = () => { ); clearTimeout(titleTimeout); }; - }, [collabAPI, excalidrawAPI, setLangCode]); + }, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]); useEffect(() => { const unloadHandler = (event: BeforeUnloadEvent) => { @@ -649,7 +654,7 @@ const ExcalidrawWrapper = () => { autoFocus={true} theme={theme} renderTopRightUI={(isMobile) => { - if (isMobile) { + if (isMobile || !collabAPI || isCollabDisabled) { return null; } return ( @@ -663,15 +668,21 @@ const ExcalidrawWrapper = () => { + - {isCollaborating && isOffline && (
{t("alerts.collabOfflineWarning")}
)} - {excalidrawAPI && } + {excalidrawAPI && !isCollabDisabled && ( + + )} {errorMessage && ( setErrorMessage("")}> diff --git a/src/utils.ts b/src/utils.ts index 98dfb48d71..c1be211bd5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -748,6 +748,8 @@ export const getFrame = () => { } }; +export const isRunningInIframe = () => getFrame() === "iframe"; + export const isPromiseLike = ( value: any, ): value is Promise> => { From 1747e939572c308ffc50f42f4de9426173e41967 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 13 Jun 2023 16:34:24 +0200 Subject: [PATCH 10/27] feat: polyfill `CanvasRenderingContext2D.roundRect` (#6673) --- src/renderer/renderScene.ts | 1 + src/renderer/roundRect.polyfill.ts | 356 +++++++++++++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 src/renderer/roundRect.polyfill.ts diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 07c5649c08..85e9eab18e 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -56,6 +56,7 @@ import { getLinkHandleFromCoords, } from "../element/Hyperlink"; import { isLinearElement } from "../element/typeChecks"; +import "./roundRect.polyfill"; export const DEFAULT_SPACING = 2; diff --git a/src/renderer/roundRect.polyfill.ts b/src/renderer/roundRect.polyfill.ts new file mode 100644 index 0000000000..10946a46d6 --- /dev/null +++ b/src/renderer/roundRect.polyfill.ts @@ -0,0 +1,356 @@ +// @ts-nocheck + +// source: https://github.com/Kaiido/roundRect/ +// rewritten to remove globalThis and Nullish coalescing assignment operator + +export {}; + +/* + * Implements the .roundRect() method of the CanvasPath mixin + * as introduced by https://github.com/whatwg/html/pull/6765 + */ +(() => { + Path2D.prototype.roundRect ??= roundRect; + if ( + typeof window !== "undefined" && + window.CanvasRenderingContext2D && + !window.CanvasRenderingContext2D.prototype.roundRect + ) { + window.CanvasRenderingContext2D.prototype.roundRect = roundRect; + } + if ( + typeof window !== "undefined" && + window.OffscreenCanvasRenderingContext2D && + !window.OffscreenCanvasRenderingContext2D.prototype.roundRect + ) { + window.OffscreenCanvasRenderingContext2D.prototype.roundRect = roundRect; + } + + function roundRect(x, y, w, h, radii) { + if (![x, y, w, h].every((input) => Number.isFinite(input))) { + return; + } + + radii = parseRadiiArgument(radii); + + let upperLeft; + let upperRight; + let lowerRight; + let lowerLeft; + + if (radii.length === 4) { + upperLeft = toCornerPoint(radii[0]); + upperRight = toCornerPoint(radii[1]); + lowerRight = toCornerPoint(radii[2]); + lowerLeft = toCornerPoint(radii[3]); + } else if (radii.length === 3) { + upperLeft = toCornerPoint(radii[0]); + upperRight = toCornerPoint(radii[1]); + lowerLeft = toCornerPoint(radii[1]); + lowerRight = toCornerPoint(radii[2]); + } else if (radii.length === 2) { + upperLeft = toCornerPoint(radii[0]); + lowerRight = toCornerPoint(radii[0]); + upperRight = toCornerPoint(radii[1]); + lowerLeft = toCornerPoint(radii[1]); + } else if (radii.length === 1) { + upperLeft = toCornerPoint(radii[0]); + upperRight = toCornerPoint(radii[0]); + lowerRight = toCornerPoint(radii[0]); + lowerLeft = toCornerPoint(radii[0]); + } else { + throw new RangeError( + `${getErrorMessageHeader(this)} ${ + radii.length + } is not a valid size for radii sequence.`, + ); + } + + const corners = [upperLeft, upperRight, lowerRight, lowerLeft]; + const negativeCorner = corners.find(({ x, y }) => x < 0 || y < 0); + + if ( + corners.some(({ x, y }) => !Number.isFinite(x) || !Number.isFinite(y)) + ) { + return; + } + + if (negativeCorner) { + throw new RangeError( + `${getErrorMessageHeader( + this, + )} Radius value ${negativeCorner} is negative.`, + ); + } + + fixOverlappingCorners(corners); + + if (w < 0 && h < 0) { + this.moveTo(x - upperLeft.x, y); + this.ellipse( + x + w + upperRight.x, + y - upperRight.y, + upperRight.x, + upperRight.y, + 0, + -Math.PI * 1.5, + -Math.PI, + ); + this.ellipse( + x + w + lowerRight.x, + y + h + lowerRight.y, + lowerRight.x, + lowerRight.y, + 0, + -Math.PI, + -Math.PI / 2, + ); + this.ellipse( + x - lowerLeft.x, + y + h + lowerLeft.y, + lowerLeft.x, + lowerLeft.y, + 0, + -Math.PI / 2, + 0, + ); + this.ellipse( + x - upperLeft.x, + y - upperLeft.y, + upperLeft.x, + upperLeft.y, + 0, + 0, + -Math.PI / 2, + ); + } else if (w < 0) { + this.moveTo(x - upperLeft.x, y); + this.ellipse( + x + w + upperRight.x, + y + upperRight.y, + upperRight.x, + upperRight.y, + 0, + -Math.PI / 2, + -Math.PI, + 1, + ); + this.ellipse( + x + w + lowerRight.x, + y + h - lowerRight.y, + lowerRight.x, + lowerRight.y, + 0, + -Math.PI, + -Math.PI * 1.5, + 1, + ); + this.ellipse( + x - lowerLeft.x, + y + h - lowerLeft.y, + lowerLeft.x, + lowerLeft.y, + 0, + Math.PI / 2, + 0, + 1, + ); + this.ellipse( + x - upperLeft.x, + y + upperLeft.y, + upperLeft.x, + upperLeft.y, + 0, + 0, + -Math.PI / 2, + 1, + ); + } else if (h < 0) { + this.moveTo(x + upperLeft.x, y); + this.ellipse( + x + w - upperRight.x, + y - upperRight.y, + upperRight.x, + upperRight.y, + 0, + Math.PI / 2, + 0, + 1, + ); + this.ellipse( + x + w - lowerRight.x, + y + h + lowerRight.y, + lowerRight.x, + lowerRight.y, + 0, + 0, + -Math.PI / 2, + 1, + ); + this.ellipse( + x + lowerLeft.x, + y + h + lowerLeft.y, + lowerLeft.x, + lowerLeft.y, + 0, + -Math.PI / 2, + -Math.PI, + 1, + ); + this.ellipse( + x + upperLeft.x, + y - upperLeft.y, + upperLeft.x, + upperLeft.y, + 0, + -Math.PI, + -Math.PI * 1.5, + 1, + ); + } else { + this.moveTo(x + upperLeft.x, y); + this.ellipse( + x + w - upperRight.x, + y + upperRight.y, + upperRight.x, + upperRight.y, + 0, + -Math.PI / 2, + 0, + ); + this.ellipse( + x + w - lowerRight.x, + y + h - lowerRight.y, + lowerRight.x, + lowerRight.y, + 0, + 0, + Math.PI / 2, + ); + this.ellipse( + x + lowerLeft.x, + y + h - lowerLeft.y, + lowerLeft.x, + lowerLeft.y, + 0, + Math.PI / 2, + Math.PI, + ); + this.ellipse( + x + upperLeft.x, + y + upperLeft.y, + upperLeft.x, + upperLeft.y, + 0, + Math.PI, + Math.PI * 1.5, + ); + } + + this.closePath(); + this.moveTo(x, y); + + function toDOMPointInit(value) { + const { x, y, z, w } = value; + return { x, y, z, w }; + } + + function parseRadiiArgument(value) { + // https://webidl.spec.whatwg.org/#es-union + // with 'optional (unrestricted double or DOMPointInit + // or sequence<(unrestricted double or DOMPointInit)>) radii = 0' + const type = typeof value; + + if (type === "undefined" || value === null) { + return [0]; + } + if (type === "function") { + return [NaN]; + } + if (type === "object") { + if (typeof value[Symbol.iterator] === "function") { + return [...value].map((elem) => { + // https://webidl.spec.whatwg.org/#es-union + // with '(unrestricted double or DOMPointInit)' + const elemType = typeof elem; + if (elemType === "undefined" || elem === null) { + return 0; + } + if (elemType === "function") { + return NaN; + } + if (elemType === "object") { + return toDOMPointInit(elem); + } + return toUnrestrictedNumber(elem); + }); + } + + return [toDOMPointInit(value)]; + } + + return [toUnrestrictedNumber(value)]; + } + + function toUnrestrictedNumber(value) { + return +value; + } + + function toCornerPoint(value) { + const asNumber = toUnrestrictedNumber(value); + if (Number.isFinite(asNumber)) { + return { + x: asNumber, + y: asNumber, + }; + } + if (Object(value) === value) { + return { + x: toUnrestrictedNumber(value.x ?? 0), + y: toUnrestrictedNumber(value.y ?? 0), + }; + } + + return { + x: NaN, + y: NaN, + }; + } + + function fixOverlappingCorners(corners) { + const [upperLeft, upperRight, lowerRight, lowerLeft] = corners; + const factors = [ + Math.abs(w) / (upperLeft.x + upperRight.x), + Math.abs(h) / (upperRight.y + lowerRight.y), + Math.abs(w) / (lowerRight.x + lowerLeft.x), + Math.abs(h) / (upperLeft.y + lowerLeft.y), + ]; + const minFactor = Math.min(...factors); + if (minFactor <= 1) { + for (const radii of corners) { + radii.x *= minFactor; + radii.y *= minFactor; + } + } + } + } + + function getErrorMessageHeader(instance) { + return `Failed to execute 'roundRect' on '${getConstructorName( + instance, + )}':`; + } + + function getConstructorName(instance) { + if (typeof window === "undefined") { + return "UNKNOWN"; + } + return Object(instance) === instance && instance instanceof Path2D + ? "Path2D" + : instance instanceof window?.CanvasRenderingContext2D + ? "CanvasRenderingContext2D" + : instance instanceof window?.OffscreenCanvasRenderingContext2D + ? "OffscreenCanvasRenderingContext2D" + : instance?.constructor.name || instance; + } +})(); From 4d7d96eb7b444f60bb0bb2ce1aedaf047d1df8cb Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 14 Jun 2023 17:26:29 +0530 Subject: [PATCH 11/27] feat: add canvas-roundrect-polyfill package (#6675) * feat: add canvas-roundrect-polyfill instead of maintaining a copy of it and transplile it since its not transpiled in the package * transform canvas-roundrect-polyfill in jest --- package.json | 3 +- src/packages/excalidraw/webpack.dev.config.js | 3 +- .../excalidraw/webpack.prod.config.js | 4 +- src/renderer/renderScene.ts | 2 +- src/renderer/roundRect.polyfill.ts | 356 ------------------ yarn.lock | 5 + 6 files changed, 13 insertions(+), 360 deletions(-) delete mode 100644 src/renderer/roundRect.polyfill.ts diff --git a/package.json b/package.json index b5432d37bf..584bdcbc4c 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@testing-library/react": "12.1.5", "@tldraw/vec": "1.7.1", "browser-fs-access": "0.29.1", + "canvas-roundrect-polyfill": "0.0.1", "clsx": "1.1.1", "cross-env": "7.0.3", "fake-indexeddb": "3.1.7", @@ -107,7 +108,7 @@ "/src/packages/excalidraw/example" ], "transformIgnorePatterns": [ - "node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)" + "node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access|canvas-roundrect-polyfill)/)" ], "resetMocks": false }, diff --git a/src/packages/excalidraw/webpack.dev.config.js b/src/packages/excalidraw/webpack.dev.config.js index 9e8180f5f3..e16d8da729 100644 --- a/src/packages/excalidraw/webpack.dev.config.js +++ b/src/packages/excalidraw/webpack.dev.config.js @@ -44,7 +44,8 @@ module.exports = { }, { test: /\.(ts|tsx|js|jsx|mjs)$/, - exclude: /node_modules\/(?!browser-fs-access)/, + exclude: + /node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/, use: [ { loader: "ts-loader", diff --git a/src/packages/excalidraw/webpack.prod.config.js b/src/packages/excalidraw/webpack.prod.config.js index 0450d36fab..0a1e63618e 100644 --- a/src/packages/excalidraw/webpack.prod.config.js +++ b/src/packages/excalidraw/webpack.prod.config.js @@ -46,7 +46,9 @@ module.exports = { }, { test: /\.(ts|tsx|js|jsx|mjs)$/, - exclude: /node_modules\/(?!browser-fs-access)/, + exclude: + /node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/, + use: [ { loader: "ts-loader", diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 85e9eab18e..16ca3eb172 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -56,7 +56,7 @@ import { getLinkHandleFromCoords, } from "../element/Hyperlink"; import { isLinearElement } from "../element/typeChecks"; -import "./roundRect.polyfill"; +import "canvas-roundrect-polyfill"; export const DEFAULT_SPACING = 2; diff --git a/src/renderer/roundRect.polyfill.ts b/src/renderer/roundRect.polyfill.ts deleted file mode 100644 index 10946a46d6..0000000000 --- a/src/renderer/roundRect.polyfill.ts +++ /dev/null @@ -1,356 +0,0 @@ -// @ts-nocheck - -// source: https://github.com/Kaiido/roundRect/ -// rewritten to remove globalThis and Nullish coalescing assignment operator - -export {}; - -/* - * Implements the .roundRect() method of the CanvasPath mixin - * as introduced by https://github.com/whatwg/html/pull/6765 - */ -(() => { - Path2D.prototype.roundRect ??= roundRect; - if ( - typeof window !== "undefined" && - window.CanvasRenderingContext2D && - !window.CanvasRenderingContext2D.prototype.roundRect - ) { - window.CanvasRenderingContext2D.prototype.roundRect = roundRect; - } - if ( - typeof window !== "undefined" && - window.OffscreenCanvasRenderingContext2D && - !window.OffscreenCanvasRenderingContext2D.prototype.roundRect - ) { - window.OffscreenCanvasRenderingContext2D.prototype.roundRect = roundRect; - } - - function roundRect(x, y, w, h, radii) { - if (![x, y, w, h].every((input) => Number.isFinite(input))) { - return; - } - - radii = parseRadiiArgument(radii); - - let upperLeft; - let upperRight; - let lowerRight; - let lowerLeft; - - if (radii.length === 4) { - upperLeft = toCornerPoint(radii[0]); - upperRight = toCornerPoint(radii[1]); - lowerRight = toCornerPoint(radii[2]); - lowerLeft = toCornerPoint(radii[3]); - } else if (radii.length === 3) { - upperLeft = toCornerPoint(radii[0]); - upperRight = toCornerPoint(radii[1]); - lowerLeft = toCornerPoint(radii[1]); - lowerRight = toCornerPoint(radii[2]); - } else if (radii.length === 2) { - upperLeft = toCornerPoint(radii[0]); - lowerRight = toCornerPoint(radii[0]); - upperRight = toCornerPoint(radii[1]); - lowerLeft = toCornerPoint(radii[1]); - } else if (radii.length === 1) { - upperLeft = toCornerPoint(radii[0]); - upperRight = toCornerPoint(radii[0]); - lowerRight = toCornerPoint(radii[0]); - lowerLeft = toCornerPoint(radii[0]); - } else { - throw new RangeError( - `${getErrorMessageHeader(this)} ${ - radii.length - } is not a valid size for radii sequence.`, - ); - } - - const corners = [upperLeft, upperRight, lowerRight, lowerLeft]; - const negativeCorner = corners.find(({ x, y }) => x < 0 || y < 0); - - if ( - corners.some(({ x, y }) => !Number.isFinite(x) || !Number.isFinite(y)) - ) { - return; - } - - if (negativeCorner) { - throw new RangeError( - `${getErrorMessageHeader( - this, - )} Radius value ${negativeCorner} is negative.`, - ); - } - - fixOverlappingCorners(corners); - - if (w < 0 && h < 0) { - this.moveTo(x - upperLeft.x, y); - this.ellipse( - x + w + upperRight.x, - y - upperRight.y, - upperRight.x, - upperRight.y, - 0, - -Math.PI * 1.5, - -Math.PI, - ); - this.ellipse( - x + w + lowerRight.x, - y + h + lowerRight.y, - lowerRight.x, - lowerRight.y, - 0, - -Math.PI, - -Math.PI / 2, - ); - this.ellipse( - x - lowerLeft.x, - y + h + lowerLeft.y, - lowerLeft.x, - lowerLeft.y, - 0, - -Math.PI / 2, - 0, - ); - this.ellipse( - x - upperLeft.x, - y - upperLeft.y, - upperLeft.x, - upperLeft.y, - 0, - 0, - -Math.PI / 2, - ); - } else if (w < 0) { - this.moveTo(x - upperLeft.x, y); - this.ellipse( - x + w + upperRight.x, - y + upperRight.y, - upperRight.x, - upperRight.y, - 0, - -Math.PI / 2, - -Math.PI, - 1, - ); - this.ellipse( - x + w + lowerRight.x, - y + h - lowerRight.y, - lowerRight.x, - lowerRight.y, - 0, - -Math.PI, - -Math.PI * 1.5, - 1, - ); - this.ellipse( - x - lowerLeft.x, - y + h - lowerLeft.y, - lowerLeft.x, - lowerLeft.y, - 0, - Math.PI / 2, - 0, - 1, - ); - this.ellipse( - x - upperLeft.x, - y + upperLeft.y, - upperLeft.x, - upperLeft.y, - 0, - 0, - -Math.PI / 2, - 1, - ); - } else if (h < 0) { - this.moveTo(x + upperLeft.x, y); - this.ellipse( - x + w - upperRight.x, - y - upperRight.y, - upperRight.x, - upperRight.y, - 0, - Math.PI / 2, - 0, - 1, - ); - this.ellipse( - x + w - lowerRight.x, - y + h + lowerRight.y, - lowerRight.x, - lowerRight.y, - 0, - 0, - -Math.PI / 2, - 1, - ); - this.ellipse( - x + lowerLeft.x, - y + h + lowerLeft.y, - lowerLeft.x, - lowerLeft.y, - 0, - -Math.PI / 2, - -Math.PI, - 1, - ); - this.ellipse( - x + upperLeft.x, - y - upperLeft.y, - upperLeft.x, - upperLeft.y, - 0, - -Math.PI, - -Math.PI * 1.5, - 1, - ); - } else { - this.moveTo(x + upperLeft.x, y); - this.ellipse( - x + w - upperRight.x, - y + upperRight.y, - upperRight.x, - upperRight.y, - 0, - -Math.PI / 2, - 0, - ); - this.ellipse( - x + w - lowerRight.x, - y + h - lowerRight.y, - lowerRight.x, - lowerRight.y, - 0, - 0, - Math.PI / 2, - ); - this.ellipse( - x + lowerLeft.x, - y + h - lowerLeft.y, - lowerLeft.x, - lowerLeft.y, - 0, - Math.PI / 2, - Math.PI, - ); - this.ellipse( - x + upperLeft.x, - y + upperLeft.y, - upperLeft.x, - upperLeft.y, - 0, - Math.PI, - Math.PI * 1.5, - ); - } - - this.closePath(); - this.moveTo(x, y); - - function toDOMPointInit(value) { - const { x, y, z, w } = value; - return { x, y, z, w }; - } - - function parseRadiiArgument(value) { - // https://webidl.spec.whatwg.org/#es-union - // with 'optional (unrestricted double or DOMPointInit - // or sequence<(unrestricted double or DOMPointInit)>) radii = 0' - const type = typeof value; - - if (type === "undefined" || value === null) { - return [0]; - } - if (type === "function") { - return [NaN]; - } - if (type === "object") { - if (typeof value[Symbol.iterator] === "function") { - return [...value].map((elem) => { - // https://webidl.spec.whatwg.org/#es-union - // with '(unrestricted double or DOMPointInit)' - const elemType = typeof elem; - if (elemType === "undefined" || elem === null) { - return 0; - } - if (elemType === "function") { - return NaN; - } - if (elemType === "object") { - return toDOMPointInit(elem); - } - return toUnrestrictedNumber(elem); - }); - } - - return [toDOMPointInit(value)]; - } - - return [toUnrestrictedNumber(value)]; - } - - function toUnrestrictedNumber(value) { - return +value; - } - - function toCornerPoint(value) { - const asNumber = toUnrestrictedNumber(value); - if (Number.isFinite(asNumber)) { - return { - x: asNumber, - y: asNumber, - }; - } - if (Object(value) === value) { - return { - x: toUnrestrictedNumber(value.x ?? 0), - y: toUnrestrictedNumber(value.y ?? 0), - }; - } - - return { - x: NaN, - y: NaN, - }; - } - - function fixOverlappingCorners(corners) { - const [upperLeft, upperRight, lowerRight, lowerLeft] = corners; - const factors = [ - Math.abs(w) / (upperLeft.x + upperRight.x), - Math.abs(h) / (upperRight.y + lowerRight.y), - Math.abs(w) / (lowerRight.x + lowerLeft.x), - Math.abs(h) / (upperLeft.y + lowerLeft.y), - ]; - const minFactor = Math.min(...factors); - if (minFactor <= 1) { - for (const radii of corners) { - radii.x *= minFactor; - radii.y *= minFactor; - } - } - } - } - - function getErrorMessageHeader(instance) { - return `Failed to execute 'roundRect' on '${getConstructorName( - instance, - )}':`; - } - - function getConstructorName(instance) { - if (typeof window === "undefined") { - return "UNKNOWN"; - } - return Object(instance) === instance && instance instanceof Path2D - ? "Path2D" - : instance instanceof window?.CanvasRenderingContext2D - ? "CanvasRenderingContext2D" - : instance instanceof window?.OffscreenCanvasRenderingContext2D - ? "OffscreenCanvasRenderingContext2D" - : instance?.constructor.name || instance; - } -})(); diff --git a/yarn.lock b/yarn.lock index 7d385cce71..2e3c1ea8e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3830,6 +3830,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz#0ef8a1cf8b16be47a0f9fc4ecfc952232724b32a" integrity sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw== +canvas-roundrect-polyfill@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/canvas-roundrect-polyfill/-/canvas-roundrect-polyfill-0.0.1.tgz#70bf107ebe2037f26d839d7f809a26f4a95f5696" + integrity sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw== + case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" From 81ebf82979f7efba460a882478284583451f2f32 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Thu, 15 Jun 2023 00:42:01 +0800 Subject: [PATCH 12/27] feat: introduce frames (#6123) Co-authored-by: dwelle --- src/actions/actionAddToLibrary.ts | 5 +- src/actions/actionAlign.tsx | 40 +- src/actions/actionBoundText.tsx | 1 + src/actions/actionCanvas.tsx | 10 +- src/actions/actionClipboard.tsx | 25 +- src/actions/actionDeleteSelected.tsx | 14 +- src/actions/actionDistribute.tsx | 18 +- src/actions/actionDuplicateSelection.tsx | 80 +- src/actions/actionElementLock.ts | 21 +- src/actions/actionFlip.ts | 14 +- src/actions/actionFrame.ts | 140 +++ src/actions/actionGroup.tsx | 86 +- src/actions/actionLinearEditor.ts | 8 +- src/actions/actionMenu.tsx | 1 - src/actions/actionProperties.tsx | 5 +- src/actions/actionSelectAll.ts | 22 +- src/actions/actionStyles.ts | 12 +- src/actions/types.ts | 5 + src/align.ts | 4 +- src/appState.ts | 14 + src/clipboard.ts | 21 +- src/components/Actions.tsx | 150 ++- src/components/App.tsx | 1025 +++++++++++++++-- src/components/HelpDialog.tsx | 1 + src/components/ImageExportDialog.tsx | 5 +- src/components/LayerUI.tsx | 9 +- src/components/LibraryMenu.tsx | 6 +- src/components/ToolButton.tsx | 4 +- src/components/Toolbar.scss | 19 +- .../dropdownMenu/DropdownMenuTrigger.tsx | 10 +- .../hoc/withInternalFallback.test.tsx | 24 +- src/components/icons.tsx | 21 + src/components/main-menu/MainMenu.tsx | 1 + src/constants.ts | 11 + src/data/restore.ts | 6 + src/element/Hyperlink.tsx | 6 +- src/element/binding.ts | 2 +- src/element/bounds.ts | 266 ++++- src/element/collision.ts | 168 ++- src/element/dragElements.ts | 35 +- src/element/index.ts | 14 +- src/element/linearElementEditor.ts | 2 +- src/element/newElement.ts | 25 + src/element/resizeElements.ts | 82 +- src/element/textElement.ts | 10 +- src/element/transformHandles.ts | 14 +- src/element/typeChecks.ts | 7 + src/element/types.ts | 12 +- src/frame.ts | 705 ++++++++++++ src/groups.ts | 40 + src/keys.ts | 1 + src/locales/en.json | 6 +- src/math.ts | 2 +- src/packages/excalidraw/example/App.tsx | 1 + .../excalidraw/example/initialData.js | 44 + src/renderer/renderElement.ts | 163 ++- src/renderer/renderScene.ts | 203 +++- src/scene/Scene.ts | 52 +- src/scene/comparisons.ts | 3 +- src/scene/export.ts | 83 +- src/scene/selection.ts | 96 +- src/shapes.tsx | 8 + .../__snapshots__/contextmenu.test.tsx.snap | 275 +++++ .../__snapshots__/dragCreate.test.tsx.snap | 5 + src/tests/__snapshots__/export.test.tsx.snap | 3 +- src/tests/__snapshots__/move.test.tsx.snap | 6 + .../multiPointCreate.test.tsx.snap | 2 + .../regressionTests.test.tsx.snap | 642 +++++++++++ .../__snapshots__/selection.test.tsx.snap | 5 + .../data/__snapshots__/restore.test.ts.snap | 9 + src/tests/fixtures/elementFixture.ts | 1 + src/tests/helpers/api.ts | 7 +- .../packages/__snapshots__/utils.test.ts.snap | 5 + src/tests/queries/toolQueries.ts | 1 + .../scene/__snapshots__/export.test.ts.snap | 5 +- src/types.ts | 35 +- src/utils.ts | 21 +- src/zindex.ts | 133 ++- 78 files changed, 4563 insertions(+), 480 deletions(-) create mode 100644 src/actions/actionFrame.ts create mode 100644 src/frame.ts diff --git a/src/actions/actionAddToLibrary.ts b/src/actions/actionAddToLibrary.ts index a4fca560a2..ef69a60de0 100644 --- a/src/actions/actionAddToLibrary.ts +++ b/src/actions/actionAddToLibrary.ts @@ -12,7 +12,10 @@ export const actionAddToLibrary = register({ const selectedElements = getSelectedElements( getNonDeletedElements(elements), appState, - true, + { + includeBoundTextElement: true, + includeElementsInFrames: true, + }, ); if (selectedElements.some((element) => element.type === "image")) { return { diff --git a/src/actions/actionAlign.tsx b/src/actions/actionAlign.tsx index eceb421714..d917f80372 100644 --- a/src/actions/actionAlign.tsx +++ b/src/actions/actionAlign.tsx @@ -10,6 +10,7 @@ import { import { ToolButton } from "../components/ToolButton"; import { getNonDeletedElements } from "../element"; import { ExcalidrawElement } from "../element/types"; +import { updateFrameMembershipOfSelectedElements } from "../frame"; import { t } from "../i18n"; import { KEYS } from "../keys"; import { getSelectedElements, isSomeElementSelected } from "../scene"; @@ -17,10 +18,20 @@ import { AppState } from "../types"; import { arrayToMap, getShortcutKey } from "../utils"; import { register } from "./register"; -const enableActionGroup = ( +const alignActionsPredicate = ( elements: readonly ExcalidrawElement[], appState: AppState, -) => getSelectedElements(getNonDeletedElements(elements), appState).length > 1; +) => { + const selectedElements = getSelectedElements( + getNonDeletedElements(elements), + appState, + ); + return ( + selectedElements.length > 1 && + // TODO enable aligning frames when implemented properly + !selectedElements.some((el) => el.type === "frame") + ); +}; const alignSelectedElements = ( elements: readonly ExcalidrawElement[], @@ -36,14 +47,16 @@ const alignSelectedElements = ( const updatedElementsMap = arrayToMap(updatedElements); - return elements.map( - (element) => updatedElementsMap.get(element.id) || element, + return updateFrameMembershipOfSelectedElements( + elements.map((element) => updatedElementsMap.get(element.id) || element), + appState, ); }; export const actionAlignTop = register({ name: "alignTop", trackEvent: { category: "element" }, + predicate: alignActionsPredicate, perform: (elements, appState) => { return { appState, @@ -58,7 +71,7 @@ export const actionAlignTop = register({ event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP, PanelComponent: ({ elements, appState, updateData }) => (