From 809d5ba17f2e55e4db0d0302d5af38bbb9c8922a Mon Sep 17 00:00:00 2001 From: David Luzar Date: Sun, 8 Jan 2023 16:22:04 +0100 Subject: [PATCH 01/14] fix: png-exporting does not preserve angles correctly for flipped images (#6085) * fix: png-exporting does not preserve angles correctly for flipped images * refactor related code * simplify further and comment --- src/renderer/renderElement.ts | 50 +++++++++++++++++++---------------- src/scene/scroll.ts | 4 +-- src/utils.ts | 5 ++-- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 0c7882cad..f77a8c482 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -713,22 +713,8 @@ const drawElementFromCanvas = ( const cx = ((x1 + x2) / 2 + renderConfig.scrollX) * window.devicePixelRatio; const cy = ((y1 + y2) / 2 + renderConfig.scrollY) * window.devicePixelRatio; - const _isPendingImageElement = isPendingImageElement(element, renderConfig); - - const scaleXFactor = - "scale" in elementWithCanvas.element && !_isPendingImageElement - ? elementWithCanvas.element.scale[0] - : 1; - const scaleYFactor = - "scale" in elementWithCanvas.element && !_isPendingImageElement - ? elementWithCanvas.element.scale[1] - : 1; - context.save(); - context.scale( - (1 / window.devicePixelRatio) * scaleXFactor, - (1 / window.devicePixelRatio) * scaleYFactor, - ); + context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio); const boundTextElement = getBoundTextElement(element); if (isArrowElement(element) && boundTextElement) { @@ -793,7 +779,7 @@ const drawElementFromCanvas = ( zoom, ); - context.translate(cx * scaleXFactor, cy * scaleYFactor); + context.translate(cx, cy); context.drawImage( tempCanvas, (-(x2 - x1) / 2) * window.devicePixelRatio - offsetX / zoom - padding, @@ -802,15 +788,30 @@ const drawElementFromCanvas = ( tempCanvas.height / zoom, ); } else { - context.translate(cx * scaleXFactor, cy * scaleYFactor); + // we translate context to element center so that rotation and scale + // originates from the element center + context.translate(cx, cy); - context.rotate(element.angle * scaleXFactor * scaleYFactor); + context.rotate(element.angle); + + if ( + "scale" in elementWithCanvas.element && + !isPendingImageElement(element, renderConfig) + ) { + context.scale( + elementWithCanvas.element.scale[0], + elementWithCanvas.element.scale[1], + ); + } + + // revert afterwards we don't have account for it during drawing + context.translate(-cx, -cy); context.drawImage( elementWithCanvas.canvas!, - (-(x2 - x1) / 2) * window.devicePixelRatio - + (x1 + renderConfig.scrollX) * window.devicePixelRatio - (padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom, - (-(y2 - y1) / 2) * window.devicePixelRatio - + (y1 + renderConfig.scrollY) * window.devicePixelRatio - (padding * elementWithCanvas.canvasZoom) / elementWithCanvas.canvasZoom, elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom, elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom, @@ -905,9 +906,6 @@ export const renderElement = ( } context.save(); context.translate(cx, cy); - if (element.type === "image") { - context.scale(element.scale[0], element.scale[1]); - } if (shouldResetImageFilter(element, renderConfig)) { context.filter = "none"; @@ -973,6 +971,12 @@ export const renderElement = ( ); } else { context.rotate(element.angle); + + if (element.type === "image") { + // note: scale must be applied *after* rotating + context.scale(element.scale[0], element.scale[1]); + } + context.translate(-shiftX, -shiftY); drawElementOnCanvas(element, rc, context, renderConfig); } diff --git a/src/scene/scroll.ts b/src/scene/scroll.ts index 4fce5e641..114d6db05 100644 --- a/src/scene/scroll.ts +++ b/src/scene/scroll.ts @@ -41,8 +41,8 @@ export const centerScrollOn = ({ zoom: Zoom; }) => { return { - scrollX: (viewportDimensions.width / 2) * (1 / zoom.value) - scenePoint.x, - scrollY: (viewportDimensions.height / 2) * (1 / zoom.value) - scenePoint.y, + scrollX: viewportDimensions.width / 2 / zoom.value - scenePoint.x, + scrollY: viewportDimensions.height / 2 / zoom.value - scenePoint.y, }; }; diff --git a/src/utils.ts b/src/utils.ts index ffaf414de..119eb7ae5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -352,9 +352,8 @@ export const viewportCoordsToSceneCoords = ( scrollY: number; }, ) => { - const invScale = 1 / zoom.value; - const x = (clientX - offsetLeft) * invScale - scrollX; - const y = (clientY - offsetTop) * invScale - scrollY; + const x = (clientX - offsetLeft) / zoom.value - scrollX; + const y = (clientY - offsetTop) / zoom.value - scrollY; return { x, y }; }; From 06b45e0cfc9bc93b3565acee2932d643169fb298 Mon Sep 17 00:00:00 2001 From: Antonio Della Fortuna <50418432+adarkforce@users.noreply.github.com> Date: Sun, 8 Jan 2023 17:19:13 +0100 Subject: [PATCH 02/14] fix: image horizontal flip fix + improved tests (#5799) Co-authored-by: Antonio Della Fortuna Co-authored-by: dwelle fixes https://github.com/excalidraw/excalidraw/issues/5784 --- src/element/resizeElements.ts | 8 +- src/tests/flip.test.tsx | 1120 ++++++++++++++++++--------------- 2 files changed, 621 insertions(+), 507 deletions(-) diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index 3782c232c..605ab0c2b 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -557,10 +557,10 @@ export const resizeSingleElement = ( mutateElement(element, { scale: [ // defaulting because scaleX/Y can be 0/-0 - (Math.sign(scaleX) || stateAtResizeStart.scale[0]) * - stateAtResizeStart.scale[0], - (Math.sign(scaleY) || stateAtResizeStart.scale[1]) * - stateAtResizeStart.scale[1], + (Math.sign(newBoundsX2 - stateAtResizeStart.x) || + stateAtResizeStart.scale[0]) * stateAtResizeStart.scale[0], + (Math.sign(newBoundsY2 - stateAtResizeStart.y) || + stateAtResizeStart.scale[1]) * stateAtResizeStart.scale[1], ], }); } diff --git a/src/tests/flip.test.tsx b/src/tests/flip.test.tsx index a388ad4eb..45a5e1477 100644 --- a/src/tests/flip.test.tsx +++ b/src/tests/flip.test.tsx @@ -1,22 +1,53 @@ import ReactDOM from "react-dom"; -import { render } from "./test-utils"; -import App from "../components/App"; -import { defaultLang, setLanguage } from "../i18n"; +import { GlobalTestState, render, waitFor } from "./test-utils"; import { UI, Pointer } from "./helpers/ui"; import { API } from "./helpers/api"; import { actionFlipHorizontal, actionFlipVertical } from "../actions"; +import { getElementAbsoluteCoords } from "../element"; +import { + ExcalidrawElement, + ExcalidrawImageElement, + ExcalidrawLinearElement, + FileId, +} from "../element/types"; +import { newLinearElement } from "../element"; +import ExcalidrawApp from "../excalidraw-app"; +import { mutateElement } from "../element/mutateElement"; +import { NormalizedZoomValue } from "../types"; +import { ROUNDNESS } from "../constants"; const { h } = window; const mouse = new Pointer("mouse"); +jest.mock("../data/blob", () => { + const originalModule = jest.requireActual("../data/blob"); + //Prevent Node.js modules errors (document is not defined etc...) + return { + __esModule: true, + ...originalModule, + resizeImageFile: (imageFile: File) => imageFile, + generateIdFromFile: () => "fileId" as FileId, + }; +}); beforeEach(async () => { // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); - mouse.reset(); - await setLanguage(defaultLang); - await render(); + mouse.reset(); + localStorage.clear(); + sessionStorage.clear(); + jest.clearAllMocks(); + + Object.assign(document, { + elementFromPoint: () => GlobalTestState.canvas, + }); + await render(); + h.setState({ + zoom: { + value: 1 as NormalizedZoomValue, + }, + }); }); const createAndSelectOneRectangle = (angle: number = 0) => { @@ -79,593 +110,676 @@ const createAndReturnOneDraw = (angle: number = 0) => { }); }; -const FLIP_PRECISION_DECIMALS = 7; +const createLinearElementWithCurveInsideMinMaxPoints = ( + type: "line" | "arrow", + extraProps: any = {}, +) => { + return newLinearElement({ + type, + x: 2256.910668124894, + y: -2412.5069664197654, + width: 1750.4888916015625, + height: 410.51605224609375, + angle: 0, + strokeColor: "#000000", + backgroundColor: "#fa5252", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + groupIds: [], + roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS }, + boundElements: null, + link: null, + locked: false, + points: [ + [0, 0], + [-922.4761962890625, 300.3277587890625], + [828.0126953125, 410.51605224609375], + ], + startArrowhead: null, + endArrowhead: null, + }); +}; + +const createLinearElementsWithCurveOutsideMinMaxPoints = ( + type: "line" | "arrow", + extraProps: any = {}, +) => { + return newLinearElement({ + type, + x: -1388.6555370382996, + y: 1037.698247710191, + width: 591.2804897585779, + height: 69.32871961377737, + angle: 0, + strokeColor: "#000000", + backgroundColor: "transparent", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + groupIds: [], + roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS }, + boundElements: null, + link: null, + locked: false, + points: [ + [0, 0], + [-584.1485186423079, -15.365636022723947], + [-591.2804897585779, 36.09360810181511], + [-148.56510566829502, 53.96308359105342], + ], + startArrowhead: null, + endArrowhead: null, + ...extraProps, + }); +}; + +const checkElementsBoundingBox = async ( + element1: ExcalidrawElement, + element2: ExcalidrawElement, + toleranceInPx: number = 0, +) => { + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1); + + const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2); + + debugger; + await waitFor(() => { + // Check if width and height did not change + expect(x1 - toleranceInPx <= x12 && x12 <= x1 + toleranceInPx).toBeTruthy(); + expect(y1 - toleranceInPx <= y12 && y12 <= y1 + toleranceInPx).toBeTruthy(); + expect(x2 - toleranceInPx <= x22 && x22 <= x2 + toleranceInPx).toBeTruthy(); + expect(y2 - toleranceInPx <= y22 && y22 <= y2 + toleranceInPx).toBeTruthy(); + }); +}; + +const checkHorizontalFlip = async (toleranceInPx: number = 0.00001) => { + const originalElement = JSON.parse(JSON.stringify(h.elements[0])); + h.app.actionManager.executeAction(actionFlipHorizontal); + const newElement = h.elements[0]; + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkTwoPointsLineHorizontalFlip = async () => { + const originalElement = JSON.parse( + JSON.stringify(h.elements[0]), + ) as ExcalidrawLinearElement; + h.app.actionManager.executeAction(actionFlipHorizontal); + const newElement = h.elements[0] as ExcalidrawLinearElement; + await waitFor(() => { + expect(originalElement.points[0][0]).toEqual( + newElement.points[0][0] !== 0 ? -newElement.points[0][0] : 0, + ); + expect(originalElement.points[0][1]).toEqual(newElement.points[0][1]); + expect(originalElement.points[1][0]).toEqual( + newElement.points[1][0] !== 0 ? -newElement.points[1][0] : 0, + ); + expect(originalElement.points[1][1]).toEqual(newElement.points[1][1]); + }); +}; + +const checkTwoPointsLineVerticalFlip = async () => { + const originalElement = JSON.parse( + JSON.stringify(h.elements[0]), + ) as ExcalidrawLinearElement; + h.app.actionManager.executeAction(actionFlipVertical); + const newElement = h.elements[0] as ExcalidrawLinearElement; + await waitFor(() => { + expect(originalElement.points[0][0]).toEqual( + newElement.points[0][0] !== 0 ? -newElement.points[0][0] : 0, + ); + expect(originalElement.points[0][1]).toEqual(newElement.points[0][1]); + expect(originalElement.points[1][0]).toEqual( + newElement.points[1][0] !== 0 ? -newElement.points[1][0] : 0, + ); + expect(originalElement.points[1][1]).toEqual(newElement.points[1][1]); + }); +}; + +const checkRotatedHorizontalFlip = async ( + expectedAngle: number, + toleranceInPx: number = 0.00001, +) => { + const originalElement = JSON.parse(JSON.stringify(h.elements[0])); + h.app.actionManager.executeAction(actionFlipHorizontal); + const newElement = h.elements[0]; + await waitFor(() => { + expect(newElement.angle).toBeCloseTo(expectedAngle); + }); + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkRotatedVerticalFlip = async ( + expectedAngle: number, + toleranceInPx: number = 0.00001, +) => { + const originalElement = JSON.parse(JSON.stringify(h.elements[0])); + h.app.actionManager.executeAction(actionFlipVertical); + const newElement = h.elements[0]; + await waitFor(() => { + expect(newElement.angle).toBeCloseTo(expectedAngle); + }); + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkVerticalFlip = async (toleranceInPx: number = 0.00001) => { + const originalElement = JSON.parse(JSON.stringify(h.elements[0])); + + h.app.actionManager.executeAction(actionFlipVertical); + + const newElement = h.elements[0]; + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkVerticalHorizontalFlip = async (toleranceInPx: number = 0.00001) => { + const originalElement = JSON.parse(JSON.stringify(h.elements[0])); + + h.app.actionManager.executeAction(actionFlipHorizontal); + h.app.actionManager.executeAction(actionFlipVertical); + + const newElement = h.elements[0]; + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS = 5; +const MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS = 20; // Rectangle element +describe("rectangle", () => { + it("flips an unrotated rectangle horizontally correctly", async () => { + createAndSelectOneRectangle(); -it("flips an unrotated rectangle horizontally correctly", () => { - createAndSelectOneRectangle(); + await checkHorizontalFlip(); + }); - expect(API.getSelectedElements()[0].x).toEqual(0); + it("flips an unrotated rectangle vertically correctly", async () => { + createAndSelectOneRectangle(); - expect(API.getSelectedElements()[0].y).toEqual(0); + await checkVerticalFlip(); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips a rotated rectangle horizontally correctly", async () => { + const originalAngle = (3 * Math.PI) / 4; + const expectedAngle = (5 * Math.PI) / 4; - h.app.actionManager.executeAction(actionFlipHorizontal); + createAndSelectOneRectangle(originalAngle); - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); + await checkRotatedHorizontalFlip(expectedAngle); + }); - expect(API.getSelectedElements()[0].y).toEqual(0); + it("flips a rotated rectangle vertically correctly", async () => { + const originalAngle = (3 * Math.PI) / 4; + const expectedAgnle = Math.PI / 4; - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); + createAndSelectOneRectangle(originalAngle); - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips an unrotated rectangle vertically correctly", () => { - createAndSelectOneRectangle(); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips a rotated rectangle horizontally correctly", () => { - const originalAngle = (3 * Math.PI) / 4; - const expectedAngle = (5 * Math.PI) / 4; - - createAndSelectOneRectangle(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipHorizontal); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); -}); - -it("flips a rotated rectangle vertically correctly", () => { - const originalAngle = (3 * Math.PI) / 4; - const expectedAgnle = Math.PI / 4; - - createAndSelectOneRectangle(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAgnle); + await checkRotatedVerticalFlip(expectedAgnle); + }); }); // Diamond element +describe("diamond", () => { + it("flips an unrotated diamond horizontally correctly", async () => { + createAndSelectOneDiamond(); -it("flips an unrotated diamond horizontally correctly", () => { - createAndSelectOneDiamond(); + await checkHorizontalFlip(); + }); - expect(API.getSelectedElements()[0].x).toEqual(0); + it("flips an unrotated diamond vertically correctly", async () => { + createAndSelectOneDiamond(); - expect(API.getSelectedElements()[0].y).toEqual(0); + await checkVerticalFlip(); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips a rotated diamond horizontally correctly", async () => { + const originalAngle = (5 * Math.PI) / 4; + const expectedAngle = (3 * Math.PI) / 4; - h.app.actionManager.executeAction(actionFlipHorizontal); + createAndSelectOneDiamond(originalAngle); - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); + await checkRotatedHorizontalFlip(expectedAngle); + }); - expect(API.getSelectedElements()[0].y).toEqual(0); + it("flips a rotated diamond vertically correctly", async () => { + const originalAngle = (5 * Math.PI) / 4; + const expectedAngle = (7 * Math.PI) / 4; - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); + createAndSelectOneDiamond(originalAngle); - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips an unrotated diamond vertically correctly", () => { - createAndSelectOneDiamond(); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips a rotated diamond horizontally correctly", () => { - const originalAngle = (5 * Math.PI) / 4; - const expectedAngle = (3 * Math.PI) / 4; - - createAndSelectOneDiamond(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipHorizontal); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); -}); - -it("flips a rotated diamond vertically correctly", () => { - const originalAngle = (5 * Math.PI) / 4; - const expectedAngle = (7 * Math.PI) / 4; - - createAndSelectOneDiamond(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); + await checkRotatedVerticalFlip(expectedAngle); + }); }); // Ellipse element +describe("ellipse", () => { + it("flips an unrotated ellipse horizontally correctly", async () => { + createAndSelectOneEllipse(); -it("flips an unrotated ellipse horizontally correctly", () => { - createAndSelectOneEllipse(); + await checkHorizontalFlip(); + }); - expect(API.getSelectedElements()[0].x).toEqual(0); + it("flips an unrotated ellipse vertically correctly", async () => { + createAndSelectOneEllipse(); - expect(API.getSelectedElements()[0].y).toEqual(0); + await checkVerticalFlip(); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips a rotated ellipse horizontally correctly", async () => { + const originalAngle = (7 * Math.PI) / 4; + const expectedAngle = Math.PI / 4; - h.app.actionManager.executeAction(actionFlipHorizontal); + createAndSelectOneEllipse(originalAngle); - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); + await checkRotatedHorizontalFlip(expectedAngle); + }); - expect(API.getSelectedElements()[0].y).toEqual(0); + it("flips a rotated ellipse vertically correctly", async () => { + const originalAngle = (7 * Math.PI) / 4; + const expectedAngle = (5 * Math.PI) / 4; - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); + createAndSelectOneEllipse(originalAngle); - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips an unrotated ellipse vertically correctly", () => { - createAndSelectOneEllipse(); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips a rotated ellipse horizontally correctly", () => { - const originalAngle = (7 * Math.PI) / 4; - const expectedAngle = Math.PI / 4; - - createAndSelectOneEllipse(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipHorizontal); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); -}); - -it("flips a rotated ellipse vertically correctly", () => { - const originalAngle = (7 * Math.PI) / 4; - const expectedAngle = (5 * Math.PI) / 4; - - createAndSelectOneEllipse(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); + await checkRotatedVerticalFlip(expectedAngle); + }); }); // Arrow element +describe("arrow", () => { + it("flips an unrotated arrow horizontally with line inside min/max points bounds", async () => { + const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([arrow]); + h.app.setState({ selectedElementIds: { [arrow.id]: true } }); + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); -it("flips an unrotated arrow horizontally correctly", () => { - createAndSelectOneArrow(); + it("flips an unrotated arrow vertically with line inside min/max points bounds", async () => { + const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([arrow]); + h.app.setState({ selectedElementIds: { [arrow.id]: true } }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + await checkVerticalFlip(50); + }); - h.app.actionManager.executeAction(actionFlipHorizontal); + it("flips a rotated arrow horizontally with line inside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([line]); + h.app.state.selectedElementIds[line.id] = true; + mutateElement(line, { + angle: originalAngle, + }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + await checkRotatedHorizontalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); -}); + it("flips a rotated arrow vertically with line inside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([line]); + h.app.state.selectedElementIds[line.id] = true; + mutateElement(line, { + angle: originalAngle, + }); -it("flips an unrotated arrow vertically correctly", () => { - createAndSelectOneArrow(); + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips an unrotated arrow horizontally with line outside min/max points bounds", async () => { + const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([arrow]); + h.app.setState({ selectedElementIds: { [arrow.id]: true } }); - h.app.actionManager.executeAction(actionFlipVertical); + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips a rotated arrow horizontally with line outside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + mutateElement(line, { angle: originalAngle }); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); -}); + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); -//@TODO fix the tests with rotation -it.skip("flips a rotated arrow horizontally correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (7 * Math.PI) / 4; - createAndSelectOneArrow(originalAngle); + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips an unrotated arrow vertically with line outside min/max points bounds", async () => { + const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([arrow]); + h.app.setState({ selectedElementIds: { [arrow.id]: true } }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); - h.app.actionManager.executeAction(actionFlipHorizontal); + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips a rotated arrow vertically with line outside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + mutateElement(line, { angle: originalAngle }); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); -}); + it("flips an unrotated arrow horizontally correctly", async () => { + createAndSelectOneArrow(); + await checkHorizontalFlip( + TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); -it.skip("flips a rotated arrow vertically correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (3 * Math.PI) / 4; - createAndSelectOneArrow(originalAngle); + it("flips an unrotated arrow vertically correctly", async () => { + createAndSelectOneArrow(); + await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips a two points arrow horizontally correctly", async () => { + createAndSelectOneArrow(); + await checkTwoPointsLineHorizontalFlip(); + }); - h.app.actionManager.executeAction(actionFlipVertical); + it("flips a two points arrow vertically correctly", async () => { + createAndSelectOneArrow(); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); - - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); + await checkTwoPointsLineVerticalFlip(); + }); }); // Line element +describe("line", () => { + it("flips an unrotated line horizontally with line inside min/max points bounds", async () => { + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); -it("flips an unrotated line horizontally correctly", () => { - createAndSelectOneLine(); + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips an unrotated line vertically with line inside min/max points bounds", async () => { + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - h.app.actionManager.executeAction(actionFlipHorizontal); + await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + it("flips an unrotated line horizontally correctly", async () => { + createAndSelectOneLine(); + await checkHorizontalFlip( + TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips an unrotated line horizontally with line outside min/max points bounds", async () => { + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); -}); + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); -it("flips an unrotated line vertically correctly", () => { - createAndSelectOneLine(); + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips an unrotated line vertically with line outside min/max points bounds", async () => { + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); - h.app.actionManager.executeAction(actionFlipVertical); + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips a rotated line horizontally with line outside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + mutateElement(line, { angle: originalAngle }); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + await checkRotatedHorizontalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); -}); + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips a rotated line vertically with line outside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + mutateElement(line, { angle: originalAngle }); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); -it.skip("flips a rotated line horizontally correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (7 * Math.PI) / 4; + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - createAndSelectOneLine(originalAngle); + it("flips an unrotated line vertically correctly", async () => { + createAndSelectOneLine(); + await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips a rotated line horizontally with line inside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.state.selectedElementIds[line.id] = true; + mutateElement(line, { + angle: originalAngle, + }); - h.app.actionManager.executeAction(actionFlipHorizontal); + await checkRotatedHorizontalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + it("flips a rotated line vertically with line inside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.state.selectedElementIds[line.id] = true; + mutateElement(line, { + angle: originalAngle, + }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); -}); + it("flips a two points line horizontally correctly", async () => { + createAndSelectOneLine(); + await checkTwoPointsLineHorizontalFlip(); + }); -it.skip("flips a rotated line vertically correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (3 * Math.PI) / 4; - - createAndSelectOneLine(originalAngle); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); - - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); + it("flips a two points line vertically correctly", async () => { + createAndSelectOneLine(); + await checkTwoPointsLineVerticalFlip(); + }); }); // Draw element +describe("freedraw", () => { + it("flips an unrotated drawing horizontally correctly", async () => { + const draw = createAndReturnOneDraw(); + // select draw, since not done automatically + h.state.selectedElementIds[draw.id] = true; + await checkHorizontalFlip(); + }); -it("flips an unrotated drawing horizontally correctly", () => { - const draw = createAndReturnOneDraw(); - // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; + it("flips an unrotated drawing vertically correctly", async () => { + const draw = createAndReturnOneDraw(); + // select draw, since not done automatically + h.state.selectedElementIds[draw.id] = true; + await checkVerticalFlip(); + }); - const originalWidth = draw.width; - const originalHeight = draw.height; + it("flips a rotated drawing horizontally correctly", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; - h.app.actionManager.executeAction(actionFlipHorizontal); + const draw = createAndReturnOneDraw(originalAngle); + // select draw, since not done automatically + h.state.selectedElementIds[draw.id] = true; - // Check if width and height did not change - expect(draw.width).toBeCloseTo(originalWidth, FLIP_PRECISION_DECIMALS); + await checkRotatedHorizontalFlip(expectedAngle); + }); - expect(draw.height).toBeCloseTo(originalHeight, FLIP_PRECISION_DECIMALS); + it("flips a rotated drawing vertically correctly", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + + const draw = createAndReturnOneDraw(originalAngle); + // select draw, since not done automatically + h.state.selectedElementIds[draw.id] = true; + + await checkRotatedVerticalFlip(expectedAngle); + }); }); -it("flips an unrotated drawing vertically correctly", () => { - const draw = createAndReturnOneDraw(); - // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; +//image +//TODO: currently there is no test for pixel colors at flipped positions. +describe("image", () => { + const createImage = async () => { + const sendPasteEvent = (file?: File) => { + const clipboardEvent = new Event("paste", { + bubbles: true, + cancelable: true, + composed: true, + }); - const originalWidth = draw.width; - const originalHeight = draw.height; + // set `clipboardData` properties. + // @ts-ignore + clipboardEvent.clipboardData = { + getData: () => window.navigator.clipboard.readText(), + files: [file], + }; - h.app.actionManager.executeAction(actionFlipVertical); + document.dispatchEvent(clipboardEvent); + }; - // Check if width and height did not change - expect(draw.width).toBeCloseTo(originalWidth, FLIP_PRECISION_DECIMALS); + sendPasteEvent(await API.loadFile("./fixtures/smiley_embedded_v2.png")); + }; - expect(draw.height).toBeCloseTo(originalHeight, FLIP_PRECISION_DECIMALS); -}); - -it("flips a rotated drawing horizontally correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (7 * Math.PI) / 4; - - const draw = createAndReturnOneDraw(originalAngle); - // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; - - const originalWidth = draw.width; - const originalHeight = draw.height; - - h.app.actionManager.executeAction(actionFlipHorizontal); - - // Check if width and height did not change - expect(draw.width).toBeCloseTo(originalWidth, FLIP_PRECISION_DECIMALS); - - expect(draw.height).toBeCloseTo(originalHeight, FLIP_PRECISION_DECIMALS); - - // Check angle - expect(draw.angle).toBeCloseTo(expectedAngle); -}); - -it("flips a rotated drawing vertically correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (3 * Math.PI) / 4; - - const draw = createAndReturnOneDraw(originalAngle); - // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; - - const originalWidth = draw.width; - const originalHeight = draw.height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if width and height did not change - - expect(API.getSelectedElement().width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); - - expect(API.getSelectedElement().height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); - - // Check angle - expect(API.getSelectedElement().angle).toBeCloseTo(expectedAngle); + it("flips an unrotated image horizontally correctly", async () => { + //paste image + await createImage(); + + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + await checkHorizontalFlip(); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]); + expect(h.elements[0].angle).toBeCloseTo(0); + }); + + it("flips an unrotated image vertically correctly", async () => { + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + + await checkVerticalFlip(); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]); + expect(h.elements[0].angle).toBeCloseTo(Math.PI); + }); + + it("flips an rotated image horizontally correctly", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + mutateElement(h.elements[0], { + angle: originalAngle, + }); + await checkRotatedHorizontalFlip(expectedAngle); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]); + }); + + it("flips an rotated image vertically correctly", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(h.elements[0].angle).toEqual(0); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + mutateElement(h.elements[0], { + angle: originalAngle, + }); + + await checkRotatedVerticalFlip(expectedAngle); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]); + expect(h.elements[0].angle).toBeCloseTo(expectedAngle); + }); + + it("flips an image both vertically & horizontally", async () => { + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + + await checkVerticalHorizontalFlip(); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(h.elements[0].angle).toBeCloseTo(Math.PI); + }); }); From 618442299f85229a0e5c1fdb7c3bb9aeaa850a50 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Mon, 9 Jan 2023 10:24:17 +0100 Subject: [PATCH 03/14] fix: React.memo resolvers not accounting for all props (#6042) --- src/components/LayerUI.tsx | 55 ++++++++++++++++++++----------- src/packages/excalidraw/index.tsx | 14 ++++---- src/utils.ts | 12 +++++++ 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index e8a385d02..47f344575 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -15,7 +15,11 @@ import { BinaryFiles, UIChildrenComponents, } from "../types"; -import { muteFSAbortError, ReactChildrenToObject } from "../utils"; +import { + isShallowEqual, + muteFSAbortError, + ReactChildrenToObject, +} from "../utils"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import CollabButton from "./CollabButton"; import { ErrorDialog } from "./ErrorDialog"; @@ -496,28 +500,39 @@ const LayerUI = ({ ); }; -const areEqual = (prev: LayerUIProps, next: LayerUIProps) => { - const getNecessaryObj = (appState: AppState): Partial => { - const { - suggestedBindings, - startBoundElement: boundElement, - ...ret - } = appState; - return ret; - }; - const prevAppState = getNecessaryObj(prev.appState); - const nextAppState = getNecessaryObj(next.appState); +const stripIrrelevantAppStateProps = ( + appState: AppState, +): Partial => { + const { suggestedBindings, startBoundElement, cursorButton, ...ret } = + appState; + return ret; +}; - const keys = Object.keys(prevAppState) as (keyof Partial)[]; +const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => { + // short-circuit early + if (prevProps.children !== nextProps.children) { + return false; + } + + const { + canvas: _prevCanvas, + // not stable, but shouldn't matter in our case + onInsertElements: _prevOnInsertElements, + appState: prevAppState, + ...prev + } = prevProps; + const { + canvas: _nextCanvas, + onInsertElements: _nextOnInsertElements, + appState: nextAppState, + ...next + } = nextProps; return ( - prev.renderTopRightUI === next.renderTopRightUI && - prev.renderCustomStats === next.renderCustomStats && - prev.renderCustomSidebar === next.renderCustomSidebar && - prev.langCode === next.langCode && - prev.elements === next.elements && - prev.files === next.files && - keys.every((key) => prevAppState[key] === nextAppState[key]) + isShallowEqual( + stripIrrelevantAppStateProps(prevAppState), + stripIrrelevantAppStateProps(nextAppState), + ) && isShallowEqual(prev, next) ); }; diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index 3f125fe9e..4dc1f98b2 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -1,6 +1,7 @@ import React, { useEffect, forwardRef } from "react"; import { InitializeApp } from "../../components/InitializeApp"; import App from "../../components/App"; +import { isShallowEqual } from "../../utils"; import "../../css/app.scss"; import "../../css/styles.scss"; @@ -128,6 +129,11 @@ const areEqual = ( prevProps: PublicExcalidrawProps, nextProps: PublicExcalidrawProps, ) => { + // short-circuit early + if (prevProps.children !== nextProps.children) { + return false; + } + const { initialData: prevInitialData, UIOptions: prevUIOptions = {}, @@ -176,13 +182,7 @@ const areEqual = ( return true; }); - const prevKeys = Object.keys(prevProps) as (keyof typeof prev)[]; - const nextKeys = Object.keys(nextProps) as (keyof typeof next)[]; - return ( - isUIOptionsSame && - prevKeys.length === nextKeys.length && - prevKeys.every((key) => prev[key] === next[key]) - ); + return isUIOptionsSame && isShallowEqual(prev, next); }; const forwardedRefComp = forwardRef< diff --git a/src/utils.ts b/src/utils.ts index 119eb7ae5..6f8010d92 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -709,3 +709,15 @@ export const ReactChildrenToObject = < return acc; }, {} as Partial); }; + +export const isShallowEqual = >( + objA: T, + objB: T, +) => { + const aKeys = Object.keys(objA); + const bKeys = Object.keys(objA); + if (aKeys.length !== bKeys.length) { + return false; + } + return aKeys.every((key) => objA[key] === objB[key]); +}; From 328ff6c32d04a869d10f06649da942a641492822 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 11 Jan 2023 19:47:40 +0530 Subject: [PATCH 04/14] fix: use position absolute for mobile misc tools (#6099) --- src/css/styles.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/css/styles.scss b/src/css/styles.scss index cec2af2dd..df259bb59 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -540,7 +540,7 @@ } .mobile-misc-tools-container { - position: fixed; + position: absolute; top: 5rem; right: 0; display: flex; From 699897f71bb68f9d17bb046c4d8ec846b631609a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barnab=C3=A1s=20Moln=C3=A1r?= <38168628+barnabasmolnar@users.noreply.github.com> Date: Thu, 12 Jan 2023 13:06:00 +0100 Subject: [PATCH 05/14] feat: generic button export (#6092) Co-authored-by: dwelle --- src/components/Button.scss | 8 +++++ src/components/Button.tsx | 35 +++++++++++++++++++ src/components/CollabButton.scss | 33 +++++++---------- src/components/CollabButton.tsx | 7 ++-- src/components/dropdownMenu/DropdownMenu.scss | 2 +- src/css/styles.scss | 2 +- src/css/theme.scss | 4 +-- src/css/variables.module.scss | 26 ++++++++------ .../excalidraw/example/CustomFooter.tsx | 10 ++++++ src/packages/excalidraw/index.tsx | 1 + 10 files changed, 90 insertions(+), 38 deletions(-) create mode 100644 src/components/Button.scss create mode 100644 src/components/Button.tsx diff --git a/src/components/Button.scss b/src/components/Button.scss new file mode 100644 index 000000000..1ad22cb80 --- /dev/null +++ b/src/components/Button.scss @@ -0,0 +1,8 @@ +@import "../css/theme"; + +.excalidraw { + .excalidraw-button { + @include outlineButtonStyles; + overflow: hidden; + } +} diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 000000000..3303c3ebf --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,35 @@ +import "./Button.scss"; + +interface ButtonProps extends React.HTMLAttributes { + type?: "button" | "submit" | "reset"; + onSelect: () => any; + children: React.ReactNode; + className?: string; +} + +/** + * A generic button component that follows Excalidraw's design system. + * Style can be customised using `className` or `style` prop. + * Accepts all props that a regular `button` element accepts. + */ +export const Button = ({ + type = "button", + onSelect, + children, + className = "", + ...rest +}: ButtonProps) => { + return ( + + ); +}; diff --git a/src/components/CollabButton.scss b/src/components/CollabButton.scss index 4e09d11c7..94e52d531 100644 --- a/src/components/CollabButton.scss +++ b/src/components/CollabButton.scss @@ -2,29 +2,22 @@ .excalidraw { .collab-button { - @include outlineButtonStyles; - width: var(--lg-button-size); - height: var(--lg-button-size); + --button-bg: var(--color-primary); + --button-color: white; + --button-border: var(--color-primary); + + --button-width: var(--lg-button-size); + --button-height: var(--lg-button-size); + + --button-hover-bg: var(--color-primary-darker); + --button-hover-border: var(--color-primary-darker); + + --button-active-bg: var(--color-primary-darker); - svg { - width: var(--lg-icon-size); - height: var(--lg-icon-size); - } - background-color: var(--color-primary); - border-color: var(--color-primary); - color: white; flex-shrink: 0; - &:hover { - background-color: var(--color-primary-darker); - border-color: var(--color-primary-darker); - } - - &:active { - background-color: var(--color-primary-darker); - } - - &.active { + // double .active to force specificity + &.active.active { background-color: #0fb884; border-color: #0fb884; diff --git a/src/components/CollabButton.tsx b/src/components/CollabButton.tsx index d63444a2c..345213839 100644 --- a/src/components/CollabButton.tsx +++ b/src/components/CollabButton.tsx @@ -3,6 +3,7 @@ import { UsersIcon } from "./icons"; import "./CollabButton.scss"; import clsx from "clsx"; +import { Button } from "./Button"; const CollabButton = ({ isCollaborating, @@ -14,10 +15,10 @@ const CollabButton = ({ onClick: () => void; }) => { return ( - + ); }; diff --git a/src/components/dropdownMenu/DropdownMenu.scss b/src/components/dropdownMenu/DropdownMenu.scss index 28a812876..ff94f4920 100644 --- a/src/components/dropdownMenu/DropdownMenu.scss +++ b/src/components/dropdownMenu/DropdownMenu.scss @@ -73,7 +73,7 @@ } &:hover { - background-color: var(--button-hover); + background-color: var(--button-hover-bg); text-decoration: none; } diff --git a/src/css/styles.scss b/src/css/styles.scss index df259bb59..42c111c3d 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -408,7 +408,7 @@ pointer-events: all; &:hover { - background-color: var(--button-hover); + background-color: var(--button-hover-bg); } &:active { diff --git a/src/css/theme.scss b/src/css/theme.scss index aaa8da5b5..ebf713983 100644 --- a/src/css/theme.scss +++ b/src/css/theme.scss @@ -35,7 +35,7 @@ --shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 0.931014px rgba(0, 0, 0, 0.1702); - --button-hover: var(--color-gray-10); + --button-hover-bg: var(--color-gray-10); --default-border-color: var(--color-gray-30); --default-button-size: 2rem; @@ -135,7 +135,7 @@ --popup-text-inverted-color: #2c2c2c; --select-highlight-color: #{$oc-blue-4}; --text-primary-color: var(--color-gray-40); - --button-hover: var(--color-gray-80); + --button-hover-bg: var(--color-gray-80); --default-border-color: var(--color-gray-80); --shadow-island: 0px 13px 33px rgba(0, 0, 0, 0.07), 0px 4.13px 9.94853px rgba(0, 0, 0, 0.0456112), diff --git a/src/css/variables.module.scss b/src/css/variables.module.scss index 5a367ffe3..39bf4e293 100644 --- a/src/css/variables.module.scss +++ b/src/css/variables.module.scss @@ -39,11 +39,11 @@ .ToolIcon__icon { &:hover { - background: var(--button-hover); + background: var(--button-hover-bg); } &:active { - background: var(--button-hover); + background: var(--button-hover-bg); border: 1px solid var(--color-primary-darkest); } } @@ -54,24 +54,25 @@ justify-content: center; align-items: center; padding: 0.625rem; - width: var(--default-button-size); - height: var(--default-button-size); + width: var(--button-width, var(--default-button-size)); + height: var(--button-height, var(--default-button-size)); box-sizing: border-box; border-width: 1px; border-style: solid; - border-color: var(--default-border-color); + border-color: var(--button-border, var(--default-border-color)); border-radius: var(--border-radius-lg); cursor: pointer; - background-color: transparent; - color: var(--text-primary-color); + background-color: var(--button-bg, var(--island-bg-color)); + color: var(--button-color, var(--text-primary-color)); &:hover { - background-color: var(--button-hover); + background-color: var(--button-hover-bg); + border-color: var(--button-hover-border, var(--default-border-color)); } &:active { - background-color: var(--button-hover); - border-color: var(--color-primary-darkest); + background-color: var(--button-active-bg); + border-color: var(--button-active-border, var(--color-primary-darkest)); } &.active { @@ -83,7 +84,10 @@ } svg { - color: var(--color-primary-darker); + color: var(--button-color, var(--color-primary-darker)); + + width: var(--button-width, var(--lg-icon-size)); + height: var(--button-height, var(--lg-icon-size)); } } } diff --git a/src/packages/excalidraw/example/CustomFooter.tsx b/src/packages/excalidraw/example/CustomFooter.tsx index e6f3ff42e..fbc2ea732 100644 --- a/src/packages/excalidraw/example/CustomFooter.tsx +++ b/src/packages/excalidraw/example/CustomFooter.tsx @@ -1,5 +1,7 @@ import { ExcalidrawImperativeAPI } from "../../../types"; import { MIME_TYPES } from "../entry"; +import { Button } from "../../../components/Button"; + const COMMENT_SVG = ( { return ( <> +
- -
- {WelcomeScreenMenuArrow} -
{t("welcomeScreen.menuHints")}
-
-
- {renderMenu()} + {WelcomeScreenComponents.MenuHint} + {/* wrapping to Fragment stops React from occasionally complaining + about identical Keys */} + <>{renderMenu()}
); @@ -258,9 +268,7 @@ const LayerUI = ({ return ( - {renderWelcomeScreen && !appState.isLoading && ( - - )} + {WelcomeScreenComponents.Center}
{(heading: React.ReactNode) => (
- -
-
- {t("welcomeScreen.toolbarHints")} -
- {WelcomeScreenTopToolbarArrow} -
-
- + {WelcomeScreenComponents.ToolbarHint} onLockToggle()} onPenModeToggle={onPenModeToggle} canvas={canvas} - isCollaborating={isCollaborating} onImageAction={onImageAction} renderTopRightUI={renderTopRightUI} renderCustomStats={renderCustomStats} renderSidebars={renderSidebars} device={device} renderMenu={renderMenu} + welcomeScreenCenter={WelcomeScreenComponents.Center} /> )} @@ -462,13 +458,12 @@ const LayerUI = ({ > {renderFixedSideContainer()}
- {appState.showStats && ( React.ReactNode; setAppState: React.Component["setState"]; elements: readonly NonDeletedExcalidrawElement[]; - onCollabButtonClick?: () => void; onLockToggle: () => void; onPenModeToggle: () => void; canvas: HTMLCanvasElement | null; - isCollaborating: boolean; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; renderTopRightUI?: ( @@ -40,8 +42,8 @@ type MobileMenuProps = { renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderSidebars: () => JSX.Element | null; device: Device; - renderWelcomeScreen?: boolean; renderMenu: () => React.ReactNode; + welcomeScreenCenter: UIWelcomeScreenComponents["Center"]; }; export const MobileMenu = ({ @@ -52,21 +54,18 @@ export const MobileMenu = ({ onLockToggle, onPenModeToggle, canvas, - isCollaborating, onImageAction, renderTopRightUI, renderCustomStats, renderSidebars, device, - renderWelcomeScreen, renderMenu, + welcomeScreenCenter, }: MobileMenuProps) => { const renderToolbar = () => { return ( - {renderWelcomeScreen && !appState.isLoading && ( - - )} + {welcomeScreenCenter}
{(heading: React.ReactNode) => ( @@ -74,20 +73,6 @@ export const MobileMenu = ({ {heading} - {/* - -
*/} void; - icon: JSX.Element; - link?: string; -}) => { - if (link) { - return ( - -
- {icon} - {label} -
-
- ); - } - - return ( - - ); -}; - -const WelcomeScreen = ({ - appState, - actionManager, -}: { - appState: AppState; - actionManager: ActionManager; -}) => { - let subheadingJSX; - - if (isExcalidrawPlusSignedUser) { - subheadingJSX = t("welcomeScreen.switchToPlusApp") - .split(/(Excalidraw\+)/) - .map((bit, idx) => { - if (bit === "Excalidraw+") { - return ( - - Excalidraw+ - - ); - } - return bit; - }); - } else { - subheadingJSX = t("welcomeScreen.data"); - } - - return ( -
-
- {ExcalLogo} Excalidraw -
-
- {subheadingJSX} -
-
- {!appState.viewModeEnabled && ( - actionManager.executeAction(actionLoadScene)} - shortcut={getShortcutFromShortcutName("loadScene")} - icon={LoadIcon} - /> - )} - actionManager.executeAction(actionShortcuts)} - label={t("helpDialog.title")} - shortcut="?" - icon={HelpIcon} - /> - {!isExcalidrawPlusSignedUser && ( - - )} -
-
- ); -}; - -export default WelcomeScreen; diff --git a/src/components/WelcomeScreenDecor.tsx b/src/components/WelcomeScreenDecor.tsx deleted file mode 100644 index 1c7d6cc81..000000000 --- a/src/components/WelcomeScreenDecor.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { ReactNode } from "react"; - -const WelcomeScreenDecor = ({ - children, - shouldRender, -}: { - children: ReactNode; - shouldRender: boolean; -}) => (shouldRender ? <>{children} : null); - -export default WelcomeScreenDecor; diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx index cfd389599..3cb1dd29e 100644 --- a/src/components/footer/Footer.tsx +++ b/src/components/footer/Footer.tsx @@ -1,8 +1,11 @@ import clsx from "clsx"; import { actionShortcuts } from "../../actions"; import { ActionManager } from "../../actions/manager"; -import { t } from "../../i18n"; -import { AppState, UIChildrenComponents } from "../../types"; +import { + AppState, + UIChildrenComponents, + UIWelcomeScreenComponents, +} from "../../types"; import { ExitZenModeAction, FinalizeAction, @@ -11,23 +14,21 @@ import { } from "../Actions"; import { useDevice } from "../App"; import { HelpButton } from "../HelpButton"; -import { WelcomeScreenHelpArrow } from "../icons"; import { Section } from "../Section"; import Stack from "../Stack"; -import WelcomeScreenDecor from "../WelcomeScreenDecor"; const Footer = ({ appState, actionManager, showExitZenModeBtn, - renderWelcomeScreen, footerCenter, + welcomeScreenHelp, }: { appState: AppState; actionManager: ActionManager; showExitZenModeBtn: boolean; - renderWelcomeScreen: boolean; footerCenter: UIChildrenComponents["FooterCenter"]; + welcomeScreenHelp: UIWelcomeScreenComponents["HelpHint"]; }) => { const device = useDevice(); const showFinalize = @@ -79,17 +80,8 @@ const Footer = ({ })} >
- -
-
{t("welcomeScreen.helpHints")}
- {WelcomeScreenHelpArrow} -
-
- + {welcomeScreenHelp} actionManager.executeAction(actionShortcuts)} />
diff --git a/src/components/welcome-screen/WelcomeScreen.Center.tsx b/src/components/welcome-screen/WelcomeScreen.Center.tsx new file mode 100644 index 000000000..7875c0cc9 --- /dev/null +++ b/src/components/welcome-screen/WelcomeScreen.Center.tsx @@ -0,0 +1,176 @@ +import { actionLoadScene, actionShortcuts } from "../../actions"; +import { getShortcutFromShortcutName } from "../../actions/shortcuts"; +import { t } from "../../i18n"; +import { + useDevice, + useExcalidrawActionManager, + useExcalidrawAppState, +} from "../App"; +import { ExcalLogo, HelpIcon, LoadIcon } from "../icons"; + +const WelcomeScreenMenuItemContent = ({ + icon, + shortcut, + children, +}: { + icon?: JSX.Element; + shortcut?: string | null; + children: React.ReactNode; +}) => { + const device = useDevice(); + return ( + <> +
{icon}
+
{children}
+ {shortcut && !device.isMobile && ( +
{shortcut}
+ )} + + ); +}; +WelcomeScreenMenuItemContent.displayName = "WelcomeScreenMenuItemContent"; + +const WelcomeScreenMenuItem = ({ + onSelect, + children, + icon, + shortcut, + className = "", + ...props +}: { + onSelect: () => void; + children: React.ReactNode; + icon?: JSX.Element; + shortcut?: string | null; +} & React.ButtonHTMLAttributes) => { + return ( + + ); +}; +WelcomeScreenMenuItem.displayName = "WelcomeScreenMenuItem"; + +const WelcomeScreenMenuItemLink = ({ + children, + href, + icon, + shortcut, + className = "", + ...props +}: { + children: React.ReactNode; + href: string; + icon?: JSX.Element; + shortcut?: string | null; +} & React.AnchorHTMLAttributes) => { + return ( + + + {children} + + + ); +}; +WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink"; + +const Center = ({ children }: { children?: React.ReactNode }) => { + return ( +
+ {children || ( + <> + + {t("welcomeScreen.defaults.center_heading")} + + + + + + )} +
+ ); +}; +Center.displayName = "Center"; + +const Logo = ({ children }: { children?: React.ReactNode }) => { + return ( +
+ {children || <>{ExcalLogo} Excalidraw} +
+ ); +}; +Logo.displayName = "Logo"; + +const Heading = ({ children }: { children: React.ReactNode }) => { + return ( +
+ {children} +
+ ); +}; +Heading.displayName = "Heading"; + +const Menu = ({ children }: { children?: React.ReactNode }) => { + return
{children}
; +}; +Menu.displayName = "Menu"; + +const MenuItemHelp = () => { + const actionManager = useExcalidrawActionManager(); + + return ( + actionManager.executeAction(actionShortcuts)} + shortcut="?" + icon={HelpIcon} + > + {t("helpDialog.title")} + + ); +}; +MenuItemHelp.displayName = "MenuItemHelp"; + +const MenuItemLoadScene = () => { + const appState = useExcalidrawAppState(); + const actionManager = useExcalidrawActionManager(); + + if (appState.viewModeEnabled) { + return null; + } + + return ( + actionManager.executeAction(actionLoadScene)} + shortcut={getShortcutFromShortcutName("loadScene")} + icon={LoadIcon} + > + {t("buttons.load")} + + ); +}; +MenuItemLoadScene.displayName = "MenuItemLoadScene"; + +// ----------------------------------------------------------------------------- + +Center.Logo = Logo; +Center.Heading = Heading; +Center.Menu = Menu; +Center.MenuItem = WelcomeScreenMenuItem; +Center.MenuItemLink = WelcomeScreenMenuItemLink; +Center.MenuItemHelp = MenuItemHelp; +Center.MenuItemLoadScene = MenuItemLoadScene; + +export { Center }; diff --git a/src/components/welcome-screen/WelcomeScreen.Hints.tsx b/src/components/welcome-screen/WelcomeScreen.Hints.tsx new file mode 100644 index 000000000..d961a6c65 --- /dev/null +++ b/src/components/welcome-screen/WelcomeScreen.Hints.tsx @@ -0,0 +1,42 @@ +import { t } from "../../i18n"; +import { + WelcomeScreenHelpArrow, + WelcomeScreenMenuArrow, + WelcomeScreenTopToolbarArrow, +} from "../icons"; + +const MenuHint = ({ children }: { children?: React.ReactNode }) => { + return ( +
+ {WelcomeScreenMenuArrow} +
+ {children || t("welcomeScreen.defaults.menuHint")} +
+
+ ); +}; +MenuHint.displayName = "MenuHint"; + +const ToolbarHint = ({ children }: { children?: React.ReactNode }) => { + return ( +
+
+ {children || t("welcomeScreen.defaults.toolbarHint")} +
+ {WelcomeScreenTopToolbarArrow} +
+ ); +}; +ToolbarHint.displayName = "ToolbarHint"; + +const HelpHint = ({ children }: { children?: React.ReactNode }) => { + return ( +
+
{children || t("welcomeScreen.defaults.helpHint")}
+ {WelcomeScreenHelpArrow} +
+ ); +}; +HelpHint.displayName = "HelpHint"; + +export { HelpHint, MenuHint, ToolbarHint }; diff --git a/src/components/WelcomeScreen.scss b/src/components/welcome-screen/WelcomeScreen.scss similarity index 61% rename from src/components/WelcomeScreen.scss rename to src/components/welcome-screen/WelcomeScreen.scss index 0ccb63808..f856d4594 100644 --- a/src/components/WelcomeScreen.scss +++ b/src/components/welcome-screen/WelcomeScreen.scss @@ -3,29 +3,39 @@ font-family: "Virgil"; } - .WelcomeScreen-logo { - display: flex; - align-items: center; - column-gap: 0.75rem; - font-size: 2.25rem; + // WelcomeSreen common + // --------------------------------------------------------------------------- - svg { - width: 1.625rem; - height: auto; - } - } - - .WelcomeScreen-decor { + .welcome-screen-decor { pointer-events: none; color: var(--color-gray-40); + } - &--subheading { - font-size: 1.125rem; - text-align: center; + &.theme--dark { + .welcome-screen-decor { + color: var(--color-gray-60); + } + } + + // WelcomeScreen.Hints + // --------------------------------------------------------------------------- + + .welcome-screen-decor-hint { + @media (max-height: 599px) { + display: none !important; } - &--help-pointer { + @media (max-width: 1024px), (max-width: 800px) { + .welcome-screen-decor { + &--help, + &--menu { + display: none; + } + } + } + + &--help { display: flex; position: absolute; right: 0; @@ -49,7 +59,7 @@ } } - &--top-toolbar-pointer { + &--toolbar { position: absolute; top: 100%; left: 50%; @@ -58,7 +68,7 @@ display: flex; align-items: baseline; - &__label { + .welcome-screen-decor-hint__label { width: 120px; position: relative; top: -0.5rem; @@ -74,7 +84,7 @@ } } - &--menu-pointer { + &--menu { position: absolute; width: 320px; font-size: 1rem; @@ -95,10 +105,19 @@ transform: scaleX(-1); } } + + @media (max-width: 860px) { + .welcome-screen-decor-hint__label { + max-width: 160px; + } + } } } - .WelcomeScreen-container { + // WelcomeSreen.Center + // --------------------------------------------------------------------------- + + .welcome-screen-center { display: flex; flex-direction: column; gap: 2rem; @@ -112,7 +131,24 @@ bottom: 1rem; } - .WelcomeScreen-items { + .welcome-screen-center__logo { + display: flex; + align-items: center; + column-gap: 0.75rem; + font-size: 2.25rem; + + svg { + width: 1.625rem; + height: auto; + } + } + + .welcome-screen-center__heading { + font-size: 1.125rem; + text-align: center; + } + + .welcome-screen-menu { display: flex; flex-direction: column; gap: 2px; @@ -120,7 +156,7 @@ align-items: center; } - .WelcomeScreen-item { + .welcome-screen-menu-item { box-sizing: border-box; pointer-events: all; @@ -128,8 +164,10 @@ color: var(--color-gray-50); font-size: 0.875rem; + width: 100%; min-width: 300px; - display: flex; + max-width: 400px; + display: grid; align-items: center; justify-content: space-between; @@ -140,44 +178,49 @@ border-radius: var(--border-radius-md); - &__label { + grid-template-columns: calc(var(--default-icon-size) + 0.5rem) 1fr 3rem; + + &__text { display: flex; align-items: center; + margin-right: auto; + text-align: left; column-gap: 0.5rem; + } - svg { - width: var(--default-icon-size); - height: var(--default-icon-size); - } + &__icon { + width: var(--default-icon-size); + height: var(--default-icon-size); } &__shortcut { + margin-left: auto; color: var(--color-gray-40); font-size: 0.75rem; } } - &:not(:active) .WelcomeScreen-item:hover { + &:not(:active) .welcome-screen-menu-item:hover { text-decoration: none; background: var(--color-gray-10); - .WelcomeScreen-item__shortcut { + .welcome-screen-menu-item__shortcut { color: var(--color-gray-50); } - .WelcomeScreen-item__label { + .welcome-screen-menu-item__text { color: var(--color-gray-100); } } - .WelcomeScreen-item:active { + .welcome-screen-menu-item:active { background: var(--color-gray-20); - .WelcomeScreen-item__shortcut { + .welcome-screen-menu-item__shortcut { color: var(--color-gray-50); } - .WelcomeScreen-item__label { + .welcome-screen-menu-item__text { color: var(--color-gray-100); } @@ -185,7 +228,7 @@ color: var(--color-promo) !important; &:hover { - .WelcomeScreen-item__label { + .welcome-screen-menu-item__text { color: var(--color-promo) !important; } } @@ -193,11 +236,7 @@ } &.theme--dark { - .WelcomeScreen-decor { - color: var(--color-gray-60); - } - - .WelcomeScreen-item { + .welcome-screen-menu-item { color: var(--color-gray-60); &__shortcut { @@ -205,69 +244,41 @@ } } - &:not(:active) .WelcomeScreen-item:hover { + &:not(:active) .welcome-screen-menu-item:hover { background: var(--color-gray-85); - .WelcomeScreen-item__shortcut { + .welcome-screen-menu-item__shortcut { color: var(--color-gray-50); } - .WelcomeScreen-item__label { + .welcome-screen-menu-item__text { color: var(--color-gray-10); } } - .WelcomeScreen-item:active { + .welcome-screen-menu-item:active { background-color: var(--color-gray-90); - .WelcomeScreen-item__label { + .welcome-screen-menu-item__text { color: var(--color-gray-10); } } } - // Can tweak these values but for an initial effort, it looks OK to me - @media (max-width: 1024px) { - .WelcomeScreen-decor { - &--help-pointer, - &--menu-pointer { - display: none; - } - } - } - - // @media (max-height: 400px) { - // .WelcomeScreen-container { - // margin-top: 0; - // } - // } @media (max-height: 599px) { - .WelcomeScreen-container { + .welcome-screen-center { margin-top: 4rem; } } @media (min-height: 600px) and (max-height: 900px) { - .WelcomeScreen-container { + .welcome-screen-center { margin-top: 8rem; } } - @media (max-height: 630px) { - .WelcomeScreen-decor--top-toolbar-pointer { - display: none; - } - } - @media (max-height: 500px) { - .WelcomeScreen-container { + @media (max-height: 500px), (max-width: 320px) { + .welcome-screen-center { display: none; } } - // @media (max-height: 740px) { - // .WelcomeScreen-decor { - // &--help-pointer, - // &--top-toolbar-pointer, - // &--menu-pointer { - // display: none; - // } - // } - // } + // --------------------------------------------------------------------------- } diff --git a/src/components/welcome-screen/WelcomeScreen.tsx b/src/components/welcome-screen/WelcomeScreen.tsx new file mode 100644 index 000000000..9f8c7d735 --- /dev/null +++ b/src/components/welcome-screen/WelcomeScreen.tsx @@ -0,0 +1,17 @@ +import { Center } from "./WelcomeScreen.Center"; +import { MenuHint, ToolbarHint, HelpHint } from "./WelcomeScreen.Hints"; + +import "./WelcomeScreen.scss"; + +const WelcomeScreen = (props: { children: React.ReactNode }) => { + // NOTE this component is used as a dummy wrapper to retrieve child props + // from, and will never be rendered to DOM directly. As such, we can't + // do anything here (use hooks and such) + return null; +}; +WelcomeScreen.displayName = "WelcomeScreen"; + +WelcomeScreen.Center = Center; +WelcomeScreen.Hints = { MenuHint, ToolbarHint, HelpHint }; + +export default WelcomeScreen; diff --git a/src/constants.ts b/src/constants.ts index 47ddf2b29..d6b744bb5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -150,6 +150,7 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = { toggleTheme: null, saveAsImage: true, }, + welcomeScreen: true, }; // breakpoints @@ -236,14 +237,6 @@ export const ROUNDNESS = { ADAPTIVE_RADIUS: 3, } as const; -export const COOKIES = { - AUTH_STATE_COOKIE: "excplus-auth", -} as const; - /** key containt id of precedeing elemnt id we use in reconciliation during * collaboration */ export const PRECEDING_ELEMENT_KEY = "__precedingElement__"; - -export const isExcalidrawPlusSignedUser = document.cookie.includes( - COOKIES.AUTH_STATE_COOKIE, -); diff --git a/src/excalidraw-app/app_constants.ts b/src/excalidraw-app/app_constants.ts index 55047ddf0..179fe52e7 100644 --- a/src/excalidraw-app/app_constants.ts +++ b/src/excalidraw-app/app_constants.ts @@ -38,3 +38,11 @@ export const STORAGE_KEYS = { VERSION_DATA_STATE: "version-dataState", VERSION_FILES: "version-files", } as const; + +export const COOKIES = { + AUTH_STATE_COOKIE: "excplus-auth", +} as const; + +export const isExcalidrawPlusSignedUser = document.cookie.includes( + COOKIES.AUTH_STATE_COOKIE, +); diff --git a/src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx b/src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx index febb66d51..616a2a053 100644 --- a/src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx +++ b/src/excalidraw-app/components/ExcalidrawPlusAppLink.tsx @@ -1,4 +1,4 @@ -import { isExcalidrawPlusSignedUser } from "../../constants"; +import { isExcalidrawPlusSignedUser } from "../app_constants"; export const ExcalidrawPlusAppLink = () => { if (!isExcalidrawPlusSignedUser) { diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 3b24291f9..beef0943c 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -1,6 +1,6 @@ import polyfill from "../polyfill"; import LanguageDetector from "i18next-browser-languagedetector"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { trackEvent } from "../analytics"; import { getDefaultAppState } from "../appState"; import { ErrorDialog } from "../components/ErrorDialog"; @@ -26,6 +26,7 @@ import { defaultLang, Footer, MainMenu, + WelcomeScreen, } from "../packages/excalidraw/index"; import { AppState, @@ -45,6 +46,7 @@ import { } from "../utils"; import { FIREBASE_STORAGE_PREFIXES, + isExcalidrawPlusSignedUser, STORAGE_KEYS, SYNC_BROWSER_TABS_TIMEOUT, } from "./app_constants"; @@ -85,7 +87,7 @@ import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library"; import { EncryptedIcon } from "./components/EncryptedIcon"; import { ExcalidrawPlusAppLink } from "./components/ExcalidrawPlusAppLink"; import { LanguageList } from "./components/LanguageList"; -import { PlusPromoIcon } from "../components/icons"; +import { PlusPromoIcon, UsersIcon } from "../components/icons"; polyfill(); @@ -634,6 +636,69 @@ const ExcalidrawWrapper = () => { ); }; + const welcomeScreenJSX = useMemo(() => { + let headingContent; + + if (isExcalidrawPlusSignedUser) { + headingContent = t("welcomeScreen.app.center_heading_plus") + .split(/(Excalidraw\+)/) + .map((bit, idx) => { + if (bit === "Excalidraw+") { + return ( + + Excalidraw+ + + ); + } + return bit; + }); + } else { + headingContent = t("welcomeScreen.app.center_heading"); + } + + return ( + + + {t("welcomeScreen.app.menuHint")} + + + + + + + {headingContent} + + + + + + setCollabDialogShown(true)} + icon={UsersIcon} + > + {t("labels.liveCollaboration")} + + + {!isExcalidrawPlusSignedUser && ( + + Try Excalidraw Plus! + + )} + + + + ); + }, [setCollabDialogShown]); + return (
{
+ {welcomeScreenJSX} {excalidrawAPI && } {errorMessage && ( diff --git a/src/locales/en.json b/src/locales/en.json index 50424c3fd..2fe9e7c84 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -448,10 +448,16 @@ "d9480f": "Orange 9" }, "welcomeScreen": { - "data": "All your data is saved locally in your browser.", - "switchToPlusApp": "Did you want to go to the Excalidraw+ instead?", - "menuHints": "Export, preferences, languages, ...", - "toolbarHints": "Pick a tool & Start drawing!", - "helpHints": "Shortcuts & help" + "app": { + "center_heading": "All your data is saved locally in your browser.", + "center_heading_plus": "Did you want to go to the Excalidraw+ instead?", + "menuHint": "Export, preferences, languages, ..." + }, + "defaults": { + "menuHint": "Export, preferences, and more...", + "center_heading": "Diagrams. Made. Simple.", + "toolbarHint": "Pick a tool & Start drawing!", + "helpHint": "Shortcuts & help" + } } } diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index c5dabf070..94db27467 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -15,15 +15,19 @@ Please add the latest change on the top under the correct section. ### Features -- Any top-level children passed to the `` component that do not belong to one of the officially supported Excalidraw children components are now rendered directly inside the Excalidraw container (previously, they weren't rendered at all) [#6096](https://github.com/excalidraw/excalidraw/pull/6096). +- Support customization for the editor [welcome screen](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#WelcomeScreen) [#6048](https://github.com/excalidraw/excalidraw/pull/6048). - Expose component API for the Excalidraw main menu [#6034](https://github.com/excalidraw/excalidraw/pull/6034), You can read more about its usage [here](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#MainMenu) -- Render Footer as a component instead of render prop [#5970](https://github.com/excalidraw/excalidraw/pull/5970). You can read more about its usage [here](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Footer) +- Support customization for the Excalidraw [main menu](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#MainMenu) [#6034](https://github.com/excalidraw/excalidraw/pull/6034). + +- [Footer](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Footer) is now rendered as child component instead of passed as a render prop [#5970](https://github.com/excalidraw/excalidraw/pull/5970). + +- Any top-level children passed to the `` component that do not belong to one of the officially supported Excalidraw children components are now rendered directly inside the Excalidraw container (previously, they weren't rendered at all) [#6096](https://github.com/excalidraw/excalidraw/pull/6096). #### BREAKING CHANGE -- With this change, the prop `renderFooter` is now removed. +- The prop `renderFooter` is now removed in favor of rendering as a child component. ### Excalidraw schema diff --git a/src/packages/excalidraw/README.md b/src/packages/excalidraw/README.md index b946e4323..5b9f30342 100644 --- a/src/packages/excalidraw/README.md +++ b/src/packages/excalidraw/README.md @@ -376,7 +376,7 @@ Most notably, you can customize the primary colors, by overriding these variable For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/src/css/theme.scss), though most of them will not make sense to override. -### Does this package support collaboration ? +### Does this package support collaboration? No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). @@ -405,45 +405,47 @@ const App = () => { }; ``` -This will only for `Desktop` devices. +Footer is only rendered in the desktop view. -For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component. +In the mobile view you can render it inside the [MainMenu](#mainmenu) (later we will expose other ways to customize the UI). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component. ```js import { useDevice, Footer } from "@excalidraw/excalidraw"; -const MobileFooter = ({ -}) => { +const MobileFooter = () => { const device = useDevice(); if (device.isMobile) { return (
- +
); } return null; - }; + const App = () => { - window.alert("Item1")}> Item1 - window.alert("Item2")}> Item 2 - + window.alert("Item1")}> + Item1 + + window.alert("Item2")}> + Item2 + + - -} - +
; +}; ``` -You can visit the[ example](https://ehlz3.csb.app/) for working demo. +You can visit the [example](https://ehlz3.csb.app/) for working demo. #### MainMenu @@ -456,11 +458,15 @@ import { MainMenu } from "@excalidraw/excalidraw"; const App = () => { - window.alert("Item1")}> Item1 - window.alert("Item2")}> Item 2 + window.alert("Item1")}> + Item1 + + window.alert("Item2")}> + Item2 + - -} +
; +}; ``` **MainMenu** @@ -469,28 +475,28 @@ This is the `MainMenu` component which you need to import to render the menu wit **MainMenu.Item** -To render an item, its recommended to use `MainMenu.Item`. +Use this component to render a menu item. | Prop | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `onSelect` | `Function` | Yes | `undefined` | The handler is triggered when the item is selected. | -| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item | -| `icon` | `JSX.Element` | No | `undefined` | The icon used in the menu item | -| `shortcut` | `string` | No | `undefined` | The shortcut to be shown for the menu item | -| `className` | `string` | No | "" | The class names to be added to the menu item | -| `style` | `React.CSSProperties` | No | `undefined` | The inline styles to be added to the menu item | -| `ariaLabel` | `string` | `undefined` | No | The `aria-label` to be added to the item for accessibility | -| `dataTestId` | `string` | `undefined` | No | The `data-testid` to be added to the item. | +| `onSelect` | `Function` | Yes | | The handler is triggered when the item is selected. | +| `children` | `React.ReactNode` | Yes | | The content of the menu item | +| `icon` | `JSX.Element` | No | | The icon used in the menu item | +| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) | +| `className` | `string` | No | | The class names to be added to the menu item | +| `style` | `React.CSSProperties` | No | | The inline styles to be added to the menu item | +| `ariaLabel` | `string` | | No | The `aria-label` to be added to the item for accessibility | +| `dataTestId` | `string` | | No | The `data-testid` to be added to the item. | **MainMenu.ItemLink** -To render an item as a link, its recommended to use `MainMenu.ItemLink`. +To render an external link in a menu item, you can use this component. **Usage** ```js import { MainMenu } from "@excalidraw/excalidraw"; -const App = () => { +const App = () => ( Google @@ -499,19 +505,19 @@ const App = () => { ; -}; +); ``` | Prop | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `href` | `string` | Yes | `undefined` | The `href` attribute to be added to the `anchor` element. | -| `children` | `React.ReactNode` | Yes | `undefined` | The content of the menu item | -| `icon` | `JSX.Element` | No | `undefined` | The icon used in the menu item | -| `shortcut` | `string` | No | `undefined` | The shortcut to be shown for the menu item | +| `href` | `string` | Yes | | The `href` attribute to be added to the `anchor` element. | +| `children` | `React.ReactNode` | Yes | | The content of the menu item | +| `icon` | `JSX.Element` | No | | The icon used in the menu item | +| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) | | `className` | `string` | No | "" | The class names to be added to the menu item | -| `style` | `React.CSSProperties` | No | `undefined` | The inline styles to be added to the menu item | -| `ariaLabel` | `string` | No | `undefined` | The `aria-label` to be added to the item for accessibility | -| `dataTestId` | `string` | No | `undefined` | The `data-testid` to be added to the item. | +| `style` | `React.CSSProperties` | No | | The inline styles to be added to the menu item | +| `ariaLabel` | `string` | No | | The `aria-label` to be added to the item for accessibility | +| `dataTestId` | `string` | No | | The `data-testid` to be added to the item. | **MainMenu.ItemCustom** @@ -521,7 +527,7 @@ To render a custom item, you can use `MainMenu.ItemCustom`. ```js import { MainMenu } from "@excalidraw/excalidraw"; -const App = () => { +const App = () => ( @@ -535,7 +541,7 @@ const App = () => { ; -}; +); ``` | Prop | Type | Required | Default | Description | @@ -551,7 +557,7 @@ For the items which are shown in the menu in [excalidraw.com](https://excalidraw ```js import { MainMenu } from "@excalidraw/excalidraw"; -const App = () => { +const App = () => ( @@ -560,7 +566,7 @@ const App = () => { window.alert("Item2")}> Item 2 -} +) ``` Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items. @@ -571,7 +577,7 @@ To Group item in the main menu, you can use `MainMenu.Group` ```js import { MainMenu } from "@excalidraw/excalidraw"; -const App = () => { +const App = () => ( @@ -584,16 +590,149 @@ const App = () => { -} +) ``` | Prop | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `children ` | `React.ReactNode` | Yes | `undefined` | The content of the `Menu Group` | +| `children ` | `React.ReactNode` | Yes | `undefined` | The content of the `MenuItem Group` | | `title` | `string` | No | `undefined` | The `title` for the grouped items | | `className` | `string` | No | "" | The `classname` to be added to the group | | `style` | `React.CSsSProperties` | No | `undefined` | The inline `styles` to be added to the group | +### WelcomeScreen + +When the canvas is empty, Excalidraw shows a welcome "splash" screen with a logo, a few quick action items, and hints explaining what some of the UI buttons do. You can customize the welcome screen by rendering the `WelcomeScreen` component inside your Excalidraw instance. + +You can also disable the welcome screen altogether by setting `UIOptions.welcomeScreen` to `false`. + +**Usage** + +```jsx +import { WelcomScreen } from "@excalidraw/excalidraw"; +const App = () => ( + + + + + Your data are autosaved to the cloud. + + + console.log("clicked!")} + > + Click me! + + + Excalidraw GitHub + + + + + + +); +``` + +To disable the WelcomeScreen: + +```jsx +import { WelcomScreen } from "@excalidraw/excalidraw"; +const App = () => ; +``` + +**WelcomeScreen** + +If you render the `` component, you are responsible for rendering the content. + +There are 2 main parts: 1) welcome screen center component, and 2) welcome screen hints. + +![WelcomeScreen overview](./welcome-screen-overview.png) + +**WelcomeScreen.Center** + +This is the center piece of the welcome screen, containing the logo, heading, and menu. All three sub-components are optional, and you can render whatever you wish into the center component. + +**WelcomeScreen.Center.Logo** + +By default renders the Excalidraw logo and name. Supply `children` to customize. + +**WelcomeScreen.Center.Heading** + +Supply `children` to change the default message. + +**WelcomeScreen.Center.Menu** + +Wrapper component for the menu items. You can build your menu using the `` and `` components, render your own, or render one of the default menu items. + +The default menu items are: + +- `` - opens the help dialog. +- `` - open the load file dialog. + +**Usage** + +```jsx +import { WelcomScreen } from "@excalidraw/excalidraw"; +const App = () => ( + + + + + console.log("clicked!")} + > + Click me! + + + Excalidraw GitHub + + + + + + +); +``` + +**WelcomeScreen.Center.MenuItem** + +Use this component to render a menu item. + +| Prop | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `onSelect` | `Function` | Yes | | The handler is triggered when the item is selected. | +| `children` | `React.ReactNode` | Yes | | The content of the menu item | +| `icon` | `JSX.Element` | No | | The icon used in the menu item | +| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) | + +**WelcomeScreen.Center.MenuItemLink** + +To render an external link in a menu item, you can use this component. + +| Prop | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `href` | `string` | Yes | | The `href` attribute to be added to the `anchor` element. | +| `children` | `React.ReactNode` | Yes | | The content of the menu item | +| `icon` | `JSX.Element` | No | | The icon used in the menu item | +| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) | + +**WelcomeScreen.Hints** + +These subcomponents render the UI hints. Text of each hint can be customized by supplying `children`. + +**WelcomeScreen.Hints.Menu** + +Hint for the main menu. Supply `children` to customize the hint text. + +**WelcomeScreen.Hints.Toolbar** + +Hint for the toolbar. Supply `children` to customize the hint text. + +**WelcomeScreen.Hints.Help** + +Hint for the help dialog. Supply `children` to customize the hint text. + ### Props | Name | Type | Default | Description | @@ -1565,8 +1704,7 @@ This hook can be used to check the type of device which is being used. It can on ```js import { useDevice, Footer } from "@excalidraw/excalidraw"; -const MobileFooter = ({ -}) => { +const MobileFooter = () => { const device = useDevice(); if (device.isMobile) { return ( diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index abc7bdd34..9be20a99c 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -13,6 +13,7 @@ import { Provider } from "jotai"; import { jotaiScope, jotaiStore } from "../../jotai"; import Footer from "../../components/footer/FooterCenter"; import MainMenu from "../../components/mainMenu/MainMenu"; +import WelcomeScreen from "../../components/welcome-screen/WelcomeScreen"; const ExcalidrawBase = (props: ExcalidrawProps) => { const { @@ -52,6 +53,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { ...DEFAULT_UI_OPTIONS.canvasActions, ...canvasActions, }, + welcomeScreen: props.UIOptions?.welcomeScreen ?? true, }; if (canvasActions?.export) { @@ -162,7 +164,7 @@ const areEqual = ( const canvasOptionKeys = Object.keys( prevUIOptions.canvasActions!, ) as (keyof Partial)[]; - canvasOptionKeys.every((key) => { + return canvasOptionKeys.every((key) => { if ( key === "export" && prevUIOptions?.canvasActions?.export && @@ -179,7 +181,7 @@ const areEqual = ( ); }); } - return true; + return prevUIOptions[key] === nextUIOptions[key]; }); return isUIOptionsSame && isShallowEqual(prev, next); @@ -243,3 +245,4 @@ export { Button } from "../../components/Button"; export { Footer }; export { MainMenu }; export { useDevice } from "../../components/App"; +export { WelcomeScreen }; diff --git a/src/packages/excalidraw/welcome-screen-overview.png b/src/packages/excalidraw/welcome-screen-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..1067761629614e099f46546305af0efd50cf2aa8 GIT binary patch literal 103250 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

    `p@-A-vx6tk}!vr7OY4g!ZE`8|J?~p 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

    wE0IE3W8 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 literal 0 HcmV?d00001 diff --git a/src/types.ts b/src/types.ts index 8ee85d279..343789e5b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -313,10 +313,7 @@ export interface ExcalidrawProps { elements: readonly NonDeletedExcalidrawElement[], appState: AppState, ) => JSX.Element; - UIOptions?: { - dockedSidebarBreakpoint?: number; - canvasActions?: CanvasActions; - }; + UIOptions?: Partial; detectScroll?: boolean; handleKeyboardGlobally?: boolean; onLibraryChange?: (libraryItems: LibraryItems) => void | Promise; @@ -373,23 +370,31 @@ export type ExportOpts = { // truthiness value will determine whether the action is rendered or not // (see manager renderAction). We also override canvasAction values in // excalidraw package index.tsx. -type CanvasActions = { - changeViewBackgroundColor?: boolean; - clearCanvas?: boolean; - export?: false | ExportOpts; - loadScene?: boolean; - saveToActiveFile?: boolean; - toggleTheme?: boolean | null; - saveAsImage?: boolean; -}; +type CanvasActions = Partial<{ + changeViewBackgroundColor: boolean; + clearCanvas: boolean; + export: false | ExportOpts; + loadScene: boolean; + saveToActiveFile: boolean; + toggleTheme: boolean | null; + saveAsImage: boolean; +}>; + +type UIOptions = Partial<{ + dockedSidebarBreakpoint: number; + welcomeScreen: boolean; + canvasActions: CanvasActions; +}>; export type AppProps = Merge< ExcalidrawProps, { - UIOptions: { - canvasActions: Required & { export: ExportOpts }; - dockedSidebarBreakpoint?: number; - }; + UIOptions: Merge< + MarkRequired, + { + canvasActions: Required & { export: ExportOpts }; + } + >; detectScroll: boolean; handleKeyboardGlobally: boolean; isCollaborating: boolean; @@ -517,7 +522,31 @@ export type Device = Readonly<{ }>; export type UIChildrenComponents = { - [k in "FooterCenter" | "Menu"]?: - | React.ReactPortal - | React.ReactElement>; + [k in "FooterCenter" | "Menu" | "WelcomeScreen"]?: React.ReactElement< + { children?: React.ReactNode }, + React.JSXElementConstructor + >; +}; + +export type UIWelcomeScreenComponents = { + [k in + | "Center" + | "MenuHint" + | "ToolbarHint" + | "HelpHint"]?: React.ReactElement< + { children?: React.ReactNode }, + React.JSXElementConstructor + >; +}; + +export type UIWelcomeScreenCenterComponents = { + [k in + | "Logo" + | "Heading" + | "Menu" + | "MenuItemLoadScene" + | "MenuItemHelp"]?: React.ReactElement< + { children?: React.ReactNode }, + React.JSXElementConstructor + >; }; From 9d04479f98fca7a322f55961f438aa756909b491 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 12 Jan 2023 20:40:09 +0530 Subject: [PATCH 08/14] fix: renamed folder MainMenu->main-menu and support rest props (#6103) * renamed folder MainMenu -> main-menu * rename ariaLabel -> aria-label and dataTestId -> data-testid * allow rest props * fix * lint * add ts check * ts for div * fix * fix * fix --- src/components/LayerUI.tsx | 2 +- src/components/LibraryMenuHeaderContent.tsx | 6 ++-- .../dropdownMenu/DropdownMenuItem.tsx | 15 +++----- .../dropdownMenu/DropdownMenuItemCustom.tsx | 12 +++---- .../dropdownMenu/DropdownMenuItemLink.tsx | 15 +++----- .../{mainMenu => main-menu}/DefaultItems.scss | 0 .../{mainMenu => main-menu}/DefaultItems.tsx | 36 +++++++++---------- .../{mainMenu => main-menu}/MainMenu.tsx | 0 src/packages/excalidraw/README.md | 28 ++++----------- src/packages/excalidraw/index.tsx | 2 +- 10 files changed, 43 insertions(+), 73 deletions(-) rename src/components/{mainMenu => main-menu}/DefaultItems.scss (100%) rename src/components/{mainMenu => main-menu}/DefaultItems.tsx (91%) rename src/components/{mainMenu => main-menu}/MainMenu.tsx (100%) diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 5d4084a10..d75b8b413 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -50,7 +50,7 @@ import WelcomeScreen from "./welcome-screen/WelcomeScreen"; import { hostSidebarCountersAtom } from "./Sidebar/Sidebar"; import { jotaiScope } from "../jotai"; import { useAtom } from "jotai"; -import MainMenu from "./mainMenu/MainMenu"; +import MainMenu from "./main-menu/MainMenu"; interface LayerUIProps { actionManager: ActionManager; diff --git a/src/components/LibraryMenuHeaderContent.tsx b/src/components/LibraryMenuHeaderContent.tsx index abf6c2a29..beeb8dd17 100644 --- a/src/components/LibraryMenuHeaderContent.tsx +++ b/src/components/LibraryMenuHeaderContent.tsx @@ -193,7 +193,7 @@ export const LibraryMenuHeader: React.FC<{ {t("buttons.load")} @@ -202,7 +202,7 @@ export const LibraryMenuHeader: React.FC<{ {t("buttons.export")} @@ -219,7 +219,7 @@ export const LibraryMenuHeader: React.FC<{ setShowPublishLibraryDialog(true)} - dataTestId="lib-dropdown--remove" + data-testid="lib-dropdown--remove" > {t("buttons.publishLibrary")} diff --git a/src/components/dropdownMenu/DropdownMenuItem.tsx b/src/components/dropdownMenu/DropdownMenuItem.tsx index 47e20166b..4f8db982d 100644 --- a/src/components/dropdownMenu/DropdownMenuItem.tsx +++ b/src/components/dropdownMenu/DropdownMenuItem.tsx @@ -9,30 +9,23 @@ const DropdownMenuItem = ({ icon, onSelect, children, - dataTestId, shortcut, className, - style, - ariaLabel, + ...rest }: { icon?: JSX.Element; onSelect: () => void; children: React.ReactNode; - dataTestId?: string; shortcut?: string; className?: string; - style?: React.CSSProperties; - ariaLabel?: string; -}) => { +} & React.ButtonHTMLAttributes) => { return ( - ); -}; - -export default CollabButton; diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index d75b8b413..8aa8809fb 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -18,7 +18,6 @@ import { } from "../types"; import { isShallowEqual, muteFSAbortError, getReactChildren } from "../utils"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; -import CollabButton from "./CollabButton"; import { ErrorDialog } from "./ErrorDialog"; import { ExportCB, ImageExportDialog } from "./ImageExportDialog"; import { FixedSideContainer } from "./FixedSideContainer"; @@ -59,7 +58,6 @@ interface LayerUIProps { canvas: HTMLCanvasElement | null; setAppState: React.Component["setState"]; elements: readonly NonDeletedExcalidrawElement[]; - onCollabButtonClick?: () => void; onLockToggle: () => void; onPenModeToggle: () => void; onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; @@ -86,7 +84,6 @@ const LayerUI = ({ setAppState, elements, canvas, - onCollabButtonClick, onLockToggle, onPenModeToggle, onInsertElements, @@ -207,12 +204,6 @@ const LayerUI = ({ {UIOptions.canvasActions.saveAsImage && ( )} - {onCollabButtonClick && ( - - )} @@ -351,13 +342,6 @@ const LayerUI = ({ )} > - {onCollabButtonClick && ( - - )} {renderTopRightUI?.(device.isMobile, appState)} {!appState.viewModeEnabled && ( diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 55650385f..cc43aaa49 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -883,7 +883,7 @@ export const CenterHorizontallyIcon = createIcon( modifiedTablerIconProps, ); -export const UsersIcon = createIcon( +export const usersIcon = createIcon( diff --git a/src/components/CollabButton.scss b/src/components/live-collaboration/LiveCollaborationTrigger.scss similarity index 97% rename from src/components/CollabButton.scss rename to src/components/live-collaboration/LiveCollaborationTrigger.scss index 94e52d531..138d6459b 100644 --- a/src/components/CollabButton.scss +++ b/src/components/live-collaboration/LiveCollaborationTrigger.scss @@ -1,4 +1,4 @@ -@import "../css/variables.module"; +@import "../../css/variables.module"; .excalidraw { .collab-button { diff --git a/src/components/live-collaboration/LiveCollaborationTrigger.tsx b/src/components/live-collaboration/LiveCollaborationTrigger.tsx new file mode 100644 index 000000000..87f696d85 --- /dev/null +++ b/src/components/live-collaboration/LiveCollaborationTrigger.tsx @@ -0,0 +1,40 @@ +import { t } from "../../i18n"; +import { usersIcon } from "../icons"; +import { Button } from "../Button"; + +import clsx from "clsx"; +import { useExcalidrawAppState } from "../App"; + +import "./LiveCollaborationTrigger.scss"; + +const LiveCollaborationTrigger = ({ + isCollaborating, + onSelect, + ...rest +}: { + isCollaborating: boolean; + onSelect: () => void; +} & React.ButtonHTMLAttributes) => { + const appState = useExcalidrawAppState(); + + return ( + + ); +}; + +export default LiveCollaborationTrigger; +LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger"; diff --git a/src/components/main-menu/DefaultItems.tsx b/src/components/main-menu/DefaultItems.tsx index e35108be1..6342b31d2 100644 --- a/src/components/main-menu/DefaultItems.tsx +++ b/src/components/main-menu/DefaultItems.tsx @@ -1,4 +1,3 @@ -import clsx from "clsx"; import { getShortcutFromShortcutName } from "../../actions/shortcuts"; import { t } from "../../i18n"; import { @@ -15,7 +14,7 @@ import { save, SunIcon, TrashIcon, - UsersIcon, + usersIcon, } from "../icons"; import { GithubIcon, DiscordIcon, TwitterIcon } from "../icons"; import DropdownMenuItem from "../dropdownMenu/DropdownMenuItem"; @@ -31,6 +30,7 @@ import { import "./DefaultItems.scss"; import { useState } from "react"; import ConfirmDialog from "../ConfirmDialog"; +import clsx from "clsx"; export const LoadScene = () => { // FIXME Hack until we tie "t" to lang state @@ -258,7 +258,7 @@ export const Socials = () => ( ); Socials.displayName = "Socials"; -export const LiveCollaboration = ({ +export const LiveCollaborationTrigger = ({ onSelect, isCollaborating, }: { @@ -271,7 +271,7 @@ export const LiveCollaboration = ({ return ( { }; MenuItemLoadScene.displayName = "MenuItemLoadScene"; +const MenuItemLiveCollaborationTrigger = ({ + onSelect, +}: { + onSelect: () => any; +}) => { + // FIXME when we tie t() to lang state + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const appState = useExcalidrawAppState(); + + return ( + + {t("labels.liveCollaboration")} + + ); +}; +MenuItemLiveCollaborationTrigger.displayName = + "MenuItemLiveCollaborationTrigger"; + // ----------------------------------------------------------------------------- Center.Logo = Logo; @@ -172,5 +190,6 @@ Center.MenuItem = WelcomeScreenMenuItem; Center.MenuItemLink = WelcomeScreenMenuItemLink; Center.MenuItemHelp = MenuItemHelp; Center.MenuItemLoadScene = MenuItemLoadScene; +Center.MenuItemLiveCollaborationTrigger = MenuItemLiveCollaborationTrigger; export { Center }; diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index beef0943c..8f2d8cbcc 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -26,6 +26,7 @@ import { defaultLang, Footer, MainMenu, + LiveCollaborationTrigger, WelcomeScreen, } from "../packages/excalidraw/index"; import { @@ -87,7 +88,7 @@ import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library"; import { EncryptedIcon } from "./components/EncryptedIcon"; import { ExcalidrawPlusAppLink } from "./components/ExcalidrawPlusAppLink"; import { LanguageList } from "./components/LanguageList"; -import { PlusPromoIcon, UsersIcon } from "../components/icons"; +import { PlusPromoIcon } from "../components/icons"; polyfill(); @@ -610,7 +611,7 @@ const ExcalidrawWrapper = () => { - setCollabDialogShown(true)} /> @@ -675,15 +676,9 @@ const ExcalidrawWrapper = () => { - - setCollabDialogShown(true)} - icon={UsersIcon} - > - {t("labels.liveCollaboration")} - - + /> {!isExcalidrawPlusSignedUser && ( { ref={excalidrawRefCallback} onChange={onChange} initialData={initialStatePromiseRef.current.promise} - onCollabButtonClick={() => setCollabDialogShown(true)} isCollaborating={isCollaborating} onPointerUpdate={collabAPI?.onPointerUpdate} UIOptions={{ @@ -744,8 +738,20 @@ const ExcalidrawWrapper = () => { onLibraryChange={onLibraryChange} autoFocus={true} theme={theme} + renderTopRightUI={(isMobile) => { + if (isMobile) { + return null; + } + return ( + setCollabDialogShown(true)} + /> + ); + }} > {renderMenu()} +