From bd4424bbe3e853798b7aac84cb7fef804af1ee62 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 28 Feb 2023 19:53:30 +0530 Subject: [PATCH 01/13] fix: consider arrow for bound text element (#6297) * fix: consider arrow for bound text element * add spec --- src/components/App.tsx | 1 - src/element/typeChecks.test.ts | 66 ++++++++++++++++++++++++++++++++++ src/element/typeChecks.ts | 2 +- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/element/typeChecks.test.ts diff --git a/src/components/App.tsx b/src/components/App.tsx index 5b9311f8d..24da7d85a 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -2767,7 +2767,6 @@ class App extends React.Component { ); if (container) { if ( - isArrowElement(container) || hasBoundTextElement(container) || !isTransparent(container.backgroundColor) || isHittingElementNotConsideringBoundingBox(container, this.state, [ diff --git a/src/element/typeChecks.test.ts b/src/element/typeChecks.test.ts new file mode 100644 index 000000000..ab04c29b7 --- /dev/null +++ b/src/element/typeChecks.test.ts @@ -0,0 +1,66 @@ +import { API } from "../tests/helpers/api"; +import { hasBoundTextElement } from "./typeChecks"; + +describe("Test TypeChecks", () => { + describe("Test hasBoundTextElement", () => { + it("should return true for text bindable containers with bound text", () => { + expect( + hasBoundTextElement( + API.createElement({ + type: "rectangle", + boundElements: [{ type: "text", id: "text-id" }], + }), + ), + ).toBeTruthy(); + + expect( + hasBoundTextElement( + API.createElement({ + type: "ellipse", + boundElements: [{ type: "text", id: "text-id" }], + }), + ), + ).toBeTruthy(); + + expect( + hasBoundTextElement( + API.createElement({ + type: "arrow", + boundElements: [{ type: "text", id: "text-id" }], + }), + ), + ).toBeTruthy(); + + expect( + hasBoundTextElement( + API.createElement({ + type: "image", + boundElements: [{ type: "text", id: "text-id" }], + }), + ), + ).toBeTruthy(); + }); + + it("should return false for text bindable containers without bound text", () => { + expect( + hasBoundTextElement( + API.createElement({ + type: "freedraw", + boundElements: [{ type: "arrow", id: "arrow-id" }], + }), + ), + ).toBeFalsy(); + }); + + it("should return false for non text bindable containers", () => { + expect( + hasBoundTextElement( + API.createElement({ + type: "freedraw", + boundElements: [{ type: "text", id: "text-id" }], + }), + ), + ).toBeFalsy(); + }); + }); +}); diff --git a/src/element/typeChecks.ts b/src/element/typeChecks.ts index a6b6cb2db..0f13648f9 100644 --- a/src/element/typeChecks.ts +++ b/src/element/typeChecks.ts @@ -139,7 +139,7 @@ export const hasBoundTextElement = ( element: ExcalidrawElement | null, ): element is MarkNonNullable => { return ( - isBindableElement(element) && + isTextBindableContainer(element) && !!element.boundElements?.some(({ type }) => type === "text") ); }; From d5b264c2d266dab14189c3acb5479e8a2797f6cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Mar 2023 14:51:36 +0530 Subject: [PATCH 02/13] build(deps): bump http-cache-semantics from 4.1.0 to 4.1.1 in /dev-docs (#6192) Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/kornelski/http-cache-semantics/releases) - [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1) --- updated-dependencies: - dependency-name: http-cache-semantics dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-docs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-docs/yarn.lock b/dev-docs/yarn.lock index 5aaa9689f..6b1bb178d 100644 --- a/dev-docs/yarn.lock +++ b/dev-docs/yarn.lock @@ -4376,9 +4376,9 @@ htmlparser2@^8.0.1: entities "^4.3.0" http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== http-deceiver@^1.2.7: version "1.2.7" From 15655acb5ab6902d096035f0bad4178fe0893c83 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 3 Mar 2023 12:58:36 +0100 Subject: [PATCH 03/13] fix: use jotai scope for editor-specific atoms (#6308) --- src/components/ActiveConfirmDialog.tsx | 2 ++ src/components/LibraryMenuHeaderContent.tsx | 1 + src/tests/shortcuts.test.tsx | 30 +++++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 src/tests/shortcuts.test.tsx diff --git a/src/components/ActiveConfirmDialog.tsx b/src/components/ActiveConfirmDialog.tsx index 3c79a5190..44a26e9a6 100644 --- a/src/components/ActiveConfirmDialog.tsx +++ b/src/components/ActiveConfirmDialog.tsx @@ -1,6 +1,7 @@ import { atom, useAtom } from "jotai"; import { actionClearCanvas } from "../actions"; import { t } from "../i18n"; +import { jotaiScope } from "../jotai"; import { useExcalidrawActionManager } from "./App"; import ConfirmDialog from "./ConfirmDialog"; @@ -9,6 +10,7 @@ export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null); export const ActiveConfirmDialog = () => { const [activeConfirmDialog, setActiveConfirmDialog] = useAtom( activeConfirmDialogAtom, + jotaiScope, ); const actionManager = useExcalidrawActionManager(); diff --git a/src/components/LibraryMenuHeaderContent.tsx b/src/components/LibraryMenuHeaderContent.tsx index 43ea96899..8e6b93523 100644 --- a/src/components/LibraryMenuHeaderContent.tsx +++ b/src/components/LibraryMenuHeaderContent.tsx @@ -48,6 +48,7 @@ export const LibraryMenuHeader: React.FC<{ const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope); const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom( isLibraryMenuOpenAtom, + jotaiScope, ); const renderRemoveLibAlert = useCallback(() => { const content = selectedItems.length diff --git a/src/tests/shortcuts.test.tsx b/src/tests/shortcuts.test.tsx new file mode 100644 index 000000000..e0b4fcc58 --- /dev/null +++ b/src/tests/shortcuts.test.tsx @@ -0,0 +1,30 @@ +import { KEYS } from "../keys"; +import { Excalidraw } from "../packages/excalidraw/entry"; +import { API } from "./helpers/api"; +import { Keyboard } from "./helpers/ui"; +import { fireEvent, render, waitFor } from "./test-utils"; + +describe("shortcuts", () => { + it("Clear canvas shortcut should display confirm dialog", async () => { + await render( + , + ); + + expect(window.h.elements.length).toBe(1); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyDown(KEYS.DELETE); + }); + const confirmDialog = document.querySelector(".confirm-dialog")!; + expect(confirmDialog).not.toBe(null); + + fireEvent.click(confirmDialog.querySelector('[aria-label="Confirm"]')!); + + await waitFor(() => { + expect(window.h.elements[0].isDeleted).toBe(true); + }); + }); +}); From 1ce933d2f59c1c459b16646a5961fa5d8fd88525 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Fri, 3 Mar 2023 17:34:11 +0530 Subject: [PATCH 04/13] fix: compute bounding box correctly for text element when multiple element resizing (#6307) --- src/element/resizeElements.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index 0c0337ed7..05eb630ed 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -693,7 +693,9 @@ const resizeMultipleElements = ( }; const fontSize = measureFontSizeFromWidth( boundTextElement ?? (element.orig as ExcalidrawTextElement), - getMaxContainerWidth(updatedElement), + boundTextElement + ? getMaxContainerWidth(updatedElement) + : updatedElement.width, ); if (!fontSize) { From 0f06fa3851c87f6e2206bcf0a184fabe379f2671 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Fri, 3 Mar 2023 17:40:42 +0530 Subject: [PATCH 05/13] feat: create bound container from text (#6301) * feat: create container from text * fix lint and spec * fix * round off dims * ceil * review fixes * fix * Add specs * fix * fix z-index and type * consider group * consider linear bindings * lint --- src/actions/actionBoundText.tsx | 166 ++++++++++++++++-- src/actions/types.ts | 3 +- src/components/App.tsx | 2 + src/element/textElement.test.ts | 23 ++- src/element/textElement.ts | 61 +++---- src/element/textWysiwyg.test.tsx | 74 ++++++++ src/global.d.ts | 2 + src/locales/en.json | 1 + .../__snapshots__/contextmenu.test.tsx.snap | 45 +++++ 9 files changed, 324 insertions(+), 53 deletions(-) diff --git a/src/actions/actionBoundText.tsx b/src/actions/actionBoundText.tsx index 7849730d8..c3ebc920b 100644 --- a/src/actions/actionBoundText.tsx +++ b/src/actions/actionBoundText.tsx @@ -1,7 +1,13 @@ -import { VERTICAL_ALIGN } from "../constants"; -import { getNonDeletedElements, isTextElement } from "../element"; +import { + BOUND_TEXT_PADDING, + ROUNDNESS, + TEXT_ALIGN, + VERTICAL_ALIGN, +} from "../constants"; +import { getNonDeletedElements, isTextElement, newElement } from "../element"; import { mutateElement } from "../element/mutateElement"; import { + computeContainerDimensionForBoundText, getBoundTextElement, measureText, redrawTextBoundingBox, @@ -13,8 +19,11 @@ import { import { hasBoundTextElement, isTextBindableContainer, + isUsingAdaptiveRadius, } from "../element/typeChecks"; import { + ExcalidrawElement, + ExcalidrawLinearElement, ExcalidrawTextContainer, ExcalidrawTextElement, } from "../element/types"; @@ -129,19 +138,152 @@ export const actionBindText = register({ }), }); redrawTextBoundingBox(textElement, container); - const updatedElements = elements.slice(); - const textElementIndex = updatedElements.findIndex( - (ele) => ele.id === textElement.id, - ); - updatedElements.splice(textElementIndex, 1); - const containerIndex = updatedElements.findIndex( - (ele) => ele.id === container.id, - ); - updatedElements.splice(containerIndex + 1, 0, textElement); + return { - elements: updatedElements, + elements: pushTextAboveContainer(elements, container, textElement), appState: { ...appState, selectedElementIds: { [container.id]: true } }, commitToHistory: true, }; }, }); + +const pushTextAboveContainer = ( + elements: readonly ExcalidrawElement[], + container: ExcalidrawElement, + textElement: ExcalidrawTextElement, +) => { + const updatedElements = elements.slice(); + const textElementIndex = updatedElements.findIndex( + (ele) => ele.id === textElement.id, + ); + updatedElements.splice(textElementIndex, 1); + + const containerIndex = updatedElements.findIndex( + (ele) => ele.id === container.id, + ); + updatedElements.splice(containerIndex + 1, 0, textElement); + return updatedElements; +}; + +const pushContainerBelowText = ( + elements: readonly ExcalidrawElement[], + container: ExcalidrawElement, + textElement: ExcalidrawTextElement, +) => { + const updatedElements = elements.slice(); + const containerIndex = updatedElements.findIndex( + (ele) => ele.id === container.id, + ); + updatedElements.splice(containerIndex, 1); + + const textElementIndex = updatedElements.findIndex( + (ele) => ele.id === textElement.id, + ); + updatedElements.splice(textElementIndex, 0, container); + return updatedElements; +}; + +export const actionCreateContainerFromText = register({ + name: "createContainerFromText", + contextItemLabel: "labels.createContainerFromText", + trackEvent: { category: "element" }, + predicate: (elements, appState) => { + const selectedElements = getSelectedElements(elements, appState); + return selectedElements.length === 1 && isTextElement(selectedElements[0]); + }, + perform: (elements, appState) => { + const selectedElements = getSelectedElements( + getNonDeletedElements(elements), + appState, + ); + const updatedElements = elements.slice(); + if (selectedElements.length === 1 && isTextElement(selectedElements[0])) { + const textElement = selectedElements[0]; + const container = newElement({ + type: "rectangle", + backgroundColor: appState.currentItemBackgroundColor, + boundElements: [ + ...(textElement.boundElements || []), + { id: textElement.id, type: "text" }, + ], + angle: textElement.angle, + fillStyle: appState.currentItemFillStyle, + strokeColor: appState.currentItemStrokeColor, + roughness: appState.currentItemRoughness, + strokeWidth: appState.currentItemStrokeWidth, + strokeStyle: appState.currentItemStrokeStyle, + roundness: + appState.currentItemRoundness === "round" + ? { + type: isUsingAdaptiveRadius("rectangle") + ? ROUNDNESS.ADAPTIVE_RADIUS + : ROUNDNESS.PROPORTIONAL_RADIUS, + } + : null, + opacity: 100, + locked: false, + x: textElement.x - BOUND_TEXT_PADDING, + y: textElement.y - BOUND_TEXT_PADDING, + width: computeContainerDimensionForBoundText( + textElement.width, + "rectangle", + ), + height: computeContainerDimensionForBoundText( + textElement.height, + "rectangle", + ), + groupIds: textElement.groupIds, + }); + + // update bindings + if (textElement.boundElements?.length) { + const linearElementIds = textElement.boundElements + .filter((ele) => ele.type === "arrow") + .map((el) => el.id); + const linearElements = updatedElements.filter((ele) => + linearElementIds.includes(ele.id), + ) as ExcalidrawLinearElement[]; + linearElements.forEach((ele) => { + let startBinding = null; + let endBinding = null; + if (ele.startBinding) { + startBinding = { ...ele.startBinding, elementId: container.id }; + } + if (ele.endBinding) { + endBinding = { ...ele.endBinding, elementId: container.id }; + } + mutateElement(ele, { startBinding, endBinding }); + }); + } + + mutateElement(textElement, { + containerId: container.id, + verticalAlign: VERTICAL_ALIGN.MIDDLE, + textAlign: TEXT_ALIGN.CENTER, + boundElements: null, + }); + redrawTextBoundingBox(textElement, container); + + return { + elements: pushContainerBelowText( + [...elements, container], + container, + textElement, + ), + appState: { + ...appState, + selectedElementIds: { + [container.id]: true, + [textElement.id]: false, + }, + }, + commitToHistory: true, + }; + } + return { + elements: updatedElements, + appState, + commitToHistory: true, + }; + }, +}); diff --git a/src/actions/types.ts b/src/actions/types.ts index 54bd5a26f..baa37eaad 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -113,7 +113,8 @@ export type ActionName = | "toggleLock" | "toggleLinearEditor" | "toggleEraserTool" - | "toggleHandTool"; + | "toggleHandTool" + | "createContainerFromText"; export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; diff --git a/src/components/App.tsx b/src/components/App.tsx index 24da7d85a..a8240f127 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -284,6 +284,7 @@ import { actionPaste } from "../actions/actionClipboard"; import { actionToggleHandTool } from "../actions/actionCanvas"; import { jotaiStore } from "../jotai"; import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; +import { actionCreateContainerFromText } from "../actions/actionBoundText"; const deviceContextInitialValue = { isSmScreen: false, @@ -6237,6 +6238,7 @@ class App extends React.Component { actionGroup, actionUnbindText, actionBindText, + actionCreateContainerFromText, actionUngroup, CONTEXT_MENU_SEPARATOR, actionAddToLibrary, diff --git a/src/element/textElement.test.ts b/src/element/textElement.test.ts index 87219b298..7bc361b4d 100644 --- a/src/element/textElement.test.ts +++ b/src/element/textElement.test.ts @@ -1,7 +1,7 @@ import { BOUND_TEXT_PADDING } from "../constants"; import { API } from "../tests/helpers/api"; import { - computeContainerHeightForBoundText, + computeContainerDimensionForBoundText, getContainerCoords, getMaxContainerWidth, getMaxContainerHeight, @@ -35,10 +35,11 @@ describe("Test wrapText", () => { describe("When text doesn't contain new lines", () => { const text = "Hello whats up"; + [ { desc: "break all words when width of each word is less than container width", - width: 90, + width: 80, res: `Hello whats up`, @@ -62,7 +63,7 @@ p`, { desc: "break words as per the width", - width: 150, + width: 140, res: `Hello whats up`, }, @@ -93,7 +94,7 @@ whats up`; [ { desc: "break all words when width of each word is less than container width", - width: 90, + width: 80, res: `Hello whats up`, @@ -214,7 +215,7 @@ describe("Test measureText", () => { }); }); - describe("Test computeContainerHeightForBoundText", () => { + describe("Test computeContainerDimensionForBoundText", () => { const params = { width: 178, height: 194, @@ -225,7 +226,9 @@ describe("Test measureText", () => { type: "rectangle", ...params, }); - expect(computeContainerHeightForBoundText(element, 150)).toEqual(160); + expect(computeContainerDimensionForBoundText(150, element.type)).toEqual( + 160, + ); }); it("should compute container height correctly for ellipse", () => { @@ -233,7 +236,9 @@ describe("Test measureText", () => { type: "ellipse", ...params, }); - expect(computeContainerHeightForBoundText(element, 150)).toEqual(226); + expect(computeContainerDimensionForBoundText(150, element.type)).toEqual( + 226, + ); }); it("should compute container height correctly for diamond", () => { @@ -241,7 +246,9 @@ describe("Test measureText", () => { type: "diamond", ...params, }); - expect(computeContainerHeightForBoundText(element, 150)).toEqual(320); + expect(computeContainerDimensionForBoundText(150, element.type)).toEqual( + 320, + ); }); }); diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 4d9fa5eb5..a4c494792 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -12,11 +12,7 @@ import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants"; import { MaybeTransformHandleType } from "./transformHandles"; import Scene from "../scene/Scene"; import { isTextElement } from "."; -import { - isBoundToContainer, - isImageElement, - isArrowElement, -} from "./typeChecks"; +import { isBoundToContainer, isArrowElement } from "./typeChecks"; import { LinearElementEditor } from "./linearElementEditor"; import { AppState } from "../types"; import { isTextBindableContainer } from "./typeChecks"; @@ -84,9 +80,9 @@ export const redrawTextBoundingBox = ( let nextHeight = containerDims.height; if (metrics.height > maxContainerHeight) { - nextHeight = computeContainerHeightForBoundText( - container, + nextHeight = computeContainerDimensionForBoundText( metrics.height, + container.type, ); mutateElement(container, { height: nextHeight }); maxContainerHeight = getMaxContainerHeight(container); @@ -188,9 +184,9 @@ export const handleBindTextResize = ( } // increase height in case text element height exceeds if (nextHeight > maxHeight) { - containerHeight = computeContainerHeightForBoundText( - container, + containerHeight = computeContainerDimensionForBoundText( nextHeight, + container.type, ); const diff = containerHeight - containerDims.height; @@ -324,7 +320,6 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => { const lines: Array = []; const originalLines = text.split("\n"); const spaceWidth = getLineWidth(" ", font); - const push = (str: string) => { if (str.trim()) { lines.push(str); @@ -398,7 +393,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => { const word = words[index]; currentLineWidthTillNow = getLineWidth(currentLine + word, font); - if (currentLineWidthTillNow >= maxWidth) { + if (currentLineWidthTillNow > maxWidth) { push(currentLine); currentLineWidthTillNow = 0; currentLine = ""; @@ -714,32 +709,34 @@ export const getTextBindableContainerAtPosition = ( return isTextBindableContainer(hitElement, false) ? hitElement : null; }; -export const isValidTextContainer = (element: ExcalidrawElement) => { - return ( - element.type === "rectangle" || - element.type === "ellipse" || - element.type === "diamond" || - isImageElement(element) || - isArrowElement(element) - ); -}; +const VALID_CONTAINER_TYPES = new Set([ + "rectangle", + "ellipse", + "diamond", + "image", + "arrow", +]); -export const computeContainerHeightForBoundText = ( - container: NonDeletedExcalidrawElement, - boundTextElementHeight: number, +export const isValidTextContainer = (element: ExcalidrawElement) => + VALID_CONTAINER_TYPES.has(element.type); + +export const computeContainerDimensionForBoundText = ( + dimension: number, + containerType: ExtractSetType, ) => { - if (container.type === "ellipse") { - return Math.round( - ((boundTextElementHeight + BOUND_TEXT_PADDING * 2) / Math.sqrt(2)) * 2, - ); + dimension = Math.ceil(dimension); + const padding = BOUND_TEXT_PADDING * 2; + + if (containerType === "ellipse") { + return Math.round(((dimension + padding) / Math.sqrt(2)) * 2); } - if (isArrowElement(container)) { - return boundTextElementHeight + BOUND_TEXT_PADDING * 8 * 2; + if (containerType === "arrow") { + return dimension + padding * 8; } - if (container.type === "diamond") { - return 2 * (boundTextElementHeight + BOUND_TEXT_PADDING * 2); + if (containerType === "diamond") { + return 2 * (dimension + padding); } - return boundTextElementHeight + BOUND_TEXT_PADDING * 2; + return dimension + padding; }; export const getMaxContainerWidth = (container: ExcalidrawElement) => { diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index 7de71198b..48138ea02 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -19,6 +19,7 @@ import { API } from "../tests/helpers/api"; import { mutateElement } from "./mutateElement"; import { resize } from "../tests/utils"; import { getOriginalContainerHeightFromCache } from "./textWysiwyg"; + // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); @@ -1307,5 +1308,78 @@ describe("textWysiwyg", () => { `); }); }); + + it("should wrap text in a container when wrap text in container triggered from context menu", async () => { + UI.clickTool("text"); + mouse.clickAt(20, 30); + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + + fireEvent.change(editor, { + target: { + value: "Excalidraw is an opensource virtual collaborative whiteboard", + }, + }); + + editor.dispatchEvent(new Event("input")); + await new Promise((cb) => setTimeout(cb, 0)); + editor.blur(); + expect(h.elements[1].width).toBe(600); + expect(h.elements[1].height).toBe(24); + expect((h.elements[1] as ExcalidrawTextElement).text).toBe( + "Excalidraw is an opensource virtual collaborative whiteboard", + ); + + API.setSelectedElements([h.elements[1]]); + + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 20, + clientY: 30, + }); + + const contextMenu = document.querySelector(".context-menu"); + fireEvent.click( + queryByText(contextMenu as HTMLElement, "Wrap text in a container")!, + ); + expect(h.elements.length).toBe(3); + + expect(h.elements[1]).toEqual( + expect.objectContaining({ + angle: 0, + backgroundColor: "transparent", + boundElements: [ + { + id: h.elements[2].id, + type: "text", + }, + ], + fillStyle: "hachure", + groupIds: [], + height: 34, + isDeleted: false, + link: null, + locked: false, + opacity: 100, + roughness: 1, + roundness: { + type: 3, + }, + strokeColor: "#000000", + strokeStyle: "solid", + strokeWidth: 1, + type: "rectangle", + updated: 1, + version: 1, + width: 610, + x: 15, + y: 25, + }), + ); + expect((h.elements[2] as ExcalidrawTextElement).text).toBe( + "Excalidraw is an opensource virtual collaborative whiteboard", + ); + }); }); }); diff --git a/src/global.d.ts b/src/global.d.ts index df7eeb37e..4ccd8f3fe 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -165,3 +165,5 @@ declare module "image-blob-reduce" { const reduce: ImageBlobReduce.ImageBlobReduceStatic; export = reduce; } + +type ExtractSetType> = T extends Set ? U : never; diff --git a/src/locales/en.json b/src/locales/en.json index 31005cb4c..f5ae003f3 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -110,6 +110,7 @@ "increaseFontSize": "Increase font size", "unbindText": "Unbind text", "bindText": "Bind text to the container", + "createContainerFromText": "Wrap text in a container", "link": { "edit": "Edit link", "create": "Create link", diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index 326cde0ed..18656edd1 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/src/tests/__snapshots__/contextmenu.test.tsx.snap @@ -119,6 +119,15 @@ Object { "category": "element", }, }, + Object { + "contextItemLabel": "labels.createContainerFromText", + "name": "createContainerFromText", + "perform": [Function], + "predicate": [Function], + "trackEvent": Object { + "category": "element", + }, + }, Object { "PanelComponent": [Function], "contextItemLabel": "labels.ungroup", @@ -4507,6 +4516,15 @@ Object { "category": "element", }, }, + Object { + "contextItemLabel": "labels.createContainerFromText", + "name": "createContainerFromText", + "perform": [Function], + "predicate": [Function], + "trackEvent": Object { + "category": "element", + }, + }, Object { "PanelComponent": [Function], "contextItemLabel": "labels.ungroup", @@ -5048,6 +5066,15 @@ Object { "category": "element", }, }, + Object { + "contextItemLabel": "labels.createContainerFromText", + "name": "createContainerFromText", + "perform": [Function], + "predicate": [Function], + "trackEvent": Object { + "category": "element", + }, + }, Object { "PanelComponent": [Function], "contextItemLabel": "labels.ungroup", @@ -5888,6 +5915,15 @@ Object { "category": "element", }, }, + Object { + "contextItemLabel": "labels.createContainerFromText", + "name": "createContainerFromText", + "perform": [Function], + "predicate": [Function], + "trackEvent": Object { + "category": "element", + }, + }, Object { "PanelComponent": [Function], "contextItemLabel": "labels.ungroup", @@ -6225,6 +6261,15 @@ Object { "category": "element", }, }, + Object { + "contextItemLabel": "labels.createContainerFromText", + "name": "createContainerFromText", + "perform": [Function], + "predicate": [Function], + "trackEvent": Object { + "category": "element", + }, + }, Object { "PanelComponent": [Function], "contextItemLabel": "labels.ungroup", From 05ffce62ef16c0cff9d10d8180b282bd5fdd121d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Mar 2023 13:20:53 +0100 Subject: [PATCH 06/13] build(deps): bump dns-packet from 5.3.1 to 5.4.0 in /src/packages/excalidraw (#6305) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/packages/excalidraw/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/packages/excalidraw/yarn.lock b/src/packages/excalidraw/yarn.lock index 09838174b..320b26a54 100644 --- a/src/packages/excalidraw/yarn.lock +++ b/src/packages/excalidraw/yarn.lock @@ -2032,9 +2032,9 @@ dns-equal@^1.0.0: integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= dns-packet@^5.2.2: - version "5.3.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.3.1.tgz#eb94413789daec0f0ebe2fcc230bdc9d7c91b43d" - integrity sha512-spBwIj0TK0Ey3666GwIdWVfUpLyubpU53BTCu8iPn4r4oXd9O14Hjg3EHw3ts2oed77/SeckunUYCyRlSngqHw== + version "5.4.0" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b" + integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g== dependencies: "@leichtgewicht/ip-codec" "^2.0.1" From 9f9666110e04122237ee842ae603b5283244d5e9 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Fri, 3 Mar 2023 18:01:55 +0530 Subject: [PATCH 07/13] chore: Add debug flag to enable text container bounding box (#6296) * debug: Add debug flag to enable text container bounding box * newline * fix --- .env.development | 5 +++++ src/renderer/renderElement.ts | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/.env.development b/.env.development index 72b67ecea..397a56565 100644 --- a/.env.development +++ b/.env.development @@ -22,3 +22,8 @@ REACT_APP_DEV_ENABLE_SW= REACT_APP_DEV_DISABLE_LIVE_RELOAD= FAST_REFRESH=false + +#Debug flags + +# To enable bounding box for text containers +REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX= diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index d39b9fbb6..e49a1f465 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -14,6 +14,7 @@ import { isFreeDrawElement, isInitializedImageElement, isArrowElement, + hasBoundTextElement, } from "../element/typeChecks"; import { getDiamondPoints, @@ -41,7 +42,10 @@ import { getStroke, StrokeOptions } from "perfect-freehand"; import { getApproxLineHeight, getBoundTextElement, + getContainerCoords, getContainerElement, + getMaxContainerHeight, + getMaxContainerWidth, } from "../element/textElement"; import { LinearElementEditor } from "../element/linearElementEditor"; @@ -811,6 +815,21 @@ const drawElementFromCanvas = ( elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom, elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom, ); + + if ( + process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX && + hasBoundTextElement(element) + ) { + const coords = getContainerCoords(element); + context.strokeStyle = "#c92a2a"; + context.lineWidth = 3; + context.strokeRect( + (coords.x + renderConfig.scrollX) * window.devicePixelRatio, + (coords.y + renderConfig.scrollY) * window.devicePixelRatio, + getMaxContainerWidth(element) * window.devicePixelRatio, + getMaxContainerHeight(element) * window.devicePixelRatio, + ); + } } context.restore(); From 5c0b15ce2b7f4f3ad80892bff55246a55995778f Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Fri, 3 Mar 2023 18:07:26 +0530 Subject: [PATCH 08/13] fix: respect text align when wrapping in a container (#6310) * fix: respect text align when wrapping in a container * fix --- src/actions/actionBoundText.tsx | 8 +------- src/element/textWysiwyg.test.tsx | 27 ++++++++++++++++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/actions/actionBoundText.tsx b/src/actions/actionBoundText.tsx index c3ebc920b..94384134d 100644 --- a/src/actions/actionBoundText.tsx +++ b/src/actions/actionBoundText.tsx @@ -1,9 +1,4 @@ -import { - BOUND_TEXT_PADDING, - ROUNDNESS, - TEXT_ALIGN, - VERTICAL_ALIGN, -} from "../constants"; +import { BOUND_TEXT_PADDING, ROUNDNESS, VERTICAL_ALIGN } from "../constants"; import { getNonDeletedElements, isTextElement, newElement } from "../element"; import { mutateElement } from "../element/mutateElement"; import { @@ -259,7 +254,6 @@ export const actionCreateContainerFromText = register({ mutateElement(textElement, { containerId: container.id, verticalAlign: VERTICAL_ALIGN.MIDDLE, - textAlign: TEXT_ALIGN.CENTER, boundElements: null, }); redrawTextBoundingBox(textElement, container); diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index 48138ea02..25d266306 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -10,7 +10,7 @@ import { } from "../tests/test-utils"; import { queryByText } from "@testing-library/react"; -import { FONT_FAMILY } from "../constants"; +import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants"; import { ExcalidrawTextElement, ExcalidrawTextElementWithContainer, @@ -1324,14 +1324,22 @@ describe("textWysiwyg", () => { editor.dispatchEvent(new Event("input")); await new Promise((cb) => setTimeout(cb, 0)); + + editor.select(); + fireEvent.click(screen.getByTitle("Left")); + await new Promise((r) => setTimeout(r, 0)); + editor.blur(); - expect(h.elements[1].width).toBe(600); - expect(h.elements[1].height).toBe(24); - expect((h.elements[1] as ExcalidrawTextElement).text).toBe( + + const textElement = h.elements[1] as ExcalidrawTextElement; + expect(textElement.width).toBe(600); + expect(textElement.height).toBe(24); + expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT); + expect((textElement as ExcalidrawTextElement).text).toBe( "Excalidraw is an opensource virtual collaborative whiteboard", ); - API.setSelectedElements([h.elements[1]]); + API.setSelectedElements([textElement]); fireEvent.contextMenu(GlobalTestState.canvas, { button: 2, @@ -1377,8 +1385,13 @@ describe("textWysiwyg", () => { y: 25, }), ); - expect((h.elements[2] as ExcalidrawTextElement).text).toBe( - "Excalidraw is an opensource virtual collaborative whiteboard", + expect(h.elements[2] as ExcalidrawTextElement).toEqual( + expect.objectContaining({ + text: "Excalidraw is an opensource virtual collaborative whiteboard", + verticalAlign: VERTICAL_ALIGN.MIDDLE, + textAlign: TEXT_ALIGN.LEFT, + boundElements: null, + }), ); }); }); From 34a7d48b95ed87fce59cf0ab061e27f3575ac996 Mon Sep 17 00:00:00 2001 From: Omar Brikaa Date: Fri, 3 Mar 2023 14:50:18 +0200 Subject: [PATCH 09/13] fix: provide HelpButton title prop (#6209) Co-authored-by: dwelle --- src/components/HelpButton.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/HelpButton.tsx b/src/components/HelpButton.tsx index 40c130271..ce387244c 100644 --- a/src/components/HelpButton.tsx +++ b/src/components/HelpButton.tsx @@ -1,7 +1,7 @@ +import { t } from "../i18n"; import { HelpIcon } from "./icons"; type HelpButtonProps = { - title?: string; name?: string; id?: string; onClick?(): void; @@ -12,8 +12,8 @@ export const HelpButton = (props: HelpButtonProps) => ( className="help-icon" onClick={props.onClick} type="button" - title={`${props.title} — ?`} - aria-label={props.title} + title={`${t("helpDialog.title")} — ?`} + aria-label={t("helpDialog.title")} > {HelpIcon} From 3322f0fa6f4d65b84cbb3162accd039705e170ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Mar 2023 14:45:29 +0100 Subject: [PATCH 10/13] build(deps): bump @sideway/formula from 3.0.0 to 3.0.1 in /dev-docs (#6309) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-docs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-docs/yarn.lock b/dev-docs/yarn.lock index 6b1bb178d..f6d61d9b6 100644 --- a/dev-docs/yarn.lock +++ b/dev-docs/yarn.lock @@ -1785,9 +1785,9 @@ "@hapi/hoek" "^9.0.0" "@sideway/formula@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" - integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== "@sideway/pinpoint@^2.0.0": version "2.0.0" From cef6094d4c597c0a0f592f0abe46ac3bfae9ba1a Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 3 Mar 2023 16:19:02 +0100 Subject: [PATCH 11/13] fix: more jotai scopes missing (#6313) --- src/components/ConfirmDialog.tsx | 3 ++- src/components/Dialog.tsx | 3 ++- src/components/main-menu/DefaultItems.tsx | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx index 9a1a91039..aebb42de7 100644 --- a/src/components/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog.tsx @@ -6,6 +6,7 @@ import DialogActionButton from "./DialogActionButton"; import { useSetAtom } from "jotai"; import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent"; import { useExcalidrawSetAppState } from "./App"; +import { jotaiScope } from "../jotai"; interface Props extends Omit { onConfirm: () => void; @@ -24,7 +25,7 @@ const ConfirmDialog = (props: Props) => { ...rest } = props; const setAppState = useExcalidrawSetAppState(); - const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom); + const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope); return ( { }, [islandNode, props.autofocus]); const setAppState = useExcalidrawSetAppState(); - const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom); + const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope); const onClose = () => { setAppState({ openMenu: null }); diff --git a/src/components/main-menu/DefaultItems.tsx b/src/components/main-menu/DefaultItems.tsx index b3cc23b90..d02719a2c 100644 --- a/src/components/main-menu/DefaultItems.tsx +++ b/src/components/main-menu/DefaultItems.tsx @@ -31,6 +31,7 @@ import "./DefaultItems.scss"; import clsx from "clsx"; import { useSetAtom } from "jotai"; import { activeConfirmDialogAtom } from "../ActiveConfirmDialog"; +import { jotaiScope } from "../../jotai"; export const LoadScene = () => { const { t } = useI18n(); @@ -113,7 +114,10 @@ Help.displayName = "Help"; export const ClearCanvas = () => { const { t } = useI18n(); - const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom); + const setActiveConfirmDialog = useSetAtom( + activeConfirmDialogAtom, + jotaiScope, + ); const actionManager = useExcalidrawActionManager(); if (!actionManager.isActionEnabled(actionClearCanvas)) { From 8542c95a7a2b30d88b9a4f71867651fe60a4c7da Mon Sep 17 00:00:00 2001 From: David Luzar Date: Sat, 4 Mar 2023 19:21:57 +0100 Subject: [PATCH 12/13] fix: move utility types out of `.d.ts` file to fix exported declaration files (#6315) --- src/actions/shortcuts.ts | 1 + src/actions/types.ts | 1 + src/data/blob.ts | 1 + src/data/restore.ts | 1 + src/element/bounds.ts | 1 + src/element/collision.ts | 1 + src/element/linearElementEditor.ts | 1 + src/element/mutateElement.ts | 1 + src/element/newElement.ts | 1 + src/element/textElement.ts | 1 + src/element/typeChecks.ts | 1 + src/element/types.ts | 1 + src/excalidraw-app/data/firebase.ts | 1 + src/excalidraw-app/index.tsx | 1 + src/global.d.ts | 49 ----------------------------- src/history.ts | 1 + src/math.ts | 1 + src/tests/helpers/api.ts | 1 + src/types.ts | 1 + src/utility-types.ts | 49 +++++++++++++++++++++++++++++ src/utils.ts | 1 + 21 files changed, 68 insertions(+), 49 deletions(-) create mode 100644 src/utility-types.ts diff --git a/src/actions/shortcuts.ts b/src/actions/shortcuts.ts index ba66c5a75..be48c6470 100644 --- a/src/actions/shortcuts.ts +++ b/src/actions/shortcuts.ts @@ -1,5 +1,6 @@ import { isDarwin } from "../constants"; import { t } from "../i18n"; +import { SubtypeOf } from "../utility-types"; import { getShortcutKey } from "../utils"; import { ActionName } from "./types"; diff --git a/src/actions/types.ts b/src/actions/types.ts index baa37eaad..e46cd2ab8 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -6,6 +6,7 @@ import { ExcalidrawProps, BinaryFiles, } from "../types"; +import { MarkOptional } from "../utility-types"; export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api"; diff --git a/src/data/blob.ts b/src/data/blob.ts index 473042b56..35c040ef3 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -7,6 +7,7 @@ import { CanvasError } from "../errors"; import { t } from "../i18n"; import { calculateScrollCenter } from "../scene"; import { AppState, DataURL, LibraryItem } from "../types"; +import { ValueOf } from "../utility-types"; import { bytesToHexString } from "../utils"; import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem"; import { isValidExcalidrawData, isValidLibrary } from "./json"; diff --git a/src/data/restore.ts b/src/data/restore.ts index 4434b7bc3..98df4547b 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -34,6 +34,7 @@ import { bumpVersion } from "../element/mutateElement"; import { getUpdatedTimestamp, updateActiveTool } from "../utils"; import { arrayToMap } from "../utils"; import oc from "open-color"; +import { MarkOptional, Mutable } from "../utility-types"; type RestoredAppState = Omit< AppState, diff --git a/src/element/bounds.ts b/src/element/bounds.ts index 2eab1d93d..3245ca3fc 100644 --- a/src/element/bounds.ts +++ b/src/element/bounds.ts @@ -23,6 +23,7 @@ import { import { rescalePoints } from "../points"; import { getBoundTextElement, getContainerElement } from "./textElement"; import { LinearElementEditor } from "./linearElementEditor"; +import { Mutable } from "../utility-types"; // x and y position of top left corner, x and y position of bottom right corner export type Bounds = readonly [number, number, number, number]; diff --git a/src/element/collision.ts b/src/element/collision.ts index 54540ae5e..097b76d34 100644 --- a/src/element/collision.ts +++ b/src/element/collision.ts @@ -38,6 +38,7 @@ import { isTextElement } from "."; import { isTransparent } from "../utils"; import { shouldShowBoundingBox } from "./transformHandles"; import { getBoundTextElement } from "./textElement"; +import { Mutable } from "../utility-types"; const isElementDraggableFromInside = ( element: NonDeletedExcalidrawElement, diff --git a/src/element/linearElementEditor.ts b/src/element/linearElementEditor.ts index 5c478515d..aedc25974 100644 --- a/src/element/linearElementEditor.ts +++ b/src/element/linearElementEditor.ts @@ -41,6 +41,7 @@ import { shouldRotateWithDiscreteAngle } from "../keys"; import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getShapeForElement } from "../renderer/renderElement"; import { DRAGGING_THRESHOLD } from "../constants"; +import { Mutable } from "../utility-types"; const editorMidPointsCache: { version: number | null; diff --git a/src/element/mutateElement.ts b/src/element/mutateElement.ts index 52038c163..1c3d66121 100644 --- a/src/element/mutateElement.ts +++ b/src/element/mutateElement.ts @@ -5,6 +5,7 @@ import { getSizeFromPoints } from "../points"; import { randomInteger } from "../random"; import { Point } from "../types"; import { getUpdatedTimestamp } from "../utils"; +import { Mutable } from "../utility-types"; type ElementUpdate = Omit< Partial, diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 0c7c90d2b..0fe16b553 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -32,6 +32,7 @@ import { } from "./textElement"; import { VERTICAL_ALIGN } from "../constants"; import { isArrowElement } from "./typeChecks"; +import { MarkOptional, Merge, Mutable } from "../utility-types"; type ElementConstructorOpts = MarkOptional< Omit, diff --git a/src/element/textElement.ts b/src/element/textElement.ts index a4c494792..18b96790b 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -23,6 +23,7 @@ import { resetOriginalContainerCache, updateOriginalContainerCache, } from "./textWysiwyg"; +import { ExtractSetType } from "../utility-types"; export const normalizeText = (text: string) => { return ( diff --git a/src/element/typeChecks.ts b/src/element/typeChecks.ts index 0f13648f9..164fafe68 100644 --- a/src/element/typeChecks.ts +++ b/src/element/typeChecks.ts @@ -1,5 +1,6 @@ import { ROUNDNESS } from "../constants"; import { AppState } from "../types"; +import { MarkNonNullable } from "../utility-types"; import { ExcalidrawElement, ExcalidrawTextElement, diff --git a/src/element/types.ts b/src/element/types.ts index af78f771e..e99b7e897 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -6,6 +6,7 @@ import { THEME, VERTICAL_ALIGN, } from "../constants"; +import { MarkNonNullable, ValueOf } from "../utility-types"; export type ChartType = "bar" | "line"; export type FillStyle = "hachure" | "cross-hatch" | "solid"; diff --git a/src/excalidraw-app/data/firebase.ts b/src/excalidraw-app/data/firebase.ts index b6b262497..02e14466f 100644 --- a/src/excalidraw-app/data/firebase.ts +++ b/src/excalidraw-app/data/firebase.ts @@ -14,6 +14,7 @@ import { encryptData, decryptData } from "../../data/encryption"; import { MIME_TYPES } from "../../constants"; import { reconcileElements } from "../collab/reconciliation"; import { getSyncableElements, SyncableExcalidrawElement } from "."; +import { ResolutionType } from "../../utility-types"; // private // ----------------------------------------------------------------------------- diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index 2b9101ad1..1c51637e0 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -85,6 +85,7 @@ import { useAtomWithInitialValue } from "../jotai"; import { appJotaiStore } from "./app-jotai"; import "./index.scss"; +import { ResolutionType } from "../utility-types"; polyfill(); diff --git a/src/global.d.ts b/src/global.d.ts index 4ccd8f3fe..4a70443d4 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -50,36 +50,6 @@ interface Clipboard extends EventTarget { write(data: any[]): Promise; } -type Mutable = { - -readonly [P in keyof T]: T[P]; -}; - -type ValueOf = T[keyof T]; - -type Merge = Omit & N; - -/** utility type to assert that the second type is a subtype of the first type. - * Returns the subtype. */ -type SubtypeOf = Subtype; - -type ResolutionType any> = T extends ( - ...args: any -) => Promise - ? R - : any; - -// https://github.com/krzkaczor/ts-essentials -type MarkOptional = Omit & Partial>; - -type MarkRequired = Exclude & - Required>; - -type MarkNonNullable = { - [P in K]-?: P extends K ? NonNullable : T[P]; -} & { [P in keyof T]: T[P] }; - -type NonOptional = Exclude; - // PNG encoding/decoding // ----------------------------------------------------------------------------- type TEXtChunk = { name: "tEXt"; data: Uint8Array }; @@ -101,23 +71,6 @@ declare module "png-chunks-extract" { } // ----------------------------------------------------------------------------- -// ----------------------------------------------------------------------------- -// type getter for interface's callable type -// src: https://stackoverflow.com/a/58658851/927631 -// ----------------------------------------------------------------------------- -type SignatureType = T extends (...args: infer R) => any ? R : never; -type CallableType any> = ( - ...args: SignatureType -) => ReturnType; -// --------------------------------------------------------------------------— - -// Type for React.forwardRef --- supply only the first generic argument T -type ForwardRef = Parameters< - CallableType> ->[1]; - -// --------------------------------------------------------------------------— - interface Blob { handle?: import("browser-fs-acces").FileSystemHandle; name?: string; @@ -165,5 +118,3 @@ declare module "image-blob-reduce" { const reduce: ImageBlobReduce.ImageBlobReduceStatic; export = reduce; } - -type ExtractSetType> = T extends Set ? U : never; diff --git a/src/history.ts b/src/history.ts index cc620cae1..d102a7ecc 100644 --- a/src/history.ts +++ b/src/history.ts @@ -2,6 +2,7 @@ import { AppState } from "./types"; import { ExcalidrawElement } from "./element/types"; import { isLinearElement } from "./element/typeChecks"; import { deepCopyElement } from "./element/newElement"; +import { Mutable } from "./utility-types"; export interface HistoryEntry { appState: ReturnType; diff --git a/src/math.ts b/src/math.ts index cfa28e230..602fe976c 100644 --- a/src/math.ts +++ b/src/math.ts @@ -12,6 +12,7 @@ import { } from "./element/types"; import { getShapeForElement } from "./renderer/renderElement"; import { getCurvePathOps } from "./element/bounds"; +import { Mutable } from "./utility-types"; export const rotate = ( x1: number, diff --git a/src/tests/helpers/api.ts b/src/tests/helpers/api.ts index 7f3e958ca..a0feab2f2 100644 --- a/src/tests/helpers/api.ts +++ b/src/tests/helpers/api.ts @@ -19,6 +19,7 @@ import { newFreeDrawElement, newImageElement } from "../../element/newElement"; import { Point } from "../../types"; import { getSelectedElements } from "../../scene/selection"; import { isLinearElementType } from "../../element/typeChecks"; +import { Mutable } from "../../utility-types"; const readFile = util.promisify(fs.readFile); diff --git a/src/types.ts b/src/types.ts index 486ec7146..e40476ead 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,6 +31,7 @@ import Library from "./data/library"; import type { FileSystemHandle } from "./data/filesystem"; import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants"; import { ContextMenuItems } from "./components/ContextMenu"; +import { Merge, ForwardRef } from "./utility-types"; export type Point = Readonly; diff --git a/src/utility-types.ts b/src/utility-types.ts new file mode 100644 index 000000000..b84eb1994 --- /dev/null +++ b/src/utility-types.ts @@ -0,0 +1,49 @@ +export type Mutable = { + -readonly [P in keyof T]: T[P]; +}; + +export type ValueOf = T[keyof T]; + +export type Merge = Omit & N; + +/** utility type to assert that the second type is a subtype of the first type. + * Returns the subtype. */ +export type SubtypeOf = Subtype; + +export type ResolutionType any> = T extends ( + ...args: any +) => Promise + ? R + : any; + +// https://github.com/krzkaczor/ts-essentials +export type MarkOptional = Omit & + Partial>; + +export type MarkRequired = Exclude & + Required>; + +export type MarkNonNullable = { + [P in K]-?: P extends K ? NonNullable : T[P]; +} & { [P in keyof T]: T[P] }; + +export type NonOptional = Exclude; + +// ----------------------------------------------------------------------------- +// type getter for interface's callable type +// src: https://stackoverflow.com/a/58658851/927631 +// ----------------------------------------------------------------------------- +export type SignatureType = T extends (...args: infer R) => any ? R : never; +export type CallableType any> = ( + ...args: SignatureType +) => ReturnType; +// --------------------------------------------------------------------------— + +// Type for React.forwardRef --- supply only the first generic argument T +export type ForwardRef = Parameters< + CallableType> +>[1]; + +export type ExtractSetType> = T extends Set + ? U + : never; diff --git a/src/utils.ts b/src/utils.ts index b2d85d4d3..61925a606 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -16,6 +16,7 @@ import { AppState, DataURL, LastActiveTool, Zoom } from "./types"; import { unstable_batchedUpdates } from "react-dom"; import { SHAPES } from "./shapes"; import { isEraserActive, isHandToolActive } from "./appState"; +import { ResolutionType } from "./utility-types"; let mockDateTime: string | null = null; From dd4c333925c48009cc1677d9688462141df0aceb Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 9 Mar 2023 13:07:36 +0530 Subject: [PATCH 13/13] fix: add an offset of 0.5px for text editor in containers (#6328) * fix: add an offset of 0.5px for text editor in containers * fix specs and lint --- src/element/textWysiwyg.tsx | 5 ++--- src/tests/__snapshots__/linearElementEditor.test.tsx.snap | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index bbe87ed23..e2810e81d 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -11,7 +11,7 @@ import { isBoundToContainer, isTextElement, } from "./typeChecks"; -import { CLASSES, isFirefox, isSafari, VERTICAL_ALIGN } from "../constants"; +import { CLASSES, VERTICAL_ALIGN } from "../constants"; import { ExcalidrawElement, ExcalidrawLinearElement, @@ -273,8 +273,7 @@ export const textWysiwyg = ({ if (!container) { maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value; textElementWidth = Math.min(textElementWidth, maxWidth); - } else if (isFirefox || isSafari) { - // As firefox, Safari needs little higher dimensions on DOM + } else { textElementWidth += 0.5; } // Make sure text editor height doesn't go beyond viewport diff --git a/src/tests/__snapshots__/linearElementEditor.test.tsx.snap b/src/tests/__snapshots__/linearElementEditor.test.tsx.snap index 5ea0ab4b1..f84eab3a2 100644 --- a/src/tests/__snapshots__/linearElementEditor.test.tsx.snap +++ b/src/tests/__snapshots__/linearElementEditor.test.tsx.snap @@ -5,7 +5,7 @@ exports[`Test Linear Elements Test bound text element should match styles for te class="excalidraw-wysiwyg" data-type="wysiwyg" dir="auto" - style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10px; height: 24px; left: 35px; top: 8px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -8px; font: Emoji 20px 20px; line-height: 24px; font-family: Virgil, Segoe UI Emoji;" + style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 24px; left: 35px; top: 8px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -8px; font: Emoji 20px 20px; line-height: 24px; font-family: Virgil, Segoe UI Emoji;" tabindex="0" wrap="off" />