From a4e5e46dd17e854fbb891d45edcb57405171c861 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Mon, 15 Jan 2024 14:52:04 +0530 Subject: [PATCH 01/31] fix: move default to last so its compatible with nextjs (#7561) --- packages/excalidraw/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/excalidraw/package.json b/packages/excalidraw/package.json index 7ec828cc1a..5e5c52b21a 100644 --- a/packages/excalidraw/package.json +++ b/packages/excalidraw/package.json @@ -7,8 +7,8 @@ "exports": { ".": { "development": "./dist/dev/index.js", - "default": "./dist/prod/index.js", - "types": "./dist/excalidraw/index.d.ts" + "types": "./dist/excalidraw/index.d.ts", + "default": "./dist/prod/index.js" }, "./index.css": { "development": "./dist/dev/index.css", From dd530737a2b3fd8c6c0ae645b552115e72d126c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=91CAT?= Date: Wed, 17 Jan 2024 19:49:42 +0900 Subject: [PATCH 02/31] docs: fix "canvas actions" link in Props page (#7536) fix "canvas actions" link in Props page --- dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx index 40773a1a2a..766c723e43 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/props/props.mdx @@ -23,7 +23,7 @@ All `props` are _optional_. | [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to | | [`theme`](#theme) | `"light"` | `"dark"` | `"light"` | The theme of the Excalidraw component | | [`name`](#name) | `string` | | Name of the drawing | -| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](#canvasactions) | +| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) | | [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. | | [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. | | [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load | From 3b0593baa79f49a8439831e6ea27f04c2db7acae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=91CAT?= Date: Fri, 19 Jan 2024 22:41:08 +0900 Subject: [PATCH 03/31] fix: Prevent the library label from being collapsed (#7579) --- packages/excalidraw/components/Sidebar/SidebarTrigger.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/excalidraw/components/Sidebar/SidebarTrigger.scss b/packages/excalidraw/components/Sidebar/SidebarTrigger.scss index 7197af7f22..5b003cdc5d 100644 --- a/packages/excalidraw/components/Sidebar/SidebarTrigger.scss +++ b/packages/excalidraw/components/Sidebar/SidebarTrigger.scss @@ -29,6 +29,7 @@ .default-sidebar-trigger .sidebar-trigger__label { display: block; + white-space: nowrap; } &.excalidraw--mobile .default-sidebar-trigger .sidebar-trigger__label { From 46da032626c36df89949683691930fca846609c0 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:41:22 +0100 Subject: [PATCH 04/31] fix: exporting frame-overlapping elements belonging to other frames (#7584) --- packages/excalidraw/components/App.tsx | 12 ++-- packages/excalidraw/data/index.ts | 8 +-- packages/excalidraw/frame.ts | 21 ++++++- packages/excalidraw/scene/export.ts | 8 +-- .../excalidraw/tests/scene/export.test.ts | 62 +++++++++++++++++++ 5 files changed, 92 insertions(+), 19 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index acbe567413..d9471d6572 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -348,6 +348,7 @@ import { updateFrameMembershipOfSelectedElements, isElementInFrame, getFrameLikeTitle, + getElementsOverlappingFrame, } from "../frame"; import { excludeElementsInFramesFromSelection, @@ -395,7 +396,7 @@ import { import { Emitter } from "../emitter"; import { ElementCanvasButtons } from "../element/ElementCanvasButtons"; import { MagicCacheData, diagramToHTML } from "../data/magic"; -import { elementsOverlappingBBox, exportToBlob } from "../../utils/export"; +import { exportToBlob } from "../../utils/export"; import { COLOR_PALETTE } from "../colors"; import { ElementCanvasButton } from "./MagicButton"; import { MagicIcon, copyIcon, fullscreenIcon } from "./icons"; @@ -1803,11 +1804,10 @@ class App extends React.Component { return; } - const magicFrameChildren = elementsOverlappingBBox({ - elements: this.scene.getNonDeletedElements(), - bounds: magicFrame, - type: "overlap", - }).filter((el) => !isMagicFrameElement(el)); + const magicFrameChildren = getElementsOverlappingFrame( + this.scene.getNonDeletedElements(), + magicFrame, + ).filter((el) => !isMagicFrameElement(el)); if (!magicFrameChildren.length) { if (source === "button") { diff --git a/packages/excalidraw/data/index.ts b/packages/excalidraw/data/index.ts index 7e93542ac0..0c63053a94 100644 --- a/packages/excalidraw/data/index.ts +++ b/packages/excalidraw/data/index.ts @@ -11,7 +11,6 @@ import { NonDeletedExcalidrawElement, } from "../element/types"; import { t } from "../i18n"; -import { elementsOverlappingBBox } from "../../utils/export"; import { isSomeElementSelected, getSelectedElements } from "../scene"; import { exportToCanvas, exportToSvg } from "../scene/export"; import { ExportType } from "../scene/types"; @@ -20,6 +19,7 @@ import { cloneJSON } from "../utils"; import { canvasToBlob } from "./blob"; import { fileSave, FileSystemHandle } from "./filesystem"; import { serializeAsJSON } from "./json"; +import { getElementsOverlappingFrame } from "../frame"; export { loadFromBlob } from "./blob"; export { loadFromJSON, saveAsJSON } from "./json"; @@ -56,11 +56,7 @@ export const prepareElementsForExport = ( isFrameLikeElement(exportedElements[0]) ) { exportingFrame = exportedElements[0]; - exportedElements = elementsOverlappingBBox({ - elements, - bounds: exportingFrame, - type: "overlap", - }); + exportedElements = getElementsOverlappingFrame(elements, exportingFrame); } else if (exportedElements.length > 1) { exportedElements = getSelectedElements( elements, diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index 3818e6684a..db58bb6264 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -21,7 +21,10 @@ import { getElementsWithinSelection, getSelectedElements } from "./scene"; import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups"; import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene"; import { getElementLineSegments } from "./element/bounds"; -import { doLineSegmentsIntersect } from "../utils/export"; +import { + doLineSegmentsIntersect, + elementsOverlappingBBox, +} from "../utils/export"; import { isFrameElement, isFrameLikeElement } from "./element/typeChecks"; // --------------------------- Frame State ------------------------------------ @@ -664,3 +667,19 @@ export const getFrameLikeTitle = ( // TODO name frames AI only is specific to AI frames return isFrameElement(element) ? `Frame ${frameIdx}` : `AI Frame ${frameIdx}`; }; + +export const getElementsOverlappingFrame = ( + elements: readonly ExcalidrawElement[], + frame: ExcalidrawFrameLikeElement, +) => { + return ( + elementsOverlappingBBox({ + elements, + bounds: frame, + type: "overlap", + }) + // removes elements who are overlapping, but are in a different frame, + // and thus invisible in target frame + .filter((el) => !el.frameId || el.frameId === frame.id) + ); +}; diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index cc84569a6f..9c357a21fd 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -26,8 +26,8 @@ import { getInitializedImageElements, updateImageCache, } from "../element/image"; -import { elementsOverlappingBBox } from "../../utils/export"; import { + getElementsOverlappingFrame, getFrameLikeElements, getFrameLikeTitle, getRootElements, @@ -168,11 +168,7 @@ const prepareElementsForRender = ({ let nextElements: readonly ExcalidrawElement[]; if (exportingFrame) { - nextElements = elementsOverlappingBBox({ - elements, - bounds: exportingFrame, - type: "overlap", - }); + nextElements = getElementsOverlappingFrame(elements, exportingFrame); } else if (frameRendering.enabled && frameRendering.name) { nextElements = addFrameLabelsAsTextElements(elements, { exportWithDarkMode, diff --git a/packages/excalidraw/tests/scene/export.test.ts b/packages/excalidraw/tests/scene/export.test.ts index 5287aa8cf5..ec9a0e6bf4 100644 --- a/packages/excalidraw/tests/scene/export.test.ts +++ b/packages/excalidraw/tests/scene/export.test.ts @@ -406,5 +406,67 @@ describe("exporting frames", () => { (frame.height + getFrameNameHeight("svg")).toString(), ); }); + + it("should not export frame-overlapping elements belonging to different frame", async () => { + const frame1 = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const frame2 = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 200, + y: 0, + }); + + const frame1Child = API.createElement({ + type: "rectangle", + width: 150, + height: 100, + x: 0, + y: 50, + frameId: frame1.id, + }); + const frame2Child = API.createElement({ + type: "rectangle", + width: 150, + height: 100, + x: 50, + y: 0, + frameId: frame2.id, + }); + + // low-level exportToSvg api expects elements to be pre-filtered, so let's + // use the filter we use in the editor + const { exportedElements, exportingFrame } = prepareElementsForExport( + [frame1Child, frame1, frame2Child, frame2], + { + selectedElementIds: { [frame1.id]: true }, + }, + true, + ); + + const svg = await exportToSvg({ + elements: exportedElements, + files: null, + exportPadding: 0, + exportingFrame, + }); + + // frame shouldn't be exported + expect(svg.querySelector(`[data-id="${frame1.id}"]`)).toBeNull(); + // frame1 child should be epxorted + expect(svg.querySelector(`[data-id="${frame1Child.id}"]`)).not.toBeNull(); + // frame2 child should not be exported even if it physically overlaps with + // frame1 + expect(svg.querySelector(`[data-id="${frame2Child.id}"]`)).toBeNull(); + + expect(svg.getAttribute("width")).toBe(frame1.width.toString()); + expect(svg.getAttribute("height")).toBe(frame1.height.toString()); + }); }); }); From 1e7df58b5b83fc2aba4c71ccc2478a0003db650f Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sun, 21 Jan 2024 14:01:43 +0100 Subject: [PATCH 05/31] feat: add pasted elements to frame under cursor (#7590) --- packages/excalidraw/components/App.tsx | 10 +++++-- packages/excalidraw/frame.ts | 16 +++++++++++ packages/excalidraw/tests/clipboard.test.tsx | 30 ++++++++++++++++++++ packages/excalidraw/tests/helpers/ui.ts | 2 ++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index d9471d6572..9e8a26eed2 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -3099,12 +3099,18 @@ class App extends React.Component { }, ); - const nextElements = [ + const allElements = [ ...this.scene.getElementsIncludingDeleted(), ...newElements, ]; - this.scene.replaceAllElements(nextElements); + const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y }); + + if (topLayerFrame) { + addElementsToFrame(allElements, newElements, topLayerFrame); + } + + this.scene.replaceAllElements(allElements); newElements.forEach((newElement) => { if (isTextElement(newElement) && isBoundToContainer(newElement)) { diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index db58bb6264..7056574f40 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -398,12 +398,28 @@ export const addElementsToFrame = ( const finalElementsToAdd: ExcalidrawElement[] = []; + const otherFrames = new Set(); + + for (const element of elementsToAdd) { + if (isFrameLikeElement(element) && element.id !== frame.id) { + otherFrames.add(element.id); + } + } + // - add bound text elements if not already in the array // - filter out elements that are already in the frame for (const element of omitGroupsContainingFrameLikes( allElements, elementsToAdd, )) { + // don't add frames or their children + if ( + isFrameLikeElement(element) || + (element.frameId && otherFrames.has(element.frameId)) + ) { + continue; + } + if (!currTargetFrameChildrenMap.has(element.id)) { finalElementsToAdd.push(element); } diff --git a/packages/excalidraw/tests/clipboard.test.tsx b/packages/excalidraw/tests/clipboard.test.tsx index 38d7b49d51..ce00e2da55 100644 --- a/packages/excalidraw/tests/clipboard.test.tsx +++ b/packages/excalidraw/tests/clipboard.test.tsx @@ -263,3 +263,33 @@ describe("Paste bound text container", () => { }); }); }); + +describe("pasting & frames", () => { + it("should add pasted elements to frame under cursor", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const rect = API.createElement({ type: "rectangle" }); + + h.elements = [frame]; + + const clipboardJSON = await serializeAsClipboardJSON({ + elements: [rect], + files: null, + }); + + mouse.moveTo(50, 50); + + pasteWithCtrlCmdV(clipboardJSON); + + await waitFor(() => { + expect(h.elements.length).toBe(2); + expect(h.elements[1].type).toBe(rect.type); + expect(h.elements[1].frameId).toBe(frame.id); + }); + }); +}); diff --git a/packages/excalidraw/tests/helpers/ui.ts b/packages/excalidraw/tests/helpers/ui.ts index f37ac00194..58579fe933 100644 --- a/packages/excalidraw/tests/helpers/ui.ts +++ b/packages/excalidraw/tests/helpers/ui.ts @@ -206,6 +206,8 @@ export class Pointer { moveTo(x: number = this.clientX, y: number = this.clientY) { this.clientX = x; this.clientY = y; + // fire "mousemove" to update editor cursor position + fireEvent.mouseMove(document, this.getEvent()); fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent()); } From b66daae1f3552b88e9b4a2c81fcb1d9f1a43c717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barnab=C3=A1s=20Moln=C3=A1r?= <38168628+barnabasmolnar@users.noreply.github.com> Date: Sun, 21 Jan 2024 20:36:09 +0100 Subject: [PATCH 06/31] fix: Truncate collaborator name in dropdown. (#7576) --- packages/excalidraw/actions/actionNavigate.tsx | 4 +++- packages/excalidraw/components/UserList.scss | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/excalidraw/actions/actionNavigate.tsx b/packages/excalidraw/actions/actionNavigate.tsx index 4ce79b96fa..ea65584fe6 100644 --- a/packages/excalidraw/actions/actionNavigate.tsx +++ b/packages/excalidraw/actions/actionNavigate.tsx @@ -57,7 +57,9 @@ export const actionGoToCollaborator = register({ isBeingFollowed={isBeingFollowed} isCurrentUser={collaborator.isCurrentUser === true} /> - {collaborator.username} +
+ {collaborator.username} +
Date: Mon, 22 Jan 2024 03:55:28 +0800 Subject: [PATCH 07/31] fix: frame name editing inconvenience (#7437) --- packages/excalidraw/frame.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index 7056574f40..115cfef064 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -676,12 +676,12 @@ export const getFrameLikeTitle = ( element: ExcalidrawFrameLikeElement, frameIdx: number, ) => { - const existingName = element.name?.trim(); - if (existingName) { - return existingName; - } // TODO name frames AI only is specific to AI frames - return isFrameElement(element) ? `Frame ${frameIdx}` : `AI Frame ${frameIdx}`; + return element.name === null + ? isFrameElement(element) + ? `Frame ${frameIdx}` + : `AI Frame $${frameIdx}` + : element.name; }; export const getElementsOverlappingFrame = ( From 740a1654529cb24ea6e17770f53c056f4a7269b0 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sun, 21 Jan 2024 20:55:57 +0100 Subject: [PATCH 08/31] fix: filter out elements not overlapping frame on paste (#7591) --- packages/excalidraw/components/App.tsx | 7 +- packages/excalidraw/frame.ts | 61 ++++++++- packages/excalidraw/tests/clipboard.test.tsx | 137 +++++++++++++++++++ 3 files changed, 198 insertions(+), 7 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 9e8a26eed2..77c972882d 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -349,6 +349,7 @@ import { isElementInFrame, getFrameLikeTitle, getElementsOverlappingFrame, + filterElementsEligibleAsFrameChildren, } from "../frame"; import { excludeElementsInFramesFromSelection, @@ -3107,7 +3108,11 @@ class App extends React.Component { const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y }); if (topLayerFrame) { - addElementsToFrame(allElements, newElements, topLayerFrame); + const eligibleElements = filterElementsEligibleAsFrameChildren( + newElements, + topLayerFrame, + ); + addElementsToFrame(allElements, eligibleElements, topLayerFrame); } this.scene.replaceAllElements(allElements); diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index 115cfef064..a5e7437111 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -107,17 +107,16 @@ export const elementsAreInFrameBounds = ( elements: readonly ExcalidrawElement[], frame: ExcalidrawFrameLikeElement, ) => { - const [selectionX1, selectionY1, selectionX2, selectionY2] = - getElementAbsoluteCoords(frame); + const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(frame); const [elementX1, elementY1, elementX2, elementY2] = getCommonBounds(elements); return ( - selectionX1 <= elementX1 && - selectionY1 <= elementY1 && - selectionX2 >= elementX2 && - selectionY2 >= elementY2 + frameX1 <= elementX1 && + frameY1 <= elementY1 && + frameX2 >= elementX2 && + frameY2 >= elementY2 ); }; @@ -372,6 +371,56 @@ export const getContainingFrame = ( // --------------------------- Frame Operations ------------------------------- +/** */ +export const filterElementsEligibleAsFrameChildren = ( + elements: readonly ExcalidrawElement[], + frame: ExcalidrawFrameLikeElement, +) => { + const otherFrames = new Set(); + + elements = omitGroupsContainingFrameLikes(elements); + + for (const element of elements) { + if (isFrameLikeElement(element) && element.id !== frame.id) { + otherFrames.add(element.id); + } + } + + const processedGroups = new Set(); + + const eligibleElements: ExcalidrawElement[] = []; + + for (const element of elements) { + // don't add frames or their children + if ( + isFrameLikeElement(element) || + (element.frameId && otherFrames.has(element.frameId)) + ) { + continue; + } + + if (element.groupIds.length) { + const shallowestGroupId = element.groupIds.at(-1)!; + if (!processedGroups.has(shallowestGroupId)) { + processedGroups.add(shallowestGroupId); + const groupElements = getElementsInGroup(elements, shallowestGroupId); + if (groupElements.some((el) => elementOverlapsWithFrame(el, frame))) { + for (const child of groupElements) { + eligibleElements.push(child); + } + } + } + } else { + const overlaps = elementOverlapsWithFrame(element, frame); + if (overlaps) { + eligibleElements.push(element); + } + } + } + + return eligibleElements; +}; + /** * Retains (or repairs for target frame) the ordering invriant where children * elements come right before the parent frame: diff --git a/packages/excalidraw/tests/clipboard.test.tsx b/packages/excalidraw/tests/clipboard.test.tsx index ce00e2da55..149ebcd1ef 100644 --- a/packages/excalidraw/tests/clipboard.test.tsx +++ b/packages/excalidraw/tests/clipboard.test.tsx @@ -292,4 +292,141 @@ describe("pasting & frames", () => { expect(h.elements[1].frameId).toBe(frame.id); }); }); + + it("should filter out elements not overlapping frame", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const rect = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + }); + const rect2 = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + x: 100, + y: 100, + }); + + h.elements = [frame]; + + const clipboardJSON = await serializeAsClipboardJSON({ + elements: [rect, rect2], + files: null, + }); + + mouse.moveTo(90, 90); + + pasteWithCtrlCmdV(clipboardJSON); + + await waitFor(() => { + expect(h.elements.length).toBe(3); + expect(h.elements[1].type).toBe(rect.type); + expect(h.elements[1].frameId).toBe(frame.id); + expect(h.elements[2].type).toBe(rect2.type); + expect(h.elements[2].frameId).toBe(null); + }); + }); + + it("should not filter out elements not overlapping frame if part of group", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const rect = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + groupIds: ["g1"], + }); + const rect2 = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + x: 100, + y: 100, + groupIds: ["g1"], + }); + + h.elements = [frame]; + + const clipboardJSON = await serializeAsClipboardJSON({ + elements: [rect, rect2], + files: null, + }); + + mouse.moveTo(90, 90); + + pasteWithCtrlCmdV(clipboardJSON); + + await waitFor(() => { + expect(h.elements.length).toBe(3); + expect(h.elements[1].type).toBe(rect.type); + expect(h.elements[1].frameId).toBe(frame.id); + expect(h.elements[2].type).toBe(rect2.type); + expect(h.elements[2].frameId).toBe(frame.id); + }); + }); + + it("should not filter out other frames and their children", async () => { + const frame = API.createElement({ + type: "frame", + width: 100, + height: 100, + x: 0, + y: 0, + }); + const rect = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + groupIds: ["g1"], + }); + + const frame2 = API.createElement({ + type: "frame", + width: 75, + height: 75, + x: 0, + y: 0, + }); + const rect2 = API.createElement({ + type: "rectangle", + width: 50, + height: 50, + x: 55, + y: 55, + frameId: frame2.id, + }); + + h.elements = [frame]; + + const clipboardJSON = await serializeAsClipboardJSON({ + elements: [rect, rect2, frame2], + files: null, + }); + + mouse.moveTo(90, 90); + + pasteWithCtrlCmdV(clipboardJSON); + + await waitFor(() => { + expect(h.elements.length).toBe(4); + expect(h.elements[1].type).toBe(rect.type); + expect(h.elements[1].frameId).toBe(frame.id); + expect(h.elements[2].type).toBe(rect2.type); + expect(h.elements[2].frameId).toBe(h.elements[3].id); + expect(h.elements[3].type).toBe(frame2.type); + expect(h.elements[3].frameId).toBe(null); + }); + }); }); From 0415c616b1ec9ec003ae53049b4f98df844dfb5f Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Mon, 22 Jan 2024 00:23:02 +0100 Subject: [PATCH 09/31] refactor: decoupling global Scene state part-1 (#7577) --- packages/excalidraw/actions/actionFlip.ts | 38 +++- packages/excalidraw/actions/actionFrame.ts | 6 +- packages/excalidraw/actions/actionGroup.tsx | 7 +- .../excalidraw/actions/actionProperties.tsx | 42 ++-- packages/excalidraw/components/Actions.tsx | 18 +- packages/excalidraw/components/App.tsx | 52 ++--- packages/excalidraw/components/LayerUI.tsx | 2 +- packages/excalidraw/components/MobileMenu.tsx | 2 +- .../components/canvases/InteractiveCanvas.tsx | 9 +- .../components/canvases/StaticCanvas.tsx | 13 +- packages/excalidraw/data/restore.ts | 21 +- packages/excalidraw/element/bounds.ts | 13 +- packages/excalidraw/element/embeddable.ts | 24 +-- packages/excalidraw/element/newElement.ts | 6 +- packages/excalidraw/element/resizeElements.ts | 53 +++-- packages/excalidraw/element/textElement.ts | 49 ++--- packages/excalidraw/element/textWysiwyg.tsx | 17 +- packages/excalidraw/element/types.ts | 30 ++- packages/excalidraw/frame.ts | 112 ++++++----- packages/excalidraw/groups.ts | 13 +- packages/excalidraw/renderer/renderElement.ts | 13 +- packages/excalidraw/renderer/renderScene.ts | 190 ++++++++++-------- packages/excalidraw/scene/Fonts.ts | 9 +- packages/excalidraw/scene/Renderer.ts | 62 +++--- packages/excalidraw/scene/Scene.ts | 72 +++++-- packages/excalidraw/scene/export.ts | 56 ++++-- packages/excalidraw/scene/scrollbars.ts | 7 +- packages/excalidraw/scene/selection.ts | 17 +- packages/excalidraw/scene/types.ts | 11 +- packages/excalidraw/utility-types.ts | 8 + packages/excalidraw/utils.ts | 42 +++- 31 files changed, 630 insertions(+), 384 deletions(-) diff --git a/packages/excalidraw/actions/actionFlip.ts b/packages/excalidraw/actions/actionFlip.ts index 12d5e2e48e..81476e2411 100644 --- a/packages/excalidraw/actions/actionFlip.ts +++ b/packages/excalidraw/actions/actionFlip.ts @@ -1,9 +1,13 @@ import { register } from "./register"; import { getSelectedElements } from "../scene"; import { getNonDeletedElements } from "../element"; -import { ExcalidrawElement, NonDeleted } from "../element/types"; +import { + ExcalidrawElement, + NonDeleted, + NonDeletedElementsMap, +} from "../element/types"; import { resizeMultipleElements } from "../element/resizeElements"; -import { AppState, PointerDownState } from "../types"; +import { AppState } from "../types"; import { arrayToMap } from "../utils"; import { CODES, KEYS } from "../keys"; import { getCommonBoundingBox } from "../element/bounds"; @@ -20,7 +24,12 @@ export const actionFlipHorizontal = register({ perform: (elements, appState, _, app) => { return { elements: updateFrameMembershipOfSelectedElements( - flipSelectedElements(elements, appState, "horizontal"), + flipSelectedElements( + elements, + app.scene.getNonDeletedElementsMap(), + appState, + "horizontal", + ), appState, app, ), @@ -38,7 +47,12 @@ export const actionFlipVertical = register({ perform: (elements, appState, _, app) => { return { elements: updateFrameMembershipOfSelectedElements( - flipSelectedElements(elements, appState, "vertical"), + flipSelectedElements( + elements, + app.scene.getNonDeletedElementsMap(), + appState, + "vertical", + ), appState, app, ), @@ -53,6 +67,7 @@ export const actionFlipVertical = register({ const flipSelectedElements = ( elements: readonly ExcalidrawElement[], + elementsMap: NonDeletedElementsMap, appState: Readonly, flipDirection: "horizontal" | "vertical", ) => { @@ -67,6 +82,7 @@ const flipSelectedElements = ( const updatedElements = flipElements( selectedElements, + elementsMap, appState, flipDirection, ); @@ -79,15 +95,17 @@ const flipSelectedElements = ( }; const flipElements = ( - elements: NonDeleted[], + selectedElements: NonDeleted[], + elementsMap: NonDeletedElementsMap, appState: AppState, flipDirection: "horizontal" | "vertical", ): ExcalidrawElement[] => { - const { minX, minY, maxX, maxY } = getCommonBoundingBox(elements); + const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements); resizeMultipleElements( - { originalElements: arrayToMap(elements) } as PointerDownState, - elements, + elementsMap, + selectedElements, + elementsMap, "nw", true, flipDirection === "horizontal" ? maxX : minX, @@ -96,7 +114,7 @@ const flipElements = ( (isBindingEnabled(appState) ? bindOrUnbindSelectedElements - : unbindLinearElements)(elements); + : unbindLinearElements)(selectedElements); - return elements; + return selectedElements; }; diff --git a/packages/excalidraw/actions/actionFrame.ts b/packages/excalidraw/actions/actionFrame.ts index 4cddb2ac0f..8232db3cd9 100644 --- a/packages/excalidraw/actions/actionFrame.ts +++ b/packages/excalidraw/actions/actionFrame.ts @@ -63,11 +63,7 @@ export const actionRemoveAllElementsFromFrame = register({ if (isFrameLikeElement(selectedElement)) { return { - elements: removeAllElementsFromFrame( - elements, - selectedElement, - appState, - ), + elements: removeAllElementsFromFrame(elements, selectedElement), appState: { ...appState, selectedElementIds: { diff --git a/packages/excalidraw/actions/actionGroup.tsx b/packages/excalidraw/actions/actionGroup.tsx index e6cb058401..42bd26efea 100644 --- a/packages/excalidraw/actions/actionGroup.tsx +++ b/packages/excalidraw/actions/actionGroup.tsx @@ -105,11 +105,7 @@ export const actionGroup = register({ const frameElementsMap = groupByFrameLikes(selectedElements); frameElementsMap.forEach((elementsInFrame, frameId) => { - nextElements = removeElementsFromFrame( - nextElements, - elementsInFrame, - appState, - ); + removeElementsFromFrame(elementsInFrame); }); } @@ -229,7 +225,6 @@ export const actionUngroup = register({ nextElements, getElementsInResizingFrame(nextElements, frame, appState), frame, - appState, ); } }); diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 9489970ae6..c2a47802ff 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -1,4 +1,4 @@ -import { AppState, Primitive } from "../types"; +import { AppClassProperties, AppState, Primitive } from "../types"; import { DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS, @@ -66,7 +66,6 @@ import { import { mutateElement, newElementWith } from "../element/mutateElement"; import { getBoundTextElement, - getContainerElement, getDefaultLineHeight, } from "../element/textElement"; import { @@ -189,6 +188,7 @@ const offsetElementAfterFontResize = ( const changeFontSize = ( elements: readonly ExcalidrawElement[], appState: AppState, + app: AppClassProperties, getNewFontSize: (element: ExcalidrawTextElement) => number, fallbackValue?: ExcalidrawTextElement["fontSize"], ) => { @@ -206,7 +206,10 @@ const changeFontSize = ( let newElement: ExcalidrawTextElement = newElementWith(oldElement, { fontSize: newFontSize, }); - redrawTextBoundingBox(newElement, getContainerElement(oldElement)); + redrawTextBoundingBox( + newElement, + app.scene.getContainerElement(oldElement), + ); newElement = offsetElementAfterFontResize(oldElement, newElement); @@ -600,8 +603,8 @@ export const actionChangeOpacity = register({ export const actionChangeFontSize = register({ name: "changeFontSize", trackEvent: false, - perform: (elements, appState, value) => { - return changeFontSize(elements, appState, () => value, value); + perform: (elements, appState, value, app) => { + return changeFontSize(elements, appState, app, () => value, value); }, PanelComponent: ({ elements, appState, updateData }) => (
@@ -663,8 +666,8 @@ export const actionChangeFontSize = register({ export const actionDecreaseFontSize = register({ name: "decreaseFontSize", trackEvent: false, - perform: (elements, appState, value) => { - return changeFontSize(elements, appState, (element) => + perform: (elements, appState, value, app) => { + return changeFontSize(elements, appState, app, (element) => Math.round( // get previous value before relative increase (doesn't work fully // due to rounding and float precision issues) @@ -685,8 +688,8 @@ export const actionDecreaseFontSize = register({ export const actionIncreaseFontSize = register({ name: "increaseFontSize", trackEvent: false, - perform: (elements, appState, value) => { - return changeFontSize(elements, appState, (element) => + perform: (elements, appState, value, app) => { + return changeFontSize(elements, appState, app, (element) => Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)), ); }, @@ -703,7 +706,7 @@ export const actionIncreaseFontSize = register({ export const actionChangeFontFamily = register({ name: "changeFontFamily", trackEvent: false, - perform: (elements, appState, value) => { + perform: (elements, appState, value, app) => { return { elements: changeProperty( elements, @@ -717,7 +720,10 @@ export const actionChangeFontFamily = register({ lineHeight: getDefaultLineHeight(value), }, ); - redrawTextBoundingBox(newElement, getContainerElement(oldElement)); + redrawTextBoundingBox( + newElement, + app.scene.getContainerElement(oldElement), + ); return newElement; } @@ -795,7 +801,7 @@ export const actionChangeFontFamily = register({ export const actionChangeTextAlign = register({ name: "changeTextAlign", trackEvent: false, - perform: (elements, appState, value) => { + perform: (elements, appState, value, app) => { return { elements: changeProperty( elements, @@ -806,7 +812,10 @@ export const actionChangeTextAlign = register({ oldElement, { textAlign: value }, ); - redrawTextBoundingBox(newElement, getContainerElement(oldElement)); + redrawTextBoundingBox( + newElement, + app.scene.getContainerElement(oldElement), + ); return newElement; } @@ -875,7 +884,7 @@ export const actionChangeTextAlign = register({ export const actionChangeVerticalAlign = register({ name: "changeVerticalAlign", trackEvent: { category: "element" }, - perform: (elements, appState, value) => { + perform: (elements, appState, value, app) => { return { elements: changeProperty( elements, @@ -887,7 +896,10 @@ export const actionChangeVerticalAlign = register({ { verticalAlign: value }, ); - redrawTextBoundingBox(newElement, getContainerElement(oldElement)); + redrawTextBoundingBox( + newElement, + app.scene.getContainerElement(oldElement), + ); return newElement; } diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index f07664f1a1..d67c8893d3 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -1,7 +1,6 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { ActionManager } from "../actions/manager"; -import { getNonDeletedElements } from "../element"; -import { ExcalidrawElement, ExcalidrawElementType } from "../element/types"; +import { ExcalidrawElementType, NonDeletedElementsMap } from "../element/types"; import { t } from "../i18n"; import { useDevice } from "./App"; import { @@ -44,17 +43,14 @@ import { useTunnels } from "../context/tunnels"; export const SelectedShapeActions = ({ appState, - elements, + elementsMap, renderAction, }: { appState: UIAppState; - elements: readonly ExcalidrawElement[]; + elementsMap: NonDeletedElementsMap; renderAction: ActionManager["renderAction"]; }) => { - const targetElements = getTargetElements( - getNonDeletedElements(elements), - appState, - ); + const targetElements = getTargetElements(elementsMap, appState); let isSingleElementBoundContainer = false; if ( @@ -137,12 +133,12 @@ export const SelectedShapeActions = ({ {renderAction("changeFontFamily")} {(appState.activeTool.type === "text" || - suppportsHorizontalAlign(targetElements)) && + suppportsHorizontalAlign(targetElements, elementsMap)) && renderAction("changeTextAlign")} )} - {shouldAllowVerticalAlign(targetElements) && + {shouldAllowVerticalAlign(targetElements, elementsMap) && renderAction("changeVerticalAlign")} {(canHaveArrowheads(appState.activeTool.type) || targetElements.some((element) => canHaveArrowheads(element.type))) && ( diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 77c972882d..9e3ff5dac2 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -1417,7 +1417,7 @@ class App extends React.Component { const { renderTopRightUI, renderCustomStats } = this.props; const versionNonce = this.scene.getVersionNonce(); - const { canvasElements, visibleElements } = + const { elementsMap, visibleElements } = this.renderer.getRenderableElements({ versionNonce, zoom: this.state.zoom, @@ -1627,7 +1627,7 @@ class App extends React.Component { { { private renderInteractiveSceneCallback = ({ atLeastOneVisibleElement, scrollBars, - elements, + elementsMap, }: RenderInteractiveSceneCallback) => { if (scrollBars) { currentScrollBars = scrollBars; @@ -2789,7 +2789,7 @@ class App extends React.Component { // hide when editing text isTextElement(this.state.editingElement) ? false - : !atLeastOneVisibleElement && elements.length > 0; + : !atLeastOneVisibleElement && elementsMap.size > 0; if (this.state.scrolledOutside !== scrolledOutside) { this.setState({ scrolledOutside }); } @@ -3119,7 +3119,10 @@ class App extends React.Component { newElements.forEach((newElement) => { if (isTextElement(newElement) && isBoundToContainer(newElement)) { - const container = getContainerElement(newElement); + const container = getContainerElement( + newElement, + this.scene.getElementsMapIncludingDeleted(), + ); redrawTextBoundingBox(newElement, container); } }); @@ -4183,11 +4186,18 @@ class App extends React.Component { this.scene.replaceAllElements([ ...this.scene.getElementsIncludingDeleted().map((_element) => { if (_element.id === element.id && isTextElement(_element)) { - return updateTextElement(_element, { - text, - isDeleted, - originalText, - }); + return updateTextElement( + _element, + getContainerElement( + _element, + this.scene.getElementsMapIncludingDeleted(), + ), + { + text, + isDeleted, + originalText, + }, + ); } return _element; }), @@ -7700,13 +7710,9 @@ class App extends React.Component { groupIds: [], }); - this.scene.replaceAllElements( - removeElementsFromFrame( - this.scene.getElementsIncludingDeleted(), - [linearElement], - this.state, - ), - ); + removeElementsFromFrame([linearElement]); + + this.scene.informMutation(); } } } @@ -7716,7 +7722,7 @@ class App extends React.Component { this.getTopLayerFrameAtSceneCoords(sceneCoords); const selectedElements = this.scene.getSelectedElements(this.state); - let nextElements = this.scene.getElementsIncludingDeleted(); + let nextElements = this.scene.getElementsMapIncludingDeleted(); const updateGroupIdsAfterEditingGroup = ( elements: ExcalidrawElement[], @@ -7809,7 +7815,7 @@ class App extends React.Component { this.scene.replaceAllElements( addElementsToFrame( - this.scene.getElementsIncludingDeleted(), + this.scene.getElementsMapIncludingDeleted(), elementsInsideFrame, draggingElement, ), @@ -7857,7 +7863,6 @@ class App extends React.Component { this.state, ), frame, - this.state, ); } @@ -9137,10 +9142,10 @@ class App extends React.Component { if ( transformElements( - pointerDownState, + pointerDownState.originalElements, transformHandleType, selectedElements, - pointerDownState.resize.arrowDirection, + this.scene.getElementsMapIncludingDeleted(), shouldRotateWithDiscreteAngle(event), shouldResizeFromCenter(event), selectedElements.length === 1 && isImageElement(selectedElements[0]) @@ -9150,7 +9155,6 @@ class App extends React.Component { resizeY, pointerDownState.resize.center.x, pointerDownState.resize.center.y, - this.state, ) ) { this.maybeSuggestBindingForAll(selectedElements); diff --git a/packages/excalidraw/components/LayerUI.tsx b/packages/excalidraw/components/LayerUI.tsx index 8cedf689d0..7247e8bf12 100644 --- a/packages/excalidraw/components/LayerUI.tsx +++ b/packages/excalidraw/components/LayerUI.tsx @@ -226,7 +226,7 @@ const LayerUI = ({ > diff --git a/packages/excalidraw/components/MobileMenu.tsx b/packages/excalidraw/components/MobileMenu.tsx index 91d0c518cf..98f85a9aca 100644 --- a/packages/excalidraw/components/MobileMenu.tsx +++ b/packages/excalidraw/components/MobileMenu.tsx @@ -183,7 +183,7 @@ export const MobileMenu = ({
diff --git a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx index 0aaa52c7ce..0782b92b93 100644 --- a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx +++ b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx @@ -7,6 +7,7 @@ import type { DOMAttributes } from "react"; import type { AppState, InteractiveCanvasAppState } from "../../types"; import type { InteractiveCanvasRenderConfig, + RenderableElementsMap, RenderInteractiveSceneCallback, } from "../../scene/types"; import type { NonDeletedExcalidrawElement } from "../../element/types"; @@ -15,7 +16,7 @@ import { isRenderThrottlingEnabled } from "../../reactUtils"; type InteractiveCanvasProps = { containerRef: React.RefObject; canvas: HTMLCanvasElement | null; - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: RenderableElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; selectedElements: readonly NonDeletedExcalidrawElement[]; versionNonce: number | undefined; @@ -113,7 +114,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => { renderInteractiveScene( { canvas: props.canvas, - elements: props.elements, + elementsMap: props.elementsMap, visibleElements: props.visibleElements, selectedElements: props.selectedElements, scale: window.devicePixelRatio, @@ -201,10 +202,10 @@ const areEqual = ( prevProps.selectionNonce !== nextProps.selectionNonce || prevProps.versionNonce !== nextProps.versionNonce || prevProps.scale !== nextProps.scale || - // we need to memoize on element arrays because they may have renewed + // we need to memoize on elementsMap because they may have renewed // even if versionNonce didn't change (e.g. we filter elements out based // on appState) - prevProps.elements !== nextProps.elements || + prevProps.elementsMap !== nextProps.elementsMap || prevProps.visibleElements !== nextProps.visibleElements || prevProps.selectedElements !== nextProps.selectedElements ) { diff --git a/packages/excalidraw/components/canvases/StaticCanvas.tsx b/packages/excalidraw/components/canvases/StaticCanvas.tsx index c8174566bb..3dc5b91751 100644 --- a/packages/excalidraw/components/canvases/StaticCanvas.tsx +++ b/packages/excalidraw/components/canvases/StaticCanvas.tsx @@ -3,14 +3,17 @@ import { RoughCanvas } from "roughjs/bin/canvas"; import { renderStaticScene } from "../../renderer/renderScene"; import { isShallowEqual } from "../../utils"; import type { AppState, StaticCanvasAppState } from "../../types"; -import type { StaticCanvasRenderConfig } from "../../scene/types"; +import type { + RenderableElementsMap, + StaticCanvasRenderConfig, +} from "../../scene/types"; import type { NonDeletedExcalidrawElement } from "../../element/types"; import { isRenderThrottlingEnabled } from "../../reactUtils"; type StaticCanvasProps = { canvas: HTMLCanvasElement; rc: RoughCanvas; - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: RenderableElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; versionNonce: number | undefined; selectionNonce: number | undefined; @@ -63,7 +66,7 @@ const StaticCanvas = (props: StaticCanvasProps) => { canvas, rc: props.rc, scale: props.scale, - elements: props.elements, + elementsMap: props.elementsMap, visibleElements: props.visibleElements, appState: props.appState, renderConfig: props.renderConfig, @@ -106,10 +109,10 @@ const areEqual = ( if ( prevProps.versionNonce !== nextProps.versionNonce || prevProps.scale !== nextProps.scale || - // we need to memoize on element arrays because they may have renewed + // we need to memoize on elementsMap because they may have renewed // even if versionNonce didn't change (e.g. we filter elements out based // on appState) - prevProps.elements !== nextProps.elements || + prevProps.elementsMap !== nextProps.elementsMap || prevProps.visibleElements !== nextProps.visibleElements ) { return false; diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index ff40635935..12e7f1af1b 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -40,6 +40,7 @@ import { arrayToMap } from "../utils"; import { MarkOptional, Mutable } from "../utility-types"; import { detectLineHeight, + getContainerElement, getDefaultLineHeight, measureBaseline, } from "../element/textElement"; @@ -179,7 +180,6 @@ const restoreElementWithProperties = < const restoreElement = ( element: Exclude, - refreshDimensions = false, ): typeof element | null => { switch (element.type) { case "text": @@ -232,10 +232,6 @@ const restoreElement = ( element = bumpVersion(element); } - if (refreshDimensions) { - element = { ...element, ...refreshTextDimensions(element) }; - } - return element; case "freedraw": { return restoreElementWithProperties(element, { @@ -426,10 +422,7 @@ export const restoreElements = ( // filtering out selection, which is legacy, no longer kept in elements, // and causing issues if retained if (element.type !== "selection" && !isInvisiblySmallElement(element)) { - let migratedElement: ExcalidrawElement | null = restoreElement( - element, - opts?.refreshDimensions, - ); + let migratedElement: ExcalidrawElement | null = restoreElement(element); if (migratedElement) { const localElement = localElementsMap?.get(element.id); if (localElement && localElement.version > migratedElement.version) { @@ -462,6 +455,16 @@ export const restoreElements = ( } else if (element.boundElements) { repairContainerElement(element, restoredElementsMap); } + + if (opts.refreshDimensions && isTextElement(element)) { + Object.assign( + element, + refreshTextDimensions( + element, + getContainerElement(element, restoredElementsMap), + ), + ); + } } return restoredElements; diff --git a/packages/excalidraw/element/bounds.ts b/packages/excalidraw/element/bounds.ts index 292fc995dd..673649e5f4 100644 --- a/packages/excalidraw/element/bounds.ts +++ b/packages/excalidraw/element/bounds.ts @@ -5,6 +5,7 @@ import { ExcalidrawFreeDrawElement, NonDeleted, ExcalidrawTextElementWithContainer, + ElementsMapOrArray, } from "./types"; import { distance2d, rotate, rotatePoint } from "../math"; import rough from "roughjs/bin/rough"; @@ -161,7 +162,11 @@ export const getElementAbsoluteCoords = ( includeBoundText, ); } else if (isTextElement(element)) { - const container = getContainerElement(element); + const elementsMap = + Scene.getScene(element)?.getElementsMapIncludingDeleted(); + const container = elementsMap + ? getContainerElement(element, elementsMap) + : null; if (isArrowElement(container)) { const coords = LinearElementEditor.getBoundTextElementPosition( container, @@ -729,10 +734,8 @@ const getLinearElementRotatedBounds = ( export const getElementBounds = (element: ExcalidrawElement): Bounds => { return ElementBounds.getBounds(element); }; -export const getCommonBounds = ( - elements: readonly ExcalidrawElement[], -): Bounds => { - if (!elements.length) { +export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => { + if ("size" in elements ? !elements.size : !elements.length) { return [0, 0, 0, 0]; } diff --git a/packages/excalidraw/element/embeddable.ts b/packages/excalidraw/element/embeddable.ts index 025ed4901e..f62b0f95f4 100644 --- a/packages/excalidraw/element/embeddable.ts +++ b/packages/excalidraw/element/embeddable.ts @@ -5,17 +5,12 @@ import { ExcalidrawProps } from "../types"; import { getFontString, updateActiveTool } from "../utils"; import { setCursorForShape } from "../cursor"; import { newTextElement } from "./newElement"; -import { getContainerElement, wrapText } from "./textElement"; -import { - isFrameLikeElement, - isIframeElement, - isIframeLikeElement, -} from "./typeChecks"; +import { wrapText } from "./textElement"; +import { isIframeElement } from "./typeChecks"; import { ExcalidrawElement, ExcalidrawIframeLikeElement, IframeData, - NonDeletedExcalidrawElement, } from "./types"; const embeddedLinkCache = new Map(); @@ -217,21 +212,6 @@ export const getEmbedLink = ( return { link, intrinsicSize: aspectRatio, type }; }; -export const isIframeLikeOrItsLabel = ( - element: NonDeletedExcalidrawElement, -): Boolean => { - if (isIframeLikeElement(element)) { - return true; - } - if (element.type === "text") { - const container = getContainerElement(element); - if (container && isFrameLikeElement(container)) { - return true; - } - } - return false; -}; - export const createPlaceholderEmbeddableLabel = ( element: ExcalidrawIframeLikeElement, ): ExcalidrawElement => { diff --git a/packages/excalidraw/element/newElement.ts b/packages/excalidraw/element/newElement.ts index 00cae296cd..3158c064cf 100644 --- a/packages/excalidraw/element/newElement.ts +++ b/packages/excalidraw/element/newElement.ts @@ -31,7 +31,6 @@ import { getElementAbsoluteCoords } from "."; import { adjustXYWithRotation } from "../math"; import { getResizedElementAbsoluteCoords } from "./bounds"; import { - getContainerElement, measureText, normalizeText, wrapText, @@ -333,12 +332,12 @@ const getAdjustedDimensions = ( export const refreshTextDimensions = ( textElement: ExcalidrawTextElement, + container: ExcalidrawTextContainer | null, text = textElement.text, ) => { if (textElement.isDeleted) { return; } - const container = getContainerElement(textElement); if (container) { text = wrapText( text, @@ -352,6 +351,7 @@ export const refreshTextDimensions = ( export const updateTextElement = ( textElement: ExcalidrawTextElement, + container: ExcalidrawTextContainer | null, { text, isDeleted, @@ -365,7 +365,7 @@ export const updateTextElement = ( return newElementWith(textElement, { originalText, isDeleted: isDeleted ?? textElement.isDeleted, - ...refreshTextDimensions(textElement, originalText), + ...refreshTextDimensions(textElement, container, originalText), }); }; diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index 883382933d..46b891aca5 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -15,6 +15,7 @@ import { ExcalidrawElement, ExcalidrawTextElementWithContainer, ExcalidrawImageElement, + ElementsMap, } from "./types"; import type { Mutable } from "../utility-types"; import { @@ -41,7 +42,7 @@ import { MaybeTransformHandleType, TransformHandleDirection, } from "./transformHandles"; -import { AppState, Point, PointerDownState } from "../types"; +import { Point, PointerDownState } from "../types"; import Scene from "../scene/Scene"; import { getApproxMinLineWidth, @@ -68,10 +69,10 @@ export const normalizeAngle = (angle: number): number => { // Returns true when transform (resizing/rotation) happened export const transformElements = ( - pointerDownState: PointerDownState, + originalElements: PointerDownState["originalElements"], transformHandleType: MaybeTransformHandleType, selectedElements: readonly NonDeletedExcalidrawElement[], - resizeArrowDirection: "origin" | "end", + elementsMap: ElementsMap, shouldRotateWithDiscreteAngle: boolean, shouldResizeFromCenter: boolean, shouldMaintainAspectRatio: boolean, @@ -79,7 +80,6 @@ export const transformElements = ( pointerY: number, centerX: number, centerY: number, - appState: AppState, ) => { if (selectedElements.length === 1) { const [element] = selectedElements; @@ -89,7 +89,6 @@ export const transformElements = ( pointerX, pointerY, shouldRotateWithDiscreteAngle, - pointerDownState.originalElements, ); updateBoundElements(element); } else if ( @@ -101,6 +100,7 @@ export const transformElements = ( ) { resizeSingleTextElement( element, + elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, @@ -109,9 +109,10 @@ export const transformElements = ( updateBoundElements(element); } else if (transformHandleType) { resizeSingleElement( - pointerDownState.originalElements, + originalElements, shouldMaintainAspectRatio, element, + elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, @@ -123,7 +124,7 @@ export const transformElements = ( } else if (selectedElements.length > 1) { if (transformHandleType === "rotation") { rotateMultipleElements( - pointerDownState, + originalElements, selectedElements, pointerX, pointerY, @@ -139,8 +140,9 @@ export const transformElements = ( transformHandleType === "se" ) { resizeMultipleElements( - pointerDownState, + originalElements, selectedElements, + elementsMap, transformHandleType, shouldResizeFromCenter, pointerX, @@ -157,7 +159,6 @@ const rotateSingleElement = ( pointerX: number, pointerY: number, shouldRotateWithDiscreteAngle: boolean, - originalElements: Map>, ) => { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const cx = (x1 + x2) / 2; @@ -207,6 +208,7 @@ const rescalePointsInElement = ( const measureFontSizeFromWidth = ( element: NonDeleted, + elementsMap: ElementsMap, nextWidth: number, nextHeight: number, ): { size: number; baseline: number } | null => { @@ -215,7 +217,7 @@ const measureFontSizeFromWidth = ( const hasContainer = isBoundToContainer(element); if (hasContainer) { - const container = getContainerElement(element); + const container = getContainerElement(element, elementsMap); if (container) { width = getBoundTextMaxWidth(container); } @@ -257,6 +259,7 @@ const getSidesForTransformHandle = ( const resizeSingleTextElement = ( element: NonDeleted, + elementsMap: ElementsMap, transformHandleType: "nw" | "ne" | "sw" | "se", shouldResizeFromCenter: boolean, pointerX: number, @@ -303,7 +306,12 @@ const resizeSingleTextElement = ( if (scale > 0) { const nextWidth = element.width * scale; const nextHeight = element.height * scale; - const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight); + const metrics = measureFontSizeFromWidth( + element, + elementsMap, + nextWidth, + nextHeight, + ); if (metrics === null) { return; } @@ -342,6 +350,7 @@ export const resizeSingleElement = ( originalElements: PointerDownState["originalElements"], shouldMaintainAspectRatio: boolean, element: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, transformHandleDirection: TransformHandleDirection, shouldResizeFromCenter: boolean, pointerX: number, @@ -448,6 +457,7 @@ export const resizeSingleElement = ( const nextFont = measureFontSizeFromWidth( boundTextElement, + elementsMap, getBoundTextMaxWidth(updatedElement), getBoundTextMaxHeight(updatedElement, boundTextElement), ); @@ -637,8 +647,9 @@ export const resizeSingleElement = ( }; export const resizeMultipleElements = ( - pointerDownState: PointerDownState, + originalElements: PointerDownState["originalElements"], selectedElements: readonly NonDeletedExcalidrawElement[], + elementsMap: ElementsMap, transformHandleType: "nw" | "ne" | "sw" | "se", shouldResizeFromCenter: boolean, pointerX: number, @@ -658,7 +669,7 @@ export const resizeMultipleElements = ( }[], element, ) => { - const origElement = pointerDownState.originalElements.get(element.id); + const origElement = originalElements.get(element.id); if (origElement) { acc.push({ orig: origElement, latest: element }); } @@ -679,7 +690,7 @@ export const resizeMultipleElements = ( if (!textId) { return acc; } - const text = pointerDownState.originalElements.get(textId) ?? null; + const text = originalElements.get(textId) ?? null; if (!isBoundToContainer(text)) { return acc; } @@ -825,7 +836,12 @@ export const resizeMultipleElements = ( } if (isTextElement(orig)) { - const metrics = measureFontSizeFromWidth(orig, width, height); + const metrics = measureFontSizeFromWidth( + orig, + elementsMap, + width, + height, + ); if (!metrics) { return; } @@ -833,7 +849,7 @@ export const resizeMultipleElements = ( update.baseline = metrics.baseline; } - const boundTextElement = pointerDownState.originalElements.get( + const boundTextElement = originalElements.get( getBoundTextElementId(orig) ?? "", ) as ExcalidrawTextElementWithContainer | undefined; @@ -884,7 +900,7 @@ export const resizeMultipleElements = ( }; const rotateMultipleElements = ( - pointerDownState: PointerDownState, + originalElements: PointerDownState["originalElements"], elements: readonly NonDeletedExcalidrawElement[], pointerX: number, pointerY: number, @@ -906,8 +922,7 @@ const rotateMultipleElements = ( const cx = (x1 + x2) / 2; const cy = (y1 + y2) / 2; const origAngle = - pointerDownState.originalElements.get(element.id)?.angle ?? - element.angle; + originalElements.get(element.id)?.angle ?? element.angle; const [rotatedCX, rotatedCY] = rotate( cx, cy, diff --git a/packages/excalidraw/element/textElement.ts b/packages/excalidraw/element/textElement.ts index e084dfba3a..da1348ec2d 100644 --- a/packages/excalidraw/element/textElement.ts +++ b/packages/excalidraw/element/textElement.ts @@ -1,5 +1,6 @@ import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils"; import { + ElementsMap, ExcalidrawElement, ExcalidrawElementType, ExcalidrawTextContainer, @@ -682,17 +683,15 @@ export const getBoundTextElement = (element: ExcalidrawElement | null) => { }; export const getContainerElement = ( - element: - | (ExcalidrawElement & { - containerId: ExcalidrawElement["id"] | null; - }) - | null, -) => { + element: ExcalidrawTextElement | null, + elementsMap: ElementsMap, +): ExcalidrawTextContainer | null => { if (!element) { return null; } if (element.containerId) { - return Scene.getScene(element)?.getElement(element.containerId) || null; + return (elementsMap.get(element.containerId) || + null) as ExcalidrawTextContainer | null; } return null; }; @@ -752,28 +751,16 @@ export const getContainerCoords = (container: NonDeletedExcalidrawElement) => { }; }; -export const getTextElementAngle = (textElement: ExcalidrawTextElement) => { - const container = getContainerElement(textElement); +export const getTextElementAngle = ( + textElement: ExcalidrawTextElement, + container: ExcalidrawTextContainer | null, +) => { if (!container || isArrowElement(container)) { return textElement.angle; } return container.angle; }; -export const getBoundTextElementOffset = ( - boundTextElement: ExcalidrawTextElement | null, -) => { - const container = getContainerElement(boundTextElement); - if (!container || !boundTextElement) { - return 0; - } - if (isArrowElement(container)) { - return BOUND_TEXT_PADDING * 8; - } - - return BOUND_TEXT_PADDING; -}; - export const getBoundTextElementPosition = ( container: ExcalidrawElement, boundTextElement: ExcalidrawTextElementWithContainer, @@ -788,12 +775,12 @@ export const getBoundTextElementPosition = ( export const shouldAllowVerticalAlign = ( selectedElements: NonDeletedExcalidrawElement[], + elementsMap: ElementsMap, ) => { return selectedElements.some((element) => { - const hasBoundContainer = isBoundToContainer(element); - if (hasBoundContainer) { - const container = getContainerElement(element); - if (isTextElement(element) && isArrowElement(container)) { + if (isBoundToContainer(element)) { + const container = getContainerElement(element, elementsMap); + if (isArrowElement(container)) { return false; } return true; @@ -804,12 +791,12 @@ export const shouldAllowVerticalAlign = ( export const suppportsHorizontalAlign = ( selectedElements: NonDeletedExcalidrawElement[], + elementsMap: ElementsMap, ) => { return selectedElements.some((element) => { - const hasBoundContainer = isBoundToContainer(element); - if (hasBoundContainer) { - const container = getContainerElement(element); - if (isTextElement(element) && isArrowElement(container)) { + if (isBoundToContainer(element)) { + const container = getContainerElement(element, elementsMap); + if (isArrowElement(container)) { return false; } return true; diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index 52f89e0b91..801f0c4405 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -153,7 +153,10 @@ export const textWysiwyg = ({ if (updatedTextElement && isTextElement(updatedTextElement)) { let coordX = updatedTextElement.x; let coordY = updatedTextElement.y; - const container = getContainerElement(updatedTextElement); + const container = getContainerElement( + updatedTextElement, + app.scene.getElementsMapIncludingDeleted(), + ); let maxWidth = updatedTextElement.width; let maxHeight = updatedTextElement.height; @@ -277,7 +280,7 @@ export const textWysiwyg = ({ transform: getTransform( textElementWidth, textElementHeight, - getTextElementAngle(updatedTextElement), + getTextElementAngle(updatedTextElement, container), appState, maxWidth, editorMaxHeight, @@ -348,7 +351,10 @@ export const textWysiwyg = ({ if (!data) { return; } - const container = getContainerElement(element); + const container = getContainerElement( + element, + app.scene.getElementsMapIncludingDeleted(), + ); const font = getFontString({ fontSize: app.state.currentItemFontSize, @@ -528,7 +534,10 @@ export const textWysiwyg = ({ return; } let text = editable.value; - const container = getContainerElement(updateElement); + const container = getContainerElement( + updateElement, + app.scene.getElementsMapIncludingDeleted(), + ); if (container) { text = updateElement.text; diff --git a/packages/excalidraw/element/types.ts b/packages/excalidraw/element/types.ts index c468eac82f..7659ad1e90 100644 --- a/packages/excalidraw/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -6,7 +6,7 @@ import { THEME, VERTICAL_ALIGN, } from "../constants"; -import { MarkNonNullable, ValueOf } from "../utility-types"; +import { MakeBrand, MarkNonNullable, ValueOf } from "../utility-types"; import { MagicCacheData } from "../data/magic"; export type ChartType = "bar" | "line"; @@ -254,3 +254,31 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase & export type FileId = string & { _brand: "FileId" }; export type ExcalidrawElementType = ExcalidrawElement["type"]; + +/** + * Map of excalidraw elements. + * Unspecified whether deleted or non-deleted. + * Can be a subset of Scene elements. + */ +export type ElementsMap = Map; + +/** + * Map of non-deleted elements. + * Can be a subset of Scene elements. + */ +export type NonDeletedElementsMap = Map< + ExcalidrawElement["id"], + NonDeletedExcalidrawElement +> & + MakeBrand<"NonDeletedElementsMap">; + +/** + * Map of all excalidraw Scene elements, including deleted. + * Not a subset. Use this type when you need access to current Scene elements. + */ +export type SceneElementsMap = Map & + MakeBrand<"SceneElementsMap">; + +export type ElementsMapOrArray = + | readonly ExcalidrawElement[] + | Readonly; diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index a5e7437111..ecb70ef1e4 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -4,6 +4,8 @@ import { isTextElement, } from "./element"; import { + ElementsMap, + ElementsMapOrArray, ExcalidrawElement, ExcalidrawFrameLikeElement, NonDeleted, @@ -26,6 +28,7 @@ import { elementsOverlappingBBox, } from "../utils/export"; import { isFrameElement, isFrameLikeElement } from "./element/typeChecks"; +import { ReadonlySetLike } from "./utility-types"; // --------------------------- Frame State ------------------------------------ export const bindElementsToFramesAfterDuplication = ( @@ -211,9 +214,17 @@ export const groupByFrameLikes = (elements: readonly ExcalidrawElement[]) => { }; export const getFrameChildren = ( - allElements: ExcalidrawElementsIncludingDeleted, + allElements: ElementsMapOrArray, frameId: string, -) => allElements.filter((element) => element.frameId === frameId); +) => { + const frameChildren: ExcalidrawElement[] = []; + for (const element of allElements.values()) { + if (element.frameId === frameId) { + frameChildren.push(element); + } + } + return frameChildren; +}; export const getFrameLikeElements = ( allElements: ExcalidrawElementsIncludingDeleted, @@ -425,23 +436,20 @@ export const filterElementsEligibleAsFrameChildren = ( * Retains (or repairs for target frame) the ordering invriant where children * elements come right before the parent frame: * [el, el, child, child, frame, el] + * + * @returns mutated allElements (same data structure) */ -export const addElementsToFrame = ( - allElements: ExcalidrawElementsIncludingDeleted, +export const addElementsToFrame = ( + allElements: T, elementsToAdd: NonDeletedExcalidrawElement[], frame: ExcalidrawFrameLikeElement, -) => { - const { currTargetFrameChildrenMap } = allElements.reduce( - (acc, element, index) => { - if (element.frameId === frame.id) { - acc.currTargetFrameChildrenMap.set(element.id, true); - } - return acc; - }, - { - currTargetFrameChildrenMap: new Map(), - }, - ); +): T => { + const currTargetFrameChildrenMap = new Map(); + for (const element of allElements.values()) { + if (element.frameId === frame.id) { + currTargetFrameChildrenMap.set(element.id, true); + } + } const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id)); @@ -492,13 +500,12 @@ export const addElementsToFrame = ( false, ); } - return allElements.slice(); + + return allElements; }; export const removeElementsFromFrame = ( - allElements: ExcalidrawElementsIncludingDeleted, - elementsToRemove: NonDeletedExcalidrawElement[], - appState: AppState, + elementsToRemove: ReadonlySetLike, ) => { const _elementsToRemove = new Map< ExcalidrawElement["id"], @@ -536,35 +543,34 @@ export const removeElementsFromFrame = ( false, ); } - - return allElements.slice(); }; -export const removeAllElementsFromFrame = ( - allElements: ExcalidrawElementsIncludingDeleted, +export const removeAllElementsFromFrame = ( + allElements: readonly T[], frame: ExcalidrawFrameLikeElement, - appState: AppState, ) => { const elementsInFrame = getFrameChildren(allElements, frame.id); - return removeElementsFromFrame(allElements, elementsInFrame, appState); + removeElementsFromFrame(elementsInFrame); + return allElements; }; -export const replaceAllElementsInFrame = ( - allElements: ExcalidrawElementsIncludingDeleted, +export const replaceAllElementsInFrame = ( + allElements: readonly T[], nextElementsInFrame: ExcalidrawElement[], frame: ExcalidrawFrameLikeElement, - appState: AppState, -) => { +): T[] => { return addElementsToFrame( - removeAllElementsFromFrame(allElements, frame, appState), + removeAllElementsFromFrame(allElements, frame), nextElementsInFrame, frame, - ); + ).slice(); }; /** does not mutate elements, but returns new ones */ -export const updateFrameMembershipOfSelectedElements = ( - allElements: ExcalidrawElementsIncludingDeleted, +export const updateFrameMembershipOfSelectedElements = < + T extends ElementsMapOrArray, +>( + allElements: T, appState: AppState, app: AppClassProperties, ) => { @@ -589,19 +595,22 @@ export const updateFrameMembershipOfSelectedElements = ( const elementsToRemove = new Set(); + const elementsMap = arrayToMap(allElements); + elementsToFilter.forEach((element) => { if ( element.frameId && !isFrameLikeElement(element) && - !isElementInFrame(element, allElements, appState) + !isElementInFrame(element, elementsMap, appState) ) { elementsToRemove.add(element); } }); - return elementsToRemove.size > 0 - ? removeElementsFromFrame(allElements, [...elementsToRemove], appState) - : allElements; + if (elementsToRemove.size > 0) { + removeElementsFromFrame(elementsToRemove); + } + return allElements; }; /** @@ -609,14 +618,16 @@ export const updateFrameMembershipOfSelectedElements = ( * anywhere in the group tree */ export const omitGroupsContainingFrameLikes = ( - allElements: ExcalidrawElementsIncludingDeleted, + allElements: ElementsMapOrArray, /** subset of elements you want to filter. Optional perf optimization so we * don't have to filter all elements unnecessarily */ selectedElements?: readonly ExcalidrawElement[], ) => { const uniqueGroupIds = new Set(); - for (const el of selectedElements || allElements) { + const elements = selectedElements || allElements; + + for (const el of elements.values()) { const topMostGroupId = el.groupIds[el.groupIds.length - 1]; if (topMostGroupId) { uniqueGroupIds.add(topMostGroupId); @@ -634,9 +645,15 @@ export const omitGroupsContainingFrameLikes = ( } } - return (selectedElements || allElements).filter( - (el) => !rejectedGroupIds.has(el.groupIds[el.groupIds.length - 1]), - ); + const ret: ExcalidrawElement[] = []; + + for (const element of elements.values()) { + if (!rejectedGroupIds.has(element.groupIds[element.groupIds.length - 1])) { + ret.push(element); + } + } + + return ret; }; /** @@ -645,10 +662,11 @@ export const omitGroupsContainingFrameLikes = ( */ export const getTargetFrame = ( element: ExcalidrawElement, + elementsMap: ElementsMap, appState: StaticCanvasAppState, ) => { const _element = isTextElement(element) - ? getContainerElement(element) || element + ? getContainerElement(element, elementsMap) || element : element; return appState.selectedElementIds[_element.id] && @@ -661,12 +679,12 @@ export const getTargetFrame = ( // given an element, return if the element is in some frame export const isElementInFrame = ( element: ExcalidrawElement, - allElements: ExcalidrawElementsIncludingDeleted, + allElements: ElementsMap, appState: StaticCanvasAppState, ) => { - const frame = getTargetFrame(element, appState); + const frame = getTargetFrame(element, allElements, appState); const _element = isTextElement(element) - ? getContainerElement(element) || element + ? getContainerElement(element, allElements) || element : element; if (frame) { diff --git a/packages/excalidraw/groups.ts b/packages/excalidraw/groups.ts index dd5512ba12..b0bedc4f95 100644 --- a/packages/excalidraw/groups.ts +++ b/packages/excalidraw/groups.ts @@ -3,6 +3,7 @@ import { ExcalidrawElement, NonDeleted, NonDeletedExcalidrawElement, + ElementsMapOrArray, } from "./element/types"; import { AppClassProperties, @@ -270,9 +271,17 @@ export const isElementInGroup = (element: ExcalidrawElement, groupId: string) => element.groupIds.includes(groupId); export const getElementsInGroup = ( - elements: readonly ExcalidrawElement[], + elements: ElementsMapOrArray, groupId: string, -) => elements.filter((element) => isElementInGroup(element, groupId)); +) => { + const elementsInGroup: ExcalidrawElement[] = []; + for (const element of elements.values()) { + if (isElementInGroup(element, groupId)) { + elementsInGroup.push(element); + } + } + return elementsInGroup; +}; export const getSelectedGroupIdForElement = ( element: ExcalidrawElement, diff --git a/packages/excalidraw/renderer/renderElement.ts b/packages/excalidraw/renderer/renderElement.ts index 94eda49f93..39e6c49749 100644 --- a/packages/excalidraw/renderer/renderElement.ts +++ b/packages/excalidraw/renderer/renderElement.ts @@ -21,7 +21,11 @@ import type { RoughCanvas } from "roughjs/bin/canvas"; import type { Drawable } from "roughjs/bin/core"; import type { RoughSVG } from "roughjs/bin/svg"; -import { SVGRenderConfig, StaticCanvasRenderConfig } from "../scene/types"; +import { + SVGRenderConfig, + StaticCanvasRenderConfig, + RenderableElementsMap, +} from "../scene/types"; import { distance, getFontString, @@ -611,6 +615,7 @@ export const renderSelectionElement = ( export const renderElement = ( element: NonDeletedExcalidrawElement, + elementsMap: RenderableElementsMap, rc: RoughCanvas, context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, @@ -715,7 +720,7 @@ export const renderElement = ( let shiftX = (x2 - x1) / 2 - (element.x - x1); let shiftY = (y2 - y1) / 2 - (element.y - y1); if (isTextElement(element)) { - const container = getContainerElement(element); + const container = getContainerElement(element, elementsMap); if (isArrowElement(container)) { const boundTextCoords = LinearElementEditor.getBoundTextElementPosition( @@ -900,6 +905,7 @@ const maybeWrapNodesInFrameClipPath = ( export const renderElementToSvg = ( element: NonDeletedExcalidrawElement, + elementsMap: RenderableElementsMap, rsvg: RoughSVG, svgRoot: SVGElement, files: BinaryFiles, @@ -912,7 +918,7 @@ export const renderElementToSvg = ( let cx = (x2 - x1) / 2 - (element.x - x1); let cy = (y2 - y1) / 2 - (element.y - y1); if (isTextElement(element)) { - const container = getContainerElement(element); + const container = getContainerElement(element, elementsMap); if (isArrowElement(container)) { const [x1, y1, x2, y2] = getElementAbsoluteCoords(container); @@ -1013,6 +1019,7 @@ export const renderElementToSvg = ( createPlaceholderEmbeddableLabel(element); renderElementToSvg( label, + elementsMap, rsvg, root, files, diff --git a/packages/excalidraw/renderer/renderScene.ts b/packages/excalidraw/renderer/renderScene.ts index a5b78d3b83..0a066bfd44 100644 --- a/packages/excalidraw/renderer/renderScene.ts +++ b/packages/excalidraw/renderer/renderScene.ts @@ -33,6 +33,7 @@ import { SVGRenderConfig, StaticCanvasRenderConfig, StaticSceneRenderConfig, + RenderableElementsMap, } from "../scene/types"; import { getScrollBars, @@ -61,7 +62,7 @@ import { TransformHandles, TransformHandleType, } from "../element/transformHandles"; -import { throttleRAF } from "../utils"; +import { arrayToMap, throttleRAF } from "../utils"; import { UserIdleState } from "../types"; import { FRAME_STYLE, THEME_FILTER } from "../constants"; import { @@ -75,10 +76,7 @@ import { isIframeLikeElement, isLinearElement, } from "../element/typeChecks"; -import { - isIframeLikeOrItsLabel, - createPlaceholderEmbeddableLabel, -} from "../element/embeddable"; +import { createPlaceholderEmbeddableLabel } from "../element/embeddable"; import { elementOverlapsWithFrame, getTargetFrame, @@ -446,7 +444,7 @@ const bootstrapCanvas = ({ const _renderInteractiveScene = ({ canvas, - elements, + elementsMap, visibleElements, selectedElements, scale, @@ -454,7 +452,7 @@ const _renderInteractiveScene = ({ renderConfig, }: InteractiveSceneRenderConfig) => { if (canvas === null) { - return { atLeastOneVisibleElement: false, elements }; + return { atLeastOneVisibleElement: false, elementsMap }; } const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions( @@ -562,75 +560,64 @@ const _renderInteractiveScene = ({ if (showBoundingBox) { // Optimisation for finding quickly relevant element ids - const locallySelectedIds = selectedElements.reduce( - (acc: Record, element) => { - acc[element.id] = true; - return acc; - }, - {}, - ); + const locallySelectedIds = arrayToMap(selectedElements); - const selections = elements.reduce( - ( - acc: { - angle: number; - elementX1: number; - elementY1: number; - elementX2: number; - elementY2: number; - selectionColors: string[]; - dashed?: boolean; - cx: number; - cy: number; - activeEmbeddable: boolean; - }[], - element, - ) => { - const selectionColors = []; - // local user - if ( - locallySelectedIds[element.id] && - !isSelectedViaGroup(appState, element) - ) { - selectionColors.push(selectionColor); - } - // remote users - if (renderConfig.remoteSelectedElementIds[element.id]) { - selectionColors.push( - ...renderConfig.remoteSelectedElementIds[element.id].map( - (socketId: string) => { - const background = getClientColor(socketId); - return background; - }, - ), - ); - } + const selections: { + angle: number; + elementX1: number; + elementY1: number; + elementX2: number; + elementY2: number; + selectionColors: string[]; + dashed?: boolean; + cx: number; + cy: number; + activeEmbeddable: boolean; + }[] = []; - if (selectionColors.length) { - const [elementX1, elementY1, elementX2, elementY2, cx, cy] = - getElementAbsoluteCoords(element, true); - acc.push({ - angle: element.angle, - elementX1, - elementY1, - elementX2, - elementY2, - selectionColors, - dashed: !!renderConfig.remoteSelectedElementIds[element.id], - cx, - cy, - activeEmbeddable: - appState.activeEmbeddable?.element === element && - appState.activeEmbeddable.state === "active", - }); - } - return acc; - }, - [], - ); + for (const element of elementsMap.values()) { + const selectionColors = []; + // local user + if ( + locallySelectedIds.has(element.id) && + !isSelectedViaGroup(appState, element) + ) { + selectionColors.push(selectionColor); + } + // remote users + if (renderConfig.remoteSelectedElementIds[element.id]) { + selectionColors.push( + ...renderConfig.remoteSelectedElementIds[element.id].map( + (socketId: string) => { + const background = getClientColor(socketId); + return background; + }, + ), + ); + } + + if (selectionColors.length) { + const [elementX1, elementY1, elementX2, elementY2, cx, cy] = + getElementAbsoluteCoords(element, true); + selections.push({ + angle: element.angle, + elementX1, + elementY1, + elementX2, + elementY2, + selectionColors, + dashed: !!renderConfig.remoteSelectedElementIds[element.id], + cx, + cy, + activeEmbeddable: + appState.activeEmbeddable?.element === element && + appState.activeEmbeddable.state === "active", + }); + } + } const addSelectionForGroupId = (groupId: GroupId) => { - const groupElements = getElementsInGroup(elements, groupId); + const groupElements = getElementsInGroup(elementsMap, groupId); const [elementX1, elementY1, elementX2, elementY2] = getCommonBounds(groupElements); selections.push({ @@ -870,7 +857,7 @@ const _renderInteractiveScene = ({ let scrollBars; if (renderConfig.renderScrollbars) { scrollBars = getScrollBars( - elements, + elementsMap, normalizedWidth, normalizedHeight, appState, @@ -897,14 +884,14 @@ const _renderInteractiveScene = ({ return { scrollBars, atLeastOneVisibleElement: visibleElements.length > 0, - elements, + elementsMap, }; }; const _renderStaticScene = ({ canvas, rc, - elements, + elementsMap, visibleElements, scale, appState, @@ -965,7 +952,7 @@ const _renderStaticScene = ({ // Paint visible elements visibleElements - .filter((el) => !isIframeLikeOrItsLabel(el)) + .filter((el) => !isIframeLikeElement(el)) .forEach((element) => { try { const frameId = element.frameId || appState.frameToHighlight?.id; @@ -977,16 +964,30 @@ const _renderStaticScene = ({ ) { context.save(); - const frame = getTargetFrame(element, appState); + const frame = getTargetFrame(element, elementsMap, appState); // TODO do we need to check isElementInFrame here? - if (frame && isElementInFrame(element, elements, appState)) { + if (frame && isElementInFrame(element, elementsMap, appState)) { frameClip(frame, context, renderConfig, appState); } - renderElement(element, rc, context, renderConfig, appState); + renderElement( + element, + elementsMap, + rc, + context, + renderConfig, + appState, + ); context.restore(); } else { - renderElement(element, rc, context, renderConfig, appState); + renderElement( + element, + elementsMap, + rc, + context, + renderConfig, + appState, + ); } if (!isExporting) { renderLinkIcon(element, context, appState); @@ -998,11 +999,18 @@ const _renderStaticScene = ({ // render embeddables on top visibleElements - .filter((el) => isIframeLikeOrItsLabel(el)) + .filter((el) => isIframeLikeElement(el)) .forEach((element) => { try { const render = () => { - renderElement(element, rc, context, renderConfig, appState); + renderElement( + element, + elementsMap, + rc, + context, + renderConfig, + appState, + ); if ( isIframeLikeElement(element) && @@ -1014,7 +1022,14 @@ const _renderStaticScene = ({ element.height ) { const label = createPlaceholderEmbeddableLabel(element); - renderElement(label, rc, context, renderConfig, appState); + renderElement( + label, + elementsMap, + rc, + context, + renderConfig, + appState, + ); } if (!isExporting) { renderLinkIcon(element, context, appState); @@ -1032,9 +1047,9 @@ const _renderStaticScene = ({ ) { context.save(); - const frame = getTargetFrame(element, appState); + const frame = getTargetFrame(element, elementsMap, appState); - if (frame && isElementInFrame(element, elements, appState)) { + if (frame && isElementInFrame(element, elementsMap, appState)) { frameClip(frame, context, renderConfig, appState); } render(); @@ -1448,6 +1463,7 @@ const renderLinkIcon = ( // This should be only called for exporting purposes export const renderSceneToSvg = ( elements: readonly NonDeletedExcalidrawElement[], + elementsMap: RenderableElementsMap, rsvg: RoughSVG, svgRoot: SVGElement, files: BinaryFiles, @@ -1459,12 +1475,13 @@ export const renderSceneToSvg = ( // render elements elements - .filter((el) => !isIframeLikeOrItsLabel(el)) + .filter((el) => !isIframeLikeElement(el)) .forEach((element) => { if (!element.isDeleted) { try { renderElementToSvg( element, + elementsMap, rsvg, svgRoot, files, @@ -1486,6 +1503,7 @@ export const renderSceneToSvg = ( try { renderElementToSvg( element, + elementsMap, rsvg, svgRoot, files, diff --git a/packages/excalidraw/scene/Fonts.ts b/packages/excalidraw/scene/Fonts.ts index 05dddadc48..1a97c06e02 100644 --- a/packages/excalidraw/scene/Fonts.ts +++ b/packages/excalidraw/scene/Fonts.ts @@ -1,5 +1,6 @@ import { isTextElement, refreshTextDimensions } from "../element"; import { newElementWith } from "../element/mutateElement"; +import { getContainerElement } from "../element/textElement"; import { isBoundToContainer } from "../element/typeChecks"; import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types"; import { getFontString } from "../utils"; @@ -57,7 +58,13 @@ export class Fonts { ShapeCache.delete(element); didUpdate = true; return newElementWith(element, { - ...refreshTextDimensions(element), + ...refreshTextDimensions( + element, + getContainerElement( + element, + this.scene.getElementsMapIncludingDeleted(), + ), + ), }); } return element; diff --git a/packages/excalidraw/scene/Renderer.ts b/packages/excalidraw/scene/Renderer.ts index 1522249510..1593d6d2e2 100644 --- a/packages/excalidraw/scene/Renderer.ts +++ b/packages/excalidraw/scene/Renderer.ts @@ -1,10 +1,14 @@ import { isElementInViewport } from "../element/sizeHelpers"; import { isImageElement } from "../element/typeChecks"; -import { NonDeletedExcalidrawElement } from "../element/types"; +import { + NonDeletedElementsMap, + NonDeletedExcalidrawElement, +} from "../element/types"; import { cancelRender } from "../renderer/renderScene"; import { AppState } from "../types"; -import { memoize } from "../utils"; +import { memoize, toBrandedType } from "../utils"; import Scene from "./Scene"; +import { RenderableElementsMap } from "./types"; export class Renderer { private scene: Scene; @@ -15,7 +19,7 @@ export class Renderer { public getRenderableElements = (() => { const getVisibleCanvasElements = ({ - elements, + elementsMap, zoom, offsetLeft, offsetTop, @@ -24,7 +28,7 @@ export class Renderer { height, width, }: { - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: NonDeletedElementsMap; zoom: AppState["zoom"]; offsetLeft: AppState["offsetLeft"]; offsetTop: AppState["offsetTop"]; @@ -33,43 +37,55 @@ export class Renderer { height: AppState["height"]; width: AppState["width"]; }): readonly NonDeletedExcalidrawElement[] => { - return elements.filter((element) => - isElementInViewport(element, width, height, { - zoom, - offsetLeft, - offsetTop, - scrollX, - scrollY, - }), - ); + const visibleElements: NonDeletedExcalidrawElement[] = []; + for (const element of elementsMap.values()) { + if ( + isElementInViewport(element, width, height, { + zoom, + offsetLeft, + offsetTop, + scrollX, + scrollY, + }) + ) { + visibleElements.push(element); + } + } + return visibleElements; }; - const getCanvasElements = ({ - editingElement, + const getRenderableElements = ({ elements, + editingElement, pendingImageElementId, }: { elements: readonly NonDeletedExcalidrawElement[]; editingElement: AppState["editingElement"]; pendingImageElementId: AppState["pendingImageElementId"]; }) => { - return elements.filter((element) => { + const elementsMap = toBrandedType(new Map()); + + for (const element of elements) { if (isImageElement(element)) { if ( // => not placed on canvas yet (but in elements array) pendingImageElementId === element.id ) { - return false; + continue; } } + // we don't want to render text element that's being currently edited // (it's rendered on remote only) - return ( + if ( !editingElement || editingElement.type !== "text" || element.id !== editingElement.id - ); - }); + ) { + elementsMap.set(element.id, element); + } + } + return elementsMap; }; return memoize( @@ -100,14 +116,14 @@ export class Renderer { }) => { const elements = this.scene.getNonDeletedElements(); - const canvasElements = getCanvasElements({ + const elementsMap = getRenderableElements({ elements, editingElement, pendingImageElementId, }); const visibleElements = getVisibleCanvasElements({ - elements: canvasElements, + elementsMap, zoom, offsetLeft, offsetTop, @@ -117,7 +133,7 @@ export class Renderer { width, }); - return { canvasElements, visibleElements }; + return { elementsMap, visibleElements }; }, ); })(); diff --git a/packages/excalidraw/scene/Scene.ts b/packages/excalidraw/scene/Scene.ts index 814638e7e3..326f98c7fb 100644 --- a/packages/excalidraw/scene/Scene.ts +++ b/packages/excalidraw/scene/Scene.ts @@ -3,14 +3,18 @@ import { NonDeletedExcalidrawElement, NonDeleted, ExcalidrawFrameLikeElement, + ElementsMapOrArray, + NonDeletedElementsMap, + SceneElementsMap, } from "../element/types"; -import { getNonDeletedElements, isNonDeletedElement } from "../element"; +import { isNonDeletedElement } from "../element"; import { LinearElementEditor } from "../element/linearElementEditor"; import { isFrameLikeElement } from "../element/typeChecks"; import { getSelectedElements } from "./selection"; import { AppState } from "../types"; import { Assert, SameType } from "../utility-types"; import { randomInteger } from "../random"; +import { toBrandedType } from "../utils"; type ElementIdKey = InstanceType["elementId"]; type ElementKey = ExcalidrawElement | ElementIdKey; @@ -20,6 +24,20 @@ type SceneStateCallbackRemover = () => void; type SelectionHash = string & { __brand: "selectionHash" }; +const getNonDeletedElements = ( + allElements: readonly T[], +) => { + const elementsMap = new Map() as NonDeletedElementsMap; + const elements: T[] = []; + for (const element of allElements) { + if (!element.isDeleted) { + elements.push(element as NonDeleted); + elementsMap.set(element.id, element as NonDeletedExcalidrawElement); + } + } + return { elementsMap, elements }; +}; + const hashSelectionOpts = ( opts: Parameters["getSelectedElements"]>[0], ) => { @@ -102,11 +120,13 @@ class Scene { private callbacks: Set = new Set(); private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = []; + private nonDeletedElementsMap: NonDeletedElementsMap = + new Map() as NonDeletedElementsMap; private elements: readonly ExcalidrawElement[] = []; private nonDeletedFramesLikes: readonly NonDeleted[] = []; private frames: readonly ExcalidrawFrameLikeElement[] = []; - private elementsMap = new Map(); + private elementsMap = toBrandedType(new Map()); private selectedElementsCache: { selectedElementIds: AppState["selectedElementIds"] | null; elements: readonly NonDeletedExcalidrawElement[] | null; @@ -118,6 +138,14 @@ class Scene { }; private versionNonce: number | undefined; + getElementsMapIncludingDeleted() { + return this.elementsMap; + } + + getNonDeletedElementsMap() { + return this.nonDeletedElementsMap; + } + getElementsIncludingDeleted() { return this.elements; } @@ -138,7 +166,7 @@ class Scene { * scene state. This in effect will likely result in cache-miss, and * the cache won't be updated in this case. */ - elements?: readonly ExcalidrawElement[]; + elements?: ElementsMapOrArray; // selection-related options includeBoundTextElement?: boolean; includeElementsInFrames?: boolean; @@ -227,23 +255,27 @@ class Scene { return didChange; } - replaceAllElements( - nextElements: readonly ExcalidrawElement[], - mapElementIds = true, - ) { - this.elements = nextElements; + replaceAllElements(nextElements: ElementsMapOrArray, mapElementIds = true) { + this.elements = + // ts doesn't like `Array.isArray` of `instanceof Map` + nextElements instanceof Array + ? nextElements + : Array.from(nextElements.values()); const nextFrameLikes: ExcalidrawFrameLikeElement[] = []; this.elementsMap.clear(); - nextElements.forEach((element) => { + this.elements.forEach((element) => { if (isFrameLikeElement(element)) { nextFrameLikes.push(element); } this.elementsMap.set(element.id, element); - Scene.mapElementToScene(element, this); + Scene.mapElementToScene(element, this, mapElementIds); }); - this.nonDeletedElements = getNonDeletedElements(this.elements); + const nonDeletedElements = getNonDeletedElements(this.elements); + this.nonDeletedElements = nonDeletedElements.elements; + this.nonDeletedElementsMap = nonDeletedElements.elementsMap; + this.frames = nextFrameLikes; - this.nonDeletedFramesLikes = getNonDeletedElements(this.frames); + this.nonDeletedFramesLikes = getNonDeletedElements(this.frames).elements; this.informMutation(); } @@ -332,6 +364,22 @@ class Scene { getElementIndex(elementId: string) { return this.elements.findIndex((element) => element.id === elementId); } + + getContainerElement = ( + element: + | (ExcalidrawElement & { + containerId: ExcalidrawElement["id"] | null; + }) + | null, + ) => { + if (!element) { + return null; + } + if (element.containerId) { + return this.getElement(element.containerId) || null; + } + return null; + }; } export default Scene; diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index 9c357a21fd..61ba8580db 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -11,7 +11,13 @@ import { getElementAbsoluteCoords, } from "../element/bounds"; import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene"; -import { cloneJSON, distance, getFontString } from "../utils"; +import { + arrayToMap, + cloneJSON, + distance, + getFontString, + toBrandedType, +} from "../utils"; import { AppState, BinaryFiles } from "../types"; import { DEFAULT_EXPORT_PADDING, @@ -37,6 +43,7 @@ import { Mutable } from "../utility-types"; import { newElementWith } from "../element/mutateElement"; import Scene from "./Scene"; import { isFrameElement, isFrameLikeElement } from "../element/typeChecks"; +import { RenderableElementsMap } from "./types"; const SVG_EXPORT_TAG = ``; @@ -59,7 +66,7 @@ const __createSceneForElementsHack__ = ( // ids to Scene instances so that we don't override the editor elements // mapping. // We still need to clone the objects themselves to regen references. - scene.replaceAllElements(cloneJSON(elements), false); + scene.replaceAllElements(cloneJSON(elements)); return scene; }; @@ -241,10 +248,14 @@ export const exportToCanvas = async ( files, }); + const elementsMap = toBrandedType( + arrayToMap(elementsForRender), + ); + renderStaticScene({ canvas, rc: rough.canvas(canvas), - elements: elementsForRender, + elementsMap, visibleElements: elementsForRender, scale, appState: { @@ -432,22 +443,29 @@ export const exportToSvg = async ( const renderEmbeddables = opts?.renderEmbeddables ?? false; - renderSceneToSvg(elementsForRender, rsvg, svgRoot, files || {}, { - offsetX, - offsetY, - isExporting: true, - exportWithDarkMode, - renderEmbeddables, - frameRendering, - canvasBackgroundColor: viewBackgroundColor, - embedsValidationStatus: renderEmbeddables - ? new Map( - elementsForRender - .filter((element) => isFrameLikeElement(element)) - .map((element) => [element.id, true]), - ) - : new Map(), - }); + renderSceneToSvg( + elementsForRender, + toBrandedType(arrayToMap(elementsForRender)), + rsvg, + svgRoot, + files || {}, + { + offsetX, + offsetY, + isExporting: true, + exportWithDarkMode, + renderEmbeddables, + frameRendering, + canvasBackgroundColor: viewBackgroundColor, + embedsValidationStatus: renderEmbeddables + ? new Map( + elementsForRender + .filter((element) => isFrameLikeElement(element)) + .map((element) => [element.id, true]), + ) + : new Map(), + }, + ); tempScene.destroy(); diff --git a/packages/excalidraw/scene/scrollbars.ts b/packages/excalidraw/scene/scrollbars.ts index 1d93f688f7..14009588bb 100644 --- a/packages/excalidraw/scene/scrollbars.ts +++ b/packages/excalidraw/scene/scrollbars.ts @@ -1,7 +1,6 @@ -import { ExcalidrawElement } from "../element/types"; import { getCommonBounds } from "../element"; import { InteractiveCanvasAppState } from "../types"; -import { ScrollBars } from "./types"; +import { RenderableElementsMap, ScrollBars } from "./types"; import { getGlobalCSSVariable } from "../utils"; import { getLanguage } from "../i18n"; @@ -10,12 +9,12 @@ export const SCROLLBAR_WIDTH = 6; export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)"; export const getScrollBars = ( - elements: readonly ExcalidrawElement[], + elements: RenderableElementsMap, viewportWidth: number, viewportHeight: number, appState: InteractiveCanvasAppState, ): ScrollBars => { - if (elements.length === 0) { + if (!elements.size) { return { horizontal: null, vertical: null, diff --git a/packages/excalidraw/scene/selection.ts b/packages/excalidraw/scene/selection.ts index 7a620155f5..ae021f6aac 100644 --- a/packages/excalidraw/scene/selection.ts +++ b/packages/excalidraw/scene/selection.ts @@ -1,4 +1,5 @@ import { + ElementsMapOrArray, ExcalidrawElement, NonDeletedExcalidrawElement, } from "../element/types"; @@ -166,26 +167,28 @@ export const getCommonAttributeOfSelectedElements = ( }; export const getSelectedElements = ( - elements: readonly NonDeletedExcalidrawElement[], + elements: ElementsMapOrArray, appState: Pick, opts?: { includeBoundTextElement?: boolean; includeElementsInFrames?: boolean; }, ) => { - const selectedElements = elements.filter((element) => { + const selectedElements: ExcalidrawElement[] = []; + for (const element of elements.values()) { if (appState.selectedElementIds[element.id]) { - return element; + selectedElements.push(element); + continue; } if ( opts?.includeBoundTextElement && isBoundToContainer(element) && appState.selectedElementIds[element?.containerId] ) { - return element; + selectedElements.push(element); + continue; } - return null; - }); + } if (opts?.includeElementsInFrames) { const elementsToInclude: ExcalidrawElement[] = []; @@ -205,7 +208,7 @@ export const getSelectedElements = ( }; export const getTargetElements = ( - elements: readonly NonDeletedExcalidrawElement[], + elements: ElementsMapOrArray, appState: Pick, ) => appState.editingElement diff --git a/packages/excalidraw/scene/types.ts b/packages/excalidraw/scene/types.ts index 57a52fbd4e..957b080b38 100644 --- a/packages/excalidraw/scene/types.ts +++ b/packages/excalidraw/scene/types.ts @@ -2,6 +2,7 @@ import type { RoughCanvas } from "roughjs/bin/canvas"; import { Drawable } from "roughjs/bin/core"; import { ExcalidrawTextElement, + NonDeletedElementsMap, NonDeletedExcalidrawElement, } from "../element/types"; import { @@ -12,6 +13,10 @@ import { InteractiveCanvasAppState, StaticCanvasAppState, } from "../types"; +import { MakeBrand } from "../utility-types"; + +export type RenderableElementsMap = NonDeletedElementsMap & + MakeBrand<"RenderableElementsMap">; export type StaticCanvasRenderConfig = { canvasBackgroundColor: AppState["viewBackgroundColor"]; @@ -53,14 +58,14 @@ export type InteractiveCanvasRenderConfig = { export type RenderInteractiveSceneCallback = { atLeastOneVisibleElement: boolean; - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: RenderableElementsMap; scrollBars?: ScrollBars; }; export type StaticSceneRenderConfig = { canvas: HTMLCanvasElement; rc: RoughCanvas; - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: RenderableElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; scale: number; appState: StaticCanvasAppState; @@ -69,7 +74,7 @@ export type StaticSceneRenderConfig = { export type InteractiveSceneRenderConfig = { canvas: HTMLCanvasElement | null; - elements: readonly NonDeletedExcalidrawElement[]; + elementsMap: RenderableElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; selectedElements: readonly NonDeletedExcalidrawElement[]; scale: number; diff --git a/packages/excalidraw/utility-types.ts b/packages/excalidraw/utility-types.ts index 860d818efa..576769634c 100644 --- a/packages/excalidraw/utility-types.ts +++ b/packages/excalidraw/utility-types.ts @@ -54,3 +54,11 @@ export type Assert = T; export type NestedKeyOf = K extends keyof T & (string | number) ? `${K}` | (T[K] extends object ? `${K}.${NestedKeyOf}` : never) : never; + +export type SetLike = Set | T[]; +export type ReadonlySetLike = ReadonlySet | readonly T[]; + +export type MakeBrand = { + /** @private using ~ to sort last in intellisense */ + [K in `~brand~${T}`]: T; +}; diff --git a/packages/excalidraw/utils.ts b/packages/excalidraw/utils.ts index 4630c5bced..47fa52311c 100644 --- a/packages/excalidraw/utils.ts +++ b/packages/excalidraw/utils.ts @@ -650,8 +650,11 @@ export const getUpdatedTimestamp = () => (isTestEnv() ? 1 : Date.now()); * or array of ids (strings), into a Map, keyd by `id`. */ export const arrayToMap = ( - items: readonly T[], + items: readonly T[] | Map, ) => { + if (items instanceof Map) { + return items; + } return items.reduce((acc: Map, element) => { acc.set(typeof element === "string" ? element : element.id, element); return acc; @@ -1050,3 +1053,40 @@ export function getSvgPathFromStroke(points: number[][], closed = true) { export const normalizeEOL = (str: string) => { return str.replace(/\r?\n|\r/g, "\n"); }; + +// ----------------------------------------------------------------------------- +type HasBrand = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [K in keyof T]: K extends `~brand${infer _}` ? true : never; +}[keyof T]; + +type RemoveAllBrands = HasBrand extends true + ? { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [K in keyof T as K extends `~brand~${infer _}` ? never : K]: T[K]; + } + : never; + +// adapted from https://github.com/colinhacks/zod/discussions/1994#discussioncomment-6068940 +// currently does not cover all types (e.g. tuples, promises...) +type Unbrand = T extends Map + ? Map + : T extends Set + ? Set + : T extends Array + ? Array + : RemoveAllBrands; + +/** + * Makes type into a branded type, ensuring that value is assignable to + * the base ubranded type. Optionally you can explicitly supply current value + * type to combine both (useful for composite branded types. Make sure you + * compose branded types which are not composite themselves.) + */ +export const toBrandedType = ( + value: Unbrand, +) => { + return value as CurrentType & BrandedType; +}; + +// ----------------------------------------------------------------------------- From c6fdac131b06c2d542a7068d1798f8ac83a41cfd Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Mon, 22 Jan 2024 17:01:00 +0530 Subject: [PATCH 10/31] ci: add the workspace ignore check to install actions as dependency for auto release (#7593) --- .github/workflows/autorelease-excalidraw.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/autorelease-excalidraw.yml b/.github/workflows/autorelease-excalidraw.yml index 4eaeb11f13..5ff5690ebf 100644 --- a/.github/workflows/autorelease-excalidraw.yml +++ b/.github/workflows/autorelease-excalidraw.yml @@ -23,5 +23,5 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Auto release run: | - yarn add @actions/core + yarn add @actions/core -W yarn autorelease From 89bd6181f29c783a59ad7943bb2a85f829dd9d49 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:23:00 +0100 Subject: [PATCH 11/31] fix: revert `mapElementIds` flag removal (#7594) --- packages/excalidraw/scene/export.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index 61ba8580db..9f1f12a22f 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -66,7 +66,7 @@ const __createSceneForElementsHack__ = ( // ids to Scene instances so that we don't override the editor elements // mapping. // We still need to clone the objects themselves to regen references. - scene.replaceAllElements(cloneJSON(elements)); + scene.replaceAllElements(cloneJSON(elements), false); return scene; }; From f3f82171252af7407fe1e8ab6790a72bbbd14477 Mon Sep 17 00:00:00 2001 From: halocean96 <146062795+halocean96@users.noreply.github.com> Date: Tue, 23 Jan 2024 23:50:51 +0900 Subject: [PATCH 12/31] docs: toggleSidebar api fix (#7575) --- .../docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx index c27e96146c..ffff19fb09 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx @@ -37,7 +37,7 @@ You can use this prop when you want to access some [Excalidraw APIs](https://git | [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool | | [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas | | [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas | -| [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off | +| [toggleSidebar](#toggleSidebar) | `function` | Toggles specific sidebar on/off | | [onChange](#onChange) | `function` | Subscribes to change events | | [onPointerDown](#onPointerDown) | `function` | Subscribes to `pointerdown` events | | [onPointerUp](#onPointerUp) | `function` | Subscribes to `pointerup` events | From 4f0a2a9593520111614bf84d95c4f35b97e82453 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 24 Jan 2024 17:07:54 +0530 Subject: [PATCH 13/31] docs: add next js with app router example (#7552) * move the existing example to with-script-in-browser * Add example with next js app router * disable ssr for excalidraw client comp * typo * update output dir * don't include nextjs example in tsconfig * remove meta.json * lint * remove example.ts * port * move the examples outside packages and use the deps as workspaces in examples * update gitignore * fix example * update path of build dir * fix * fix scripts * try local path * fix * update commands * fix * fix * fix script * skip ts * disable ts * add vercel.json * install * update tsconfig * fix lint * remove console.log * lets see if this works * revert * remove ts nocheck * add types and some utils in nextjs example * fix types * updatw example and remove nextjs dynamic syntax so we don't import excal twice * move both examples to workspaces and create generic example to be used by browser and next js both * copy the static assets to nextjs * fix ts config * render custom menu items * fix custom footer * fix types in browser example * use regular imports for importing excal and import it using dynamic next js in app router instead * Add example for pages router * fix css discrepancies * fix css * configure output dir * fix * fix css * rename to with-nextjs * move components to examples/excalidraw/components --- .gitignore | 1 + .../excalidraw/components}/App.scss | 21 +- .../excalidraw/components}/App.tsx | 301 +++++++++-------- .../excalidraw/components}/CustomFooter.tsx | 27 +- .../excalidraw/components/MobileFooter.tsx | 27 ++ .../components}/sidebar/ExampleSidebar.scss | 0 .../components}/sidebar/ExampleSidebar.tsx | 5 +- .../excalidraw}/initialData.tsx | 4 +- examples/excalidraw/package.json | 13 + examples/excalidraw/tsconfig.json | 3 + examples/excalidraw/utils.ts | 146 ++++++++ examples/excalidraw/with-nextjs/.gitignore | 36 ++ examples/excalidraw/with-nextjs/README.md | 36 ++ .../excalidraw/with-nextjs/next.config.js | 12 + examples/excalidraw/with-nextjs/package.json | 25 ++ .../with-nextjs}/public/images/doremon.png | Bin .../with-nextjs}/public/images/excalibot.png | Bin .../with-nextjs}/public/images/pika.jpeg | Bin .../with-nextjs}/public/images/rocket.jpeg | Bin .../with-nextjs/src/app/favicon.ico | Bin 0 -> 25931 bytes .../excalidraw/with-nextjs/src/app/layout.tsx | 11 + .../excalidraw/with-nextjs/src/app/page.tsx | 23 ++ .../excalidraw/with-nextjs/src/common.scss | 15 + .../with-nextjs/src/excalidrawWrapper.tsx | 22 ++ .../src/pages/excalidraw-in-pages.tsx | 22 ++ examples/excalidraw/with-nextjs/tsconfig.json | 28 ++ examples/excalidraw/with-nextjs/vercel.json | 3 + examples/excalidraw/with-nextjs/yarn.lock | 252 ++++++++++++++ .../with-script-in-browser}/index.html | 10 +- .../with-script-in-browser/index.tsx | 28 ++ .../with-script-in-browser/package.json | 19 ++ .../public/images/doremon.png | Bin 0 -> 201946 bytes .../public/images/excalibot.png | Bin 0 -> 30330 bytes .../public/images/pika.jpeg | Bin 0 -> 6250 bytes .../public/images/rocket.jpeg | Bin 0 -> 40368 bytes .../with-script-in-browser}/vercel.json | 2 +- .../with-script-in-browser/vite.config.mts | 11 + examples/excalidraw/yarn.lock | 313 ++++++++++++++++++ package.json | 4 +- packages/excalidraw/.gitignore | 2 - packages/excalidraw/components/App.tsx | 5 +- packages/excalidraw/constants.ts | 1 - packages/excalidraw/example/MobileFooter.tsx | 20 -- packages/excalidraw/example/index.tsx | 17 - packages/excalidraw/index.tsx | 9 +- packages/excalidraw/renderer/renderScene.ts | 1 - packages/excalidraw/tsconfig.json | 2 +- packages/excalidraw/vite.config.mts | 15 - scripts/buildExample.mjs | 7 +- tsconfig.json | 2 +- yarn.lock | 159 ++++++++- 51 files changed, 1431 insertions(+), 229 deletions(-) rename {packages/excalidraw/example => examples/excalidraw/components}/App.scss (83%) rename {packages/excalidraw/example => examples/excalidraw/components}/App.tsx (83%) rename {packages/excalidraw/example => examples/excalidraw/components}/CustomFooter.tsx (79%) create mode 100644 examples/excalidraw/components/MobileFooter.tsx rename {packages/excalidraw/example => examples/excalidraw/components}/sidebar/ExampleSidebar.scss (100%) rename {packages/excalidraw/example => examples/excalidraw/components}/sidebar/ExampleSidebar.tsx (90%) rename {packages/excalidraw/example => examples/excalidraw}/initialData.tsx (99%) create mode 100644 examples/excalidraw/package.json create mode 100644 examples/excalidraw/tsconfig.json create mode 100644 examples/excalidraw/utils.ts create mode 100644 examples/excalidraw/with-nextjs/.gitignore create mode 100644 examples/excalidraw/with-nextjs/README.md create mode 100644 examples/excalidraw/with-nextjs/next.config.js create mode 100644 examples/excalidraw/with-nextjs/package.json rename {packages/excalidraw/example => examples/excalidraw/with-nextjs}/public/images/doremon.png (100%) rename {packages/excalidraw/example => examples/excalidraw/with-nextjs}/public/images/excalibot.png (100%) rename {packages/excalidraw/example => examples/excalidraw/with-nextjs}/public/images/pika.jpeg (100%) rename {packages/excalidraw/example => examples/excalidraw/with-nextjs}/public/images/rocket.jpeg (100%) create mode 100644 examples/excalidraw/with-nextjs/src/app/favicon.ico create mode 100644 examples/excalidraw/with-nextjs/src/app/layout.tsx create mode 100644 examples/excalidraw/with-nextjs/src/app/page.tsx create mode 100644 examples/excalidraw/with-nextjs/src/common.scss create mode 100644 examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx create mode 100644 examples/excalidraw/with-nextjs/src/pages/excalidraw-in-pages.tsx create mode 100644 examples/excalidraw/with-nextjs/tsconfig.json create mode 100644 examples/excalidraw/with-nextjs/vercel.json create mode 100644 examples/excalidraw/with-nextjs/yarn.lock rename {packages/excalidraw/example/public => examples/excalidraw/with-script-in-browser}/index.html (67%) create mode 100644 examples/excalidraw/with-script-in-browser/index.tsx create mode 100644 examples/excalidraw/with-script-in-browser/package.json create mode 100644 examples/excalidraw/with-script-in-browser/public/images/doremon.png create mode 100644 examples/excalidraw/with-script-in-browser/public/images/excalibot.png create mode 100644 examples/excalidraw/with-script-in-browser/public/images/pika.jpeg create mode 100644 examples/excalidraw/with-script-in-browser/public/images/rocket.jpeg rename {packages/excalidraw => examples/excalidraw/with-script-in-browser}/vercel.json (50%) create mode 100644 examples/excalidraw/with-script-in-browser/vite.config.mts create mode 100644 examples/excalidraw/yarn.lock delete mode 100644 packages/excalidraw/example/MobileFooter.tsx delete mode 100644 packages/excalidraw/example/index.tsx delete mode 100644 packages/excalidraw/vite.config.mts diff --git a/.gitignore b/.gitignore index 17e3e7dcf9..21d2730a2c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ packages/excalidraw/types coverage dev-dist html +examples/**/bundle.* \ No newline at end of file diff --git a/packages/excalidraw/example/App.scss b/examples/excalidraw/components/App.scss similarity index 83% rename from packages/excalidraw/example/App.scss rename to examples/excalidraw/components/App.scss index 7f37540d83..e41a77cccc 100644 --- a/packages/excalidraw/example/App.scss +++ b/examples/excalidraw/components/App.scss @@ -15,14 +15,23 @@ border-radius: 50%; } } + .app-title { + margin-block-start: 0.83em; + margin-block-end: 0.83em; + } } -.button-wrapper button { - z-index: 1; - height: 40px; - max-width: 200px; - margin: 10px; - padding: 5px; +.button-wrapper { + input[type="checkbox"] { + margin: 5px; + } + button { + z-index: 1; + height: 40px; + max-width: 200px; + margin: 10px; + padding: 5px; + } } .excalidraw .App-menu_top .buttonList { diff --git a/packages/excalidraw/example/App.tsx b/examples/excalidraw/components/App.tsx similarity index 83% rename from packages/excalidraw/example/App.tsx rename to examples/excalidraw/components/App.tsx index 50dc5b9a3b..eea0da6caf 100644 --- a/packages/excalidraw/example/App.tsx +++ b/examples/excalidraw/components/App.tsx @@ -1,15 +1,30 @@ +import React, { + useEffect, + useState, + useRef, + useCallback, + Children, + cloneElement, +} from "react"; import ExampleSidebar from "./sidebar/ExampleSidebar"; -import type * as TExcalidraw from "../index"; +import type * as TExcalidraw from "@excalidraw/excalidraw"; -import "./App.scss"; -import initialData from "./initialData"; import { nanoid } from "nanoid"; -import { resolvablePromise, ResolvablePromise } from "../utils"; -import { EVENT, ROUNDNESS } from "../constants"; -import { distance2d } from "../math"; -import { fileOpen } from "../data/filesystem"; -import { loadSceneOrLibraryFromBlob } from "../../utils"; + +import { + resolvablePromise, + ResolvablePromise, + distance2d, + fileOpen, + withBatchedUpdates, + withBatchedUpdatesThrottled, +} from "../utils"; + +import CustomFooter from "./CustomFooter"; +import MobileFooter from "./MobileFooter"; +import initialData from "../initialData"; + import type { AppState, BinaryFileData, @@ -18,19 +33,14 @@ import type { Gesture, LibraryItems, PointerDownState as ExcalidrawPointerDownState, -} from "../types"; -import type { NonDeletedExcalidrawElement, Theme } from "../element/types"; -import { ImportedLibraryData } from "../data/types"; -import CustomFooter from "./CustomFooter"; -import MobileFooter from "./MobileFooter"; -import { KEYS } from "../keys"; -import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils"; +} from "@excalidraw/excalidraw/dist/excalidraw/types"; +import type { + NonDeletedExcalidrawElement, + Theme, +} from "@excalidraw/excalidraw/dist/excalidraw/element/types"; +import type { ImportedLibraryData } from "@excalidraw/excalidraw/dist/excalidraw/data/types"; -declare global { - interface Window { - ExcalidrawLib: typeof TExcalidraw; - } -} +import "./App.scss"; type Comment = { x: number; @@ -51,31 +61,6 @@ type PointerDownState = { }; }; -const { useEffect, useState, useRef, useCallback } = window.React; - -// This is so that we use the bundled excalidraw.development.js file instead -// of the actual source code -const { - exportToCanvas, - exportToSvg, - exportToBlob, - exportToClipboard, - Excalidraw, - useHandleLibrary, - MIME_TYPES, - sceneCoordsToViewportCoords, - viewportCoordsToSceneCoords, - restoreElements, - Sidebar, - Footer, - WelcomeScreen, - MainMenu, - LiveCollaborationTrigger, - convertToExcalidrawElements, - TTDDialog, - TTDDialogTrigger, -} = window.ExcalidrawLib; - const COMMENT_ICON_DIMENSION = 32; const COMMENT_INPUT_HEIGHT = 50; const COMMENT_INPUT_WIDTH = 150; @@ -84,8 +69,38 @@ export interface AppProps { appTitle: string; useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void; customArgs?: any[]; + children: React.ReactNode; + excalidrawLib: typeof TExcalidraw; } -export default function App({ appTitle, useCustom, customArgs }: AppProps) { + +export default function App({ + appTitle, + useCustom, + customArgs, + children, + excalidrawLib, +}: AppProps) { + const { + exportToCanvas, + exportToSvg, + exportToBlob, + exportToClipboard, + useHandleLibrary, + MIME_TYPES, + sceneCoordsToViewportCoords, + viewportCoordsToSceneCoords, + restoreElements, + Sidebar, + Footer, + WelcomeScreen, + MainMenu, + LiveCollaborationTrigger, + convertToExcalidrawElements, + TTDDialog, + TTDDialogTrigger, + ROUNDNESS, + loadSceneOrLibraryFromBlob, + } = excalidrawLib; const appRef = useRef(null); const [viewModeEnabled, setViewModeEnabled] = useState(false); const [zenModeEnabled, setZenModeEnabled] = useState(false); @@ -147,8 +162,105 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { }; }; fetchData(); - }, [excalidrawAPI]); + }, [excalidrawAPI, convertToExcalidrawElements, MIME_TYPES]); + const renderExcalidraw = (children: React.ReactNode) => { + const Excalidraw: any = Children.toArray(children).find( + (child) => + React.isValidElement(child) && + typeof child.type !== "string" && + //@ts-ignore + child.type.displayName === "Excalidraw", + ); + if (!Excalidraw) { + return; + } + const newElement = cloneElement( + Excalidraw, + { + excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api), + initialData: initialStatePromiseRef.current.promise, + onChange: ( + elements: NonDeletedExcalidrawElement[], + state: AppState, + ) => { + console.info("Elements :", elements, "State : ", state); + }, + onPointerUpdate: (payload: { + pointer: { x: number; y: number }; + button: "down" | "up"; + pointersMap: Gesture["pointers"]; + }) => setPointerData(payload), + viewModeEnabled, + zenModeEnabled, + gridModeEnabled, + theme, + name: "Custom name of drawing", + UIOptions: { + canvasActions: { + loadScene: false, + }, + tools: { image: !disableImageTool }, + }, + renderTopRightUI, + onLinkOpen, + onPointerDown, + onScrollChange: rerenderCommentIcons, + validateEmbeddable: true, + }, + <> + {excalidrawAPI && ( +
+ +
+ )} + + + + + Tab one! + Tab two! + + One + Two + + + + + Toggle Custom Sidebar + + {renderMenu()} + {excalidrawAPI && ( + 😀}> + Text to diagram + + )} + { + console.info("submit"); + // sleep for 2s + await new Promise((resolve) => setTimeout(resolve, 2000)); + throw new Error("error, go away now"); + // return "dummy"; + }} + /> + , + ); + return newElement; + }; const renderTopRightUI = (isMobile: boolean) => { return ( <> @@ -332,8 +444,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { pointerDownState: PointerDownState, ) => { return withBatchedUpdates((event) => { - window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove); - window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp); + window.removeEventListener("pointermove", pointerDownState.onMove); + window.removeEventListener("pointerup", pointerDownState.onUp); excalidrawAPI?.setActiveTool({ type: "selection" }); const distance = distance2d( pointerDownState.x, @@ -397,8 +509,8 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { onPointerMoveFromPointerDownHandler(pointerDownState); const onPointerUp = onPointerUpFromPointerDownHandler(pointerDownState); - window.addEventListener(EVENT.POINTER_MOVE, onPointerMove); - window.addEventListener(EVENT.POINTER_UP, onPointerUp); + window.addEventListener("pointermove", onPointerMove); + window.addEventListener("pointerup", onPointerUp); pointerDownState.onMove = onPointerMove; pointerDownState.onUp = onPointerUp; @@ -490,7 +602,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { }} onBlur={saveComment} onKeyDown={(event) => { - if (!event.shiftKey && event.key === KEYS.ENTER) { + if (!event.shiftKey && event.key === "Enter") { event.preventDefault(); saveComment(); } @@ -523,7 +635,12 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { - {excalidrawAPI && } + {excalidrawAPI && ( + + )} ); }; @@ -672,83 +789,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
- - setExcalidrawAPI(api) - } - initialData={initialStatePromiseRef.current.promise} - onChange={(elements, state) => { - // console.info("Elements :", elements, "State : ", state); - }} - onPointerUpdate={(payload: { - pointer: { x: number; y: number }; - button: "down" | "up"; - pointersMap: Gesture["pointers"]; - }) => setPointerData(payload)} - viewModeEnabled={viewModeEnabled} - zenModeEnabled={zenModeEnabled} - gridModeEnabled={gridModeEnabled} - theme={theme} - name="Custom name of drawing" - UIOptions={{ - canvasActions: { - loadScene: false, - }, - tools: { image: !disableImageTool }, - }} - renderTopRightUI={renderTopRightUI} - onLinkOpen={onLinkOpen} - onPointerDown={onPointerDown} - onScrollChange={rerenderCommentIcons} - // allow all urls - validateEmbeddable={true} - > - {excalidrawAPI && ( -
- -
- )} - - - - - Tab one! - Tab two! - - One - Two - - - - - Toggle Custom Sidebar - - {renderMenu()} - {excalidrawAPI && ( - 😀}> - Text to diagram - - )} - { - console.info("submit"); - // sleep for 2s - await new Promise((resolve) => setTimeout(resolve, 2000)); - throw new Error("error, go away now"); - // return "dummy"; - }} - /> -
+ {renderExcalidraw(children)} {Object.keys(commentIcons || []).length > 0 && renderCommentIcons()} {comment && renderComment()}
diff --git a/packages/excalidraw/example/CustomFooter.tsx b/examples/excalidraw/components/CustomFooter.tsx similarity index 79% rename from packages/excalidraw/example/CustomFooter.tsx rename to examples/excalidraw/components/CustomFooter.tsx index c4ff5b6422..30d51ecf00 100644 --- a/packages/excalidraw/example/CustomFooter.tsx +++ b/examples/excalidraw/components/CustomFooter.tsx @@ -1,6 +1,6 @@ -import type { ExcalidrawImperativeAPI } from "../types"; +import type * as TExcalidraw from "@excalidraw/excalidraw"; +import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types"; -const { Button, MIME_TYPES } = window.ExcalidrawLib; const COMMENT_SVG = ( ); + const CustomFooter = ({ excalidrawAPI, + excalidrawLib, }: { excalidrawAPI: ExcalidrawImperativeAPI; + excalidrawLib: typeof TExcalidraw; }) => { + const { Button, MIME_TYPES } = excalidrawLib; + return ( <> - - + ); }; diff --git a/examples/excalidraw/components/MobileFooter.tsx b/examples/excalidraw/components/MobileFooter.tsx new file mode 100644 index 0000000000..7ab62b918d --- /dev/null +++ b/examples/excalidraw/components/MobileFooter.tsx @@ -0,0 +1,27 @@ +import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types"; +import CustomFooter from "./CustomFooter"; +import type * as TExcalidraw from "@excalidraw/excalidraw"; + +const MobileFooter = ({ + excalidrawAPI, + excalidrawLib, +}: { + excalidrawAPI: ExcalidrawImperativeAPI; + excalidrawLib: typeof TExcalidraw; +}) => { + const { useDevice, Footer } = excalidrawLib; + + const device = useDevice(); + if (device.editor.isMobile) { + return ( +
+ +
+ ); + } + return null; +}; +export default MobileFooter; diff --git a/packages/excalidraw/example/sidebar/ExampleSidebar.scss b/examples/excalidraw/components/sidebar/ExampleSidebar.scss similarity index 100% rename from packages/excalidraw/example/sidebar/ExampleSidebar.scss rename to examples/excalidraw/components/sidebar/ExampleSidebar.scss diff --git a/packages/excalidraw/example/sidebar/ExampleSidebar.tsx b/examples/excalidraw/components/sidebar/ExampleSidebar.tsx similarity index 90% rename from packages/excalidraw/example/sidebar/ExampleSidebar.tsx rename to examples/excalidraw/components/sidebar/ExampleSidebar.tsx index a6e1b64750..8b475f16fa 100644 --- a/packages/excalidraw/example/sidebar/ExampleSidebar.tsx +++ b/examples/excalidraw/components/sidebar/ExampleSidebar.tsx @@ -1,9 +1,8 @@ +import { useState } from "react"; import "./ExampleSidebar.scss"; -const React = window.React; - export default function Sidebar({ children }: { children: React.ReactNode }) { - const [open, setOpen] = React.useState(false); + const [open, setOpen] = useState(false); return ( <> diff --git a/packages/excalidraw/example/initialData.tsx b/examples/excalidraw/initialData.tsx similarity index 99% rename from packages/excalidraw/example/initialData.tsx rename to examples/excalidraw/initialData.tsx index 0299e49596..3cb5e7af4d 100644 --- a/packages/excalidraw/example/initialData.tsx +++ b/examples/excalidraw/initialData.tsx @@ -1,5 +1,5 @@ -import type { ExcalidrawElementSkeleton } from "../data/transform"; -import type { FileId } from "../element/types"; +import type { ExcalidrawElementSkeleton } from "@excalidraw/excalidraw/data/transform"; +import type { FileId } from "@excalidraw/excalidraw/element/types"; const elements: ExcalidrawElementSkeleton[] = [ { diff --git a/examples/excalidraw/package.json b/examples/excalidraw/package.json new file mode 100644 index 0000000000..fe48d55321 --- /dev/null +++ b/examples/excalidraw/package.json @@ -0,0 +1,13 @@ +{ + "name": "examples", + "version": "1.0.0", + "private": true, + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "@excalidraw/excalidraw": "*" + }, + "devDependencies": { + "typescript": "^5" + } +} diff --git a/examples/excalidraw/tsconfig.json b/examples/excalidraw/tsconfig.json new file mode 100644 index 0000000000..41716a7dd5 --- /dev/null +++ b/examples/excalidraw/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig" +} diff --git a/examples/excalidraw/utils.ts b/examples/excalidraw/utils.ts new file mode 100644 index 0000000000..822be29b78 --- /dev/null +++ b/examples/excalidraw/utils.ts @@ -0,0 +1,146 @@ +import { unstable_batchedUpdates } from "react-dom"; +import { fileOpen as _fileOpen } from "browser-fs-access"; +import type { MIME_TYPES } from "@excalidraw/excalidraw"; +import { AbortError } from "../../packages/excalidraw/errors"; + +type FILE_EXTENSION = Exclude; + +const INPUT_CHANGE_INTERVAL_MS = 500; + +export type ResolvablePromise = Promise & { + resolve: [T] extends [undefined] ? (value?: T) => void : (value: T) => void; + reject: (error: Error) => void; +}; +export const resolvablePromise = () => { + let resolve!: any; + let reject!: any; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + (promise as any).resolve = resolve; + (promise as any).reject = reject; + return promise as ResolvablePromise; +}; + +export const distance2d = (x1: number, y1: number, x2: number, y2: number) => { + const xd = x2 - x1; + const yd = y2 - y1; + return Math.hypot(xd, yd); +}; + +export const fileOpen = (opts: { + extensions?: FILE_EXTENSION[]; + description: string; + multiple?: M; +}): Promise => { + // an unsafe TS hack, alas not much we can do AFAIK + type RetType = M extends false | undefined ? File : File[]; + + const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => { + mimeTypes.push(MIME_TYPES[type]); + + return mimeTypes; + }, [] as string[]); + + const extensions = opts.extensions?.reduce((acc, ext) => { + if (ext === "jpg") { + return acc.concat(".jpg", ".jpeg"); + } + return acc.concat(`.${ext}`); + }, [] as string[]); + + return _fileOpen({ + description: opts.description, + extensions, + mimeTypes, + multiple: opts.multiple ?? false, + legacySetup: (resolve, reject, input) => { + const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS); + const focusHandler = () => { + checkForFile(); + document.addEventListener("keyup", scheduleRejection); + document.addEventListener("pointerup", scheduleRejection); + scheduleRejection(); + }; + const checkForFile = () => { + // this hack might not work when expecting multiple files + if (input.files?.length) { + const ret = opts.multiple ? [...input.files] : input.files[0]; + resolve(ret as RetType); + } + }; + requestAnimationFrame(() => { + window.addEventListener("focus", focusHandler); + }); + const interval = window.setInterval(() => { + checkForFile(); + }, INPUT_CHANGE_INTERVAL_MS); + return (rejectPromise) => { + clearInterval(interval); + scheduleRejection.cancel(); + window.removeEventListener("focus", focusHandler); + document.removeEventListener("keyup", scheduleRejection); + document.removeEventListener("pointerup", scheduleRejection); + if (rejectPromise) { + // so that something is shown in console if we need to debug this + console.warn("Opening the file was canceled (legacy-fs)."); + rejectPromise(new AbortError()); + } + }; + }, + }) as Promise; +}; + +export const debounce = ( + fn: (...args: T) => void, + timeout: number, +) => { + let handle = 0; + let lastArgs: T | null = null; + const ret = (...args: T) => { + lastArgs = args; + clearTimeout(handle); + handle = window.setTimeout(() => { + lastArgs = null; + fn(...args); + }, timeout); + }; + ret.flush = () => { + clearTimeout(handle); + if (lastArgs) { + const _lastArgs = lastArgs; + lastArgs = null; + fn(..._lastArgs); + } + }; + ret.cancel = () => { + lastArgs = null; + clearTimeout(handle); + }; + return ret; +}; + +export const withBatchedUpdates = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters["length"] extends 0 | 1 ? TFunction : never, +) => + ((event) => { + unstable_batchedUpdates(func as TFunction, event); + }) as TFunction; + +/** + * barches React state updates and throttles the calls to a single call per + * animation frame + */ +export const withBatchedUpdatesThrottled = < + TFunction extends ((event: any) => void) | (() => void), +>( + func: Parameters["length"] extends 0 | 1 ? TFunction : never, +) => { + // @ts-ignore + return throttleRAF>(((event) => { + unstable_batchedUpdates(func, event); + }) as TFunction); +}; diff --git a/examples/excalidraw/with-nextjs/.gitignore b/examples/excalidraw/with-nextjs/.gitignore new file mode 100644 index 0000000000..fd3dbb571a --- /dev/null +++ b/examples/excalidraw/with-nextjs/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/excalidraw/with-nextjs/README.md b/examples/excalidraw/with-nextjs/README.md new file mode 100644 index 0000000000..9e8d9b96d3 --- /dev/null +++ b/examples/excalidraw/with-nextjs/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3005) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/examples/excalidraw/with-nextjs/next.config.js b/examples/excalidraw/with-nextjs/next.config.js new file mode 100644 index 0000000000..701438ebfa --- /dev/null +++ b/examples/excalidraw/with-nextjs/next.config.js @@ -0,0 +1,12 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + distDir: "build", + typescript: { + // The ts config doesn't work with `jsx: preserve" and if updated to `react-jsx` it gets ovewritten by next js throwing ts errors hence I am ignoring build errors until this is fixed. + ignoreBuildErrors: true, + }, + // This is needed as in pages router the code for importing types throws error as its outside next js app + transpilePackages: ["../"], +}; + +module.exports = nextConfig; diff --git a/examples/excalidraw/with-nextjs/package.json b/examples/excalidraw/with-nextjs/package.json new file mode 100644 index 0000000000..1779524072 --- /dev/null +++ b/examples/excalidraw/with-nextjs/package.json @@ -0,0 +1,25 @@ +{ + "name": "with-nextjs", + "version": "0.1.0", + "private": true, + "scripts": { + "build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm", + "dev": "yarn build:workspace && next dev -p 3005", + "build": "yarn build:workspace && next build", + "start": "next start -p 3006", + "lint": "next lint" + }, + "dependencies": { + "@excalidraw/excalidraw": "*", + "next": "14.1", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "path2d-polyfill": "2.0.1", + "typescript": "^5" + } +} diff --git a/packages/excalidraw/example/public/images/doremon.png b/examples/excalidraw/with-nextjs/public/images/doremon.png similarity index 100% rename from packages/excalidraw/example/public/images/doremon.png rename to examples/excalidraw/with-nextjs/public/images/doremon.png diff --git a/packages/excalidraw/example/public/images/excalibot.png b/examples/excalidraw/with-nextjs/public/images/excalibot.png similarity index 100% rename from packages/excalidraw/example/public/images/excalibot.png rename to examples/excalidraw/with-nextjs/public/images/excalibot.png diff --git a/packages/excalidraw/example/public/images/pika.jpeg b/examples/excalidraw/with-nextjs/public/images/pika.jpeg similarity index 100% rename from packages/excalidraw/example/public/images/pika.jpeg rename to examples/excalidraw/with-nextjs/public/images/pika.jpeg diff --git a/packages/excalidraw/example/public/images/rocket.jpeg b/examples/excalidraw/with-nextjs/public/images/rocket.jpeg similarity index 100% rename from packages/excalidraw/example/public/images/rocket.jpeg rename to examples/excalidraw/with-nextjs/public/images/rocket.jpeg diff --git a/examples/excalidraw/with-nextjs/src/app/favicon.ico b/examples/excalidraw/with-nextjs/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/examples/excalidraw/with-nextjs/src/app/layout.tsx b/examples/excalidraw/with-nextjs/src/app/layout.tsx new file mode 100644 index 0000000000..225b6038d7 --- /dev/null +++ b/examples/excalidraw/with-nextjs/src/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/examples/excalidraw/with-nextjs/src/app/page.tsx b/examples/excalidraw/with-nextjs/src/app/page.tsx new file mode 100644 index 0000000000..bc8c98fcff --- /dev/null +++ b/examples/excalidraw/with-nextjs/src/app/page.tsx @@ -0,0 +1,23 @@ +import dynamic from "next/dynamic"; +import "../common.scss"; + +// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically +// with ssr false +const ExcalidrawWithClientOnly = dynamic( + async () => (await import("../excalidrawWrapper")).default, + { + ssr: false, + }, +); + +export default function Page() { + return ( + <> + Switch to Pages router +

App Router

+ + {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */} + + + ); +} diff --git a/examples/excalidraw/with-nextjs/src/common.scss b/examples/excalidraw/with-nextjs/src/common.scss new file mode 100644 index 0000000000..1a77600a92 --- /dev/null +++ b/examples/excalidraw/with-nextjs/src/common.scss @@ -0,0 +1,15 @@ +* { + box-sizing: border-box; + font-family: sans-serif; +} + +a { + color: #1c7ed6; + font-size: 20px; + text-decoration: none; + font-weight: 550; +} + +.page-title { + text-align: center; +} diff --git a/examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx b/examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx new file mode 100644 index 0000000000..40af9f0cce --- /dev/null +++ b/examples/excalidraw/with-nextjs/src/excalidrawWrapper.tsx @@ -0,0 +1,22 @@ +"use client"; +import * as excalidrawLib from "@excalidraw/excalidraw"; +import { Excalidraw } from "@excalidraw/excalidraw"; +import App from "../../components/App"; + +import "@excalidraw/excalidraw/index.css"; + +const ExcalidrawWrapper: React.FC = () => { + return ( + <> + {}} + excalidrawLib={excalidrawLib} + > + + + + ); +}; + +export default ExcalidrawWrapper; diff --git a/examples/excalidraw/with-nextjs/src/pages/excalidraw-in-pages.tsx b/examples/excalidraw/with-nextjs/src/pages/excalidraw-in-pages.tsx new file mode 100644 index 0000000000..527a346b94 --- /dev/null +++ b/examples/excalidraw/with-nextjs/src/pages/excalidraw-in-pages.tsx @@ -0,0 +1,22 @@ +import dynamic from "next/dynamic"; +import "../common.scss"; + +// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically +// with ssr false +const Excalidraw = dynamic( + async () => (await import("../excalidrawWrapper")).default, + { + ssr: false, + }, +); + +export default function Page() { + return ( + <> + Switch to App router +

Pages Router

+ {/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */} + + + ); +} diff --git a/examples/excalidraw/with-nextjs/tsconfig.json b/examples/excalidraw/with-nextjs/tsconfig.json new file mode 100644 index 0000000000..09ae73d2e0 --- /dev/null +++ b/examples/excalidraw/with-nextjs/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + }, + "forceConsistentCasingInFileNames": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "build/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/examples/excalidraw/with-nextjs/vercel.json b/examples/excalidraw/with-nextjs/vercel.json new file mode 100644 index 0000000000..bd885f4a5f --- /dev/null +++ b/examples/excalidraw/with-nextjs/vercel.json @@ -0,0 +1,3 @@ +{ + "outputDirectory": "build" +} diff --git a/examples/excalidraw/with-nextjs/yarn.lock b/examples/excalidraw/with-nextjs/yarn.lock new file mode 100644 index 0000000000..0072235c0f --- /dev/null +++ b/examples/excalidraw/with-nextjs/yarn.lock @@ -0,0 +1,252 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@excalidraw/excalidraw@workspace:^": + version "0.17.2" + resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.2.tgz#9a636a1e6bb3c88c5883347d3a7e75e9cce8ab96" + integrity sha512-7pqUWD8+mPjDhF4XxG3gw4rvE2JGaLW3Vss5UZfTbITPxAtFaGEc1K081bncitnaYhUwN9ENJE0i87QB3poDwQ== + +"@next/env@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.0.4.tgz#d5cda0c4a862d70ae760e58c0cd96a8899a2e49a" + integrity sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ== + +"@next/swc-darwin-arm64@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz#27b1854c2cd04eb1d5e75081a1a792ad91526618" + integrity sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg== + +"@next/swc-darwin-x64@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz#9940c449e757d0ee50bb9e792d2600cc08a3eb3b" + integrity sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw== + +"@next/swc-linux-arm64-gnu@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz#0eafd27c8587f68ace7b4fa80695711a8434de21" + integrity sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w== + +"@next/swc-linux-arm64-musl@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz#2b0072adb213f36dada5394ea67d6e82069ae7dd" + integrity sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ== + +"@next/swc-linux-x64-gnu@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz#68c67d20ebc8e3f6ced6ff23a4ba2a679dbcec32" + integrity sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A== + +"@next/swc-linux-x64-musl@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz#67cd81b42fb2caf313f7992fcf6d978af55a1247" + integrity sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw== + +"@next/swc-win32-arm64-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz#be06585906b195d755ceda28f33c633e1443f1a3" + integrity sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w== + +"@next/swc-win32-ia32-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz#e76cabefa9f2d891599c3d85928475bd8d3f6600" + integrity sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg== + +"@next/swc-win32-x64-msvc@14.0.4": + version "14.0.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz#e74892f1a9ccf41d3bf5979ad6d3d77c07b9cba1" + integrity sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A== + +"@swc/helpers@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" + integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== + dependencies: + tslib "^2.4.0" + +"@types/node@^20": + version "20.11.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.0.tgz#8e0b99e70c0c1ade1a86c4a282f7b7ef87c9552f" + integrity sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ== + dependencies: + undici-types "~5.26.4" + +"@types/prop-types@*": + version "15.7.11" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" + integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== + +"@types/react-dom@^18": + version "18.2.18" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" + integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18": + version "18.2.47" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.47.tgz#85074b27ab563df01fbc3f68dc64bf7050b0af40" + integrity sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.8" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" + integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== + +busboy@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +caniuse-lite@^1.0.30001406: + version "1.0.30001576" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz#893be772cf8ee6056d6c1e2d07df365b9ec0a5c4" + integrity sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg== + +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +graceful-fs@^4.1.2, graceful-fs@^4.2.11: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +nanoid@^3.3.6: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +next@14.0.4: + version "14.0.4" + resolved "https://registry.yarnpkg.com/next/-/next-14.0.4.tgz#bf00b6f835b20d10a5057838fa2dfced1d0d84dc" + integrity sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA== + dependencies: + "@next/env" "14.0.4" + "@swc/helpers" "0.5.2" + busboy "1.6.0" + caniuse-lite "^1.0.30001406" + graceful-fs "^4.2.11" + postcss "8.4.31" + styled-jsx "5.1.1" + watchpack "2.4.0" + optionalDependencies: + "@next/swc-darwin-arm64" "14.0.4" + "@next/swc-darwin-x64" "14.0.4" + "@next/swc-linux-arm64-gnu" "14.0.4" + "@next/swc-linux-arm64-musl" "14.0.4" + "@next/swc-linux-x64-gnu" "14.0.4" + "@next/swc-linux-x64-musl" "14.0.4" + "@next/swc-win32-arm64-msvc" "14.0.4" + "@next/swc-win32-ia32-msvc" "14.0.4" + "@next/swc-win32-x64-msvc" "14.0.4" + +path2d-polyfill@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391" + integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +postcss@8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +react-dom@^18: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react@^18: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +styled-jsx@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" + integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== + dependencies: + client-only "0.0.1" + +tslib@^2.4.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +typescript@^5: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +watchpack@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" diff --git a/packages/excalidraw/example/public/index.html b/examples/excalidraw/with-script-in-browser/index.html similarity index 67% rename from packages/excalidraw/example/public/index.html rename to examples/excalidraw/with-script-in-browser/index.html index 0fbf45e9e3..a56d7f4216 100644 --- a/packages/excalidraw/example/public/index.html +++ b/examples/excalidraw/with-script-in-browser/index.html @@ -13,20 +13,20 @@ window.name = "codesandbox"; -
- - + - + diff --git a/examples/excalidraw/with-script-in-browser/index.tsx b/examples/excalidraw/with-script-in-browser/index.tsx new file mode 100644 index 0000000000..e8584d7ca7 --- /dev/null +++ b/examples/excalidraw/with-script-in-browser/index.tsx @@ -0,0 +1,28 @@ +import App from "../components/App"; +import React, { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +import type * as TExcalidraw from "@excalidraw/excalidraw"; + +import "@excalidraw/excalidraw/index.css"; + +declare global { + interface Window { + ExcalidrawLib: typeof TExcalidraw; + } +} + +const rootElement = document.getElementById("root")!; +const root = createRoot(rootElement); +const { Excalidraw } = window.ExcalidrawLib; +root.render( + + {}} + excalidrawLib={window.ExcalidrawLib} + > + + + , +); diff --git a/examples/excalidraw/with-script-in-browser/package.json b/examples/excalidraw/with-script-in-browser/package.json new file mode 100644 index 0000000000..490b0f7961 --- /dev/null +++ b/examples/excalidraw/with-script-in-browser/package.json @@ -0,0 +1,19 @@ +{ + "name": "with-script-in-browser", + "version": "1.0.0", + "private": true, + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "@excalidraw/excalidraw": "*" + }, + "devDependencies": { + "vite": "5.0.6", + "typescript": "^5" + }, + "scripts": { + "start": "yarn workspace @excalidraw/excalidraw run build:esm && vite", + "build": "yarn workspace @excalidraw/excalidraw run build:esm && vite build", + "build:preview": "yarn build && vite preview --port 5002" + } +} diff --git a/examples/excalidraw/with-script-in-browser/public/images/doremon.png b/examples/excalidraw/with-script-in-browser/public/images/doremon.png new file mode 100644 index 0000000000000000000000000000000000000000..36208a4665fb89e44247292e92a1143a52cdf38c GIT binary patch literal 201946 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

>rqP?iYp>X2@Ky7x^}8x-)S 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

O1!W$4;+)P9SasnI-`V8^ZFleat5n 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­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 zk308F

hW@ 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 N3HSdn;j;Xm_;1tNja&c# literal 0 HcmV?d00001 diff --git a/packages/excalidraw/vercel.json b/examples/excalidraw/with-script-in-browser/vercel.json similarity index 50% rename from packages/excalidraw/vercel.json rename to examples/excalidraw/with-script-in-browser/vercel.json index a262682b89..139f31ef02 100644 --- a/packages/excalidraw/vercel.json +++ b/examples/excalidraw/with-script-in-browser/vercel.json @@ -1,4 +1,4 @@ { - "outputDirectory": "example/public", + "outputDirectory": "dist", "installCommand": "yarn install" } diff --git a/examples/excalidraw/with-script-in-browser/vite.config.mts b/examples/excalidraw/with-script-in-browser/vite.config.mts new file mode 100644 index 0000000000..e2e5e19ac0 --- /dev/null +++ b/examples/excalidraw/with-script-in-browser/vite.config.mts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + server: { + port: 3001, + // open the browser + open: true, + }, + publicDir: "public", +}); diff --git a/examples/excalidraw/yarn.lock b/examples/excalidraw/yarn.lock new file mode 100644 index 0000000000..1eb5842051 --- /dev/null +++ b/examples/excalidraw/yarn.lock @@ -0,0 +1,313 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@esbuild/aix-ppc64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz#2acd20be6d4f0458bc8c784103495ff24f13b1d3" + integrity sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g== + +"@esbuild/android-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz#b45d000017385c9051a4f03e17078abb935be220" + integrity sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q== + +"@esbuild/android-arm@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.11.tgz#f46f55414e1c3614ac682b29977792131238164c" + integrity sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw== + +"@esbuild/android-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.11.tgz#bfc01e91740b82011ef503c48f548950824922b2" + integrity sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg== + +"@esbuild/darwin-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz#533fb7f5a08c37121d82c66198263dcc1bed29bf" + integrity sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ== + +"@esbuild/darwin-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz#62f3819eff7e4ddc656b7c6815a31cf9a1e7d98e" + integrity sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g== + +"@esbuild/freebsd-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz#d478b4195aa3ca44160272dab85ef8baf4175b4a" + integrity sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA== + +"@esbuild/freebsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz#7bdcc1917409178257ca6a1a27fe06e797ec18a2" + integrity sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw== + +"@esbuild/linux-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz#58ad4ff11685fcc735d7ff4ca759ab18fcfe4545" + integrity sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg== + +"@esbuild/linux-arm@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz#ce82246d873b5534d34de1e5c1b33026f35e60e3" + integrity sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q== + +"@esbuild/linux-ia32@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz#cbae1f313209affc74b80f4390c4c35c6ab83fa4" + integrity sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA== + +"@esbuild/linux-loong64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz#5f32aead1c3ec8f4cccdb7ed08b166224d4e9121" + integrity sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg== + +"@esbuild/linux-mips64el@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz#38eecf1cbb8c36a616261de858b3c10d03419af9" + integrity sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg== + +"@esbuild/linux-ppc64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz#9c5725a94e6ec15b93195e5a6afb821628afd912" + integrity sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA== + +"@esbuild/linux-riscv64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz#2dc4486d474a2a62bbe5870522a9a600e2acb916" + integrity sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ== + +"@esbuild/linux-s390x@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz#4ad8567df48f7dd4c71ec5b1753b6f37561a65a8" + integrity sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q== + +"@esbuild/linux-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz#b7390c4d5184f203ebe7ddaedf073df82a658766" + integrity sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA== + +"@esbuild/netbsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz#d633c09492a1721377f3bccedb2d821b911e813d" + integrity sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ== + +"@esbuild/openbsd-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz#17388c76e2f01125bf831a68c03a7ffccb65d1a2" + integrity sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw== + +"@esbuild/sunos-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz#e320636f00bb9f4fdf3a80e548cb743370d41767" + integrity sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ== + +"@esbuild/win32-arm64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz#c778b45a496e90b6fc373e2a2bb072f1441fe0ee" + integrity sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ== + +"@esbuild/win32-ia32@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz#481a65fee2e5cce74ec44823e6b09ecedcc5194c" + integrity sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg== + +"@esbuild/win32-x64@0.19.11": + version "0.19.11" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz#a5d300008960bb39677c46bf16f53ec70d8dee04" + integrity sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw== + +"@rollup/rollup-android-arm-eabi@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz#b752b6c88a14ccfcbdf3f48c577ccc3a7f0e66b9" + integrity sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA== + +"@rollup/rollup-android-arm64@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz#33757c3a448b9ef77b6f6292d8b0ec45c87e9c1a" + integrity sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg== + +"@rollup/rollup-darwin-arm64@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz#5234ba62665a3f443143bc8bcea9df2cc58f55fb" + integrity sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w== + +"@rollup/rollup-darwin-x64@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz#981256c054d3247b83313724938d606798a919d1" + integrity sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA== + +"@rollup/rollup-linux-arm-gnueabihf@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz#120678a5a2b3a283a548dbb4d337f9187a793560" + integrity sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g== + +"@rollup/rollup-linux-arm64-gnu@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz#c99d857e2372ece544b6f60b85058ad259f64114" + integrity sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA== + +"@rollup/rollup-linux-arm64-musl@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz#3064060f568a5718c2a06858cd6e6d24f2ff8632" + integrity sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ== + +"@rollup/rollup-linux-riscv64-gnu@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz#987d30b5d2b992fff07d055015991a57ff55fbad" + integrity sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA== + +"@rollup/rollup-linux-x64-gnu@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz#85946ee4d068bd12197aeeec2c6f679c94978a49" + integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA== + +"@rollup/rollup-linux-x64-musl@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz#fe0b20f9749a60eb1df43d20effa96c756ddcbd4" + integrity sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg== + +"@rollup/rollup-win32-arm64-msvc@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz#422661ef0e16699a234465d15b2c1089ef963b2a" + integrity sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ== + +"@rollup/rollup-win32-ia32-msvc@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz#7b73a145891c202fbcc08759248983667a035d85" + integrity sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA== + +"@rollup/rollup-win32-x64-msvc@4.9.5": + version "4.9.5" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz#10491ccf4f63c814d4149e0316541476ea603602" + integrity sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ== + +"@types/estree@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +esbuild@^0.19.3: + version "0.19.11" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.11.tgz#4a02dca031e768b5556606e1b468fe72e3325d96" + integrity sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.19.11" + "@esbuild/android-arm" "0.19.11" + "@esbuild/android-arm64" "0.19.11" + "@esbuild/android-x64" "0.19.11" + "@esbuild/darwin-arm64" "0.19.11" + "@esbuild/darwin-x64" "0.19.11" + "@esbuild/freebsd-arm64" "0.19.11" + "@esbuild/freebsd-x64" "0.19.11" + "@esbuild/linux-arm" "0.19.11" + "@esbuild/linux-arm64" "0.19.11" + "@esbuild/linux-ia32" "0.19.11" + "@esbuild/linux-loong64" "0.19.11" + "@esbuild/linux-mips64el" "0.19.11" + "@esbuild/linux-ppc64" "0.19.11" + "@esbuild/linux-riscv64" "0.19.11" + "@esbuild/linux-s390x" "0.19.11" + "@esbuild/linux-x64" "0.19.11" + "@esbuild/netbsd-x64" "0.19.11" + "@esbuild/openbsd-x64" "0.19.11" + "@esbuild/sunos-x64" "0.19.11" + "@esbuild/win32-arm64" "0.19.11" + "@esbuild/win32-ia32" "0.19.11" + "@esbuild/win32-x64" "0.19.11" + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +postcss@^8.4.32: + version "8.4.33" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" + integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +react-dom@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react@18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +rollup@^4.2.0: + version "4.9.5" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.5.tgz#62999462c90f4c8b5d7c38fc7161e63b29101b05" + integrity sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.9.5" + "@rollup/rollup-android-arm64" "4.9.5" + "@rollup/rollup-darwin-arm64" "4.9.5" + "@rollup/rollup-darwin-x64" "4.9.5" + "@rollup/rollup-linux-arm-gnueabihf" "4.9.5" + "@rollup/rollup-linux-arm64-gnu" "4.9.5" + "@rollup/rollup-linux-arm64-musl" "4.9.5" + "@rollup/rollup-linux-riscv64-gnu" "4.9.5" + "@rollup/rollup-linux-x64-gnu" "4.9.5" + "@rollup/rollup-linux-x64-musl" "4.9.5" + "@rollup/rollup-win32-arm64-msvc" "4.9.5" + "@rollup/rollup-win32-ia32-msvc" "4.9.5" + "@rollup/rollup-win32-x64-msvc" "4.9.5" + fsevents "~2.3.2" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +vite@5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.6.tgz#f9e13503a4c5ccd67312c67803dec921f3bdea7c" + integrity sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ== + dependencies: + esbuild "^0.19.3" + postcss "^8.4.32" + rollup "^4.2.0" + optionalDependencies: + fsevents "~2.3.3" diff --git a/package.json b/package.json index 3154e69f20..a440c97b74 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "workspaces": [ "excalidraw-app", "packages/excalidraw", - "packages/utils" + "packages/utils", + "examples/excalidraw", + "examples/excalidraw/*" ], "dependencies": { "@excalidraw/random-username": "1.0.0", diff --git a/packages/excalidraw/.gitignore b/packages/excalidraw/.gitignore index f714ecd1d6..971fcb7d34 100644 --- a/packages/excalidraw/.gitignore +++ b/packages/excalidraw/.gitignore @@ -1,4 +1,2 @@ node_modules types -bundle.js -bundle.css diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 9e3ff5dac2..1618cd2aed 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -5780,7 +5780,10 @@ class App extends React.Component { event.preventDefault(); let nextPastePrevented = false; - const isLinux = /Linux/.test(window.navigator.platform); + const isLinux = + typeof window === undefined + ? false + : /Linux/.test(window.navigator.platform); setCursor(this.interactiveCanvas, CURSOR_TYPE.GRABBING); let { clientX: lastX, clientY: lastY } = event; diff --git a/packages/excalidraw/constants.ts b/packages/excalidraw/constants.ts index 72286e6986..c4df447975 100644 --- a/packages/excalidraw/constants.ts +++ b/packages/excalidraw/constants.ts @@ -2,7 +2,6 @@ import cssVariables from "./css/variables.module.scss"; import { AppProps } from "./types"; import { ExcalidrawElement, FontFamilyValues } from "./element/types"; import { COLOR_PALETTE } from "./colors"; - export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform); export const isWindows = /^Win/.test(navigator.platform); export const isAndroid = /\b(android)\b/i.test(navigator.userAgent); diff --git a/packages/excalidraw/example/MobileFooter.tsx b/packages/excalidraw/example/MobileFooter.tsx deleted file mode 100644 index e7e0f8d69d..0000000000 --- a/packages/excalidraw/example/MobileFooter.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { ExcalidrawImperativeAPI } from "../types"; -import CustomFooter from "./CustomFooter"; -const { useDevice, Footer } = window.ExcalidrawLib; - -const MobileFooter = ({ - excalidrawAPI, -}: { - excalidrawAPI: ExcalidrawImperativeAPI; -}) => { - const device = useDevice(); - if (device.editor.isMobile) { - return ( -

- ); - } - return null; -}; -export default MobileFooter; diff --git a/packages/excalidraw/example/index.tsx b/packages/excalidraw/example/index.tsx deleted file mode 100644 index fcc781289c..0000000000 --- a/packages/excalidraw/example/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import App from "./App"; - -const { StrictMode } = window.React; -//@ts-ignore -const { createRoot } = window.ReactDOM; - -const rootElement = document.getElementById("root")!; -const root = createRoot(rootElement); - -root.render( - - {}} - /> - , -); diff --git a/packages/excalidraw/index.tsx b/packages/excalidraw/index.tsx index 6524873a20..b450846936 100644 --- a/packages/excalidraw/index.tsx +++ b/packages/excalidraw/index.tsx @@ -80,6 +80,13 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { } useEffect(() => { + const importPolyfill = async () => { + //@ts-ignore + await import("canvas-roundrect-polyfill"); + }; + + importPolyfill(); + // Block pinch-zooming on iOS outside of the content area const handleTouchMove = (event: TouchEvent) => { // @ts-ignore @@ -223,7 +230,7 @@ export { } from "../utils/export"; export { isLinearElement } from "./element/typeChecks"; -export { FONT_FAMILY, THEME, MIME_TYPES } from "./constants"; +export { FONT_FAMILY, THEME, MIME_TYPES, ROUNDNESS } from "./constants"; export { mutateElement, diff --git a/packages/excalidraw/renderer/renderScene.ts b/packages/excalidraw/renderer/renderScene.ts index 0a066bfd44..6c358591b6 100644 --- a/packages/excalidraw/renderer/renderScene.ts +++ b/packages/excalidraw/renderer/renderScene.ts @@ -82,7 +82,6 @@ import { getTargetFrame, isElementInFrame, } from "../frame"; -import "canvas-roundrect-polyfill"; export const DEFAULT_SPACING = 2; diff --git a/packages/excalidraw/tsconfig.json b/packages/excalidraw/tsconfig.json index 28e276c356..4d7d4b3c15 100644 --- a/packages/excalidraw/tsconfig.json +++ b/packages/excalidraw/tsconfig.json @@ -1,5 +1,5 @@ { - "exclude": ["**/*.test.*", "tests", "types", "example", "dist"], + "exclude": ["**/*.test.*", "tests", "types", "examples", "dist"], "compilerOptions": { "target": "ESNext", "strict": true, diff --git a/packages/excalidraw/vite.config.mts b/packages/excalidraw/vite.config.mts deleted file mode 100644 index 9639966b2f..0000000000 --- a/packages/excalidraw/vite.config.mts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig, loadEnv } from "vite"; -import react from "@vitejs/plugin-react"; - -// To load .env.local variables -const envVars = loadEnv("", `../../`); -// https://vitejs.dev/config/ -export default defineConfig({ - root: "example/public", - server: { - port: 3001, - // open the browser - open: true, - }, - publicDir: "public", -}); diff --git a/scripts/buildExample.mjs b/scripts/buildExample.mjs index cfcbe84200..5cc50c6c63 100644 --- a/scripts/buildExample.mjs +++ b/scripts/buildExample.mjs @@ -4,8 +4,9 @@ import { execSync } from "child_process"; const createDevBuild = async () => { return await esbuild.build({ - entryPoints: ["example/index.tsx"], - outfile: "example/public/bundle.js", + entryPoints: ["../../examples/excalidraw/with-script-in-browser/index.tsx"], + outfile: + "../../examples/excalidraw/with-script-in-browser/public/bundle.js", define: { "import.meta.env": "{}", }, @@ -26,7 +27,7 @@ const startServer = async (ctx) => { }); }; execSync( - `rm -rf example/public/dist && yarn build:esm && cp -r dist example/public`, + `rm -rf ../../examples/excalidraw/with-script-in-browser/public/dist && yarn build:esm && cp -r dist ../../examples/excalidraw/with-script-in-browser/public`, ); const ctx = await createDevBuild(); diff --git a/tsconfig.json b/tsconfig.json index 10ac4b9a8e..585fa4cdb6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,5 @@ "jsx": "react-jsx" }, "include": ["packages", "excalidraw-app"], - "exclude": ["packages/excalidraw/types"] + "exclude": ["packages/excalidraw/types", "examples"] } diff --git a/yarn.lock b/yarn.lock index f857f7fb43..83b861b95e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2649,6 +2649,56 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@next/env@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.0.tgz#43d92ebb53bc0ae43dcc64fb4d418f8f17d7a341" + integrity sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw== + +"@next/swc-darwin-arm64@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz#70a57c87ab1ae5aa963a3ba0f4e59e18f4ecea39" + integrity sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ== + +"@next/swc-darwin-x64@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz#0863a22feae1540e83c249384b539069fef054e9" + integrity sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g== + +"@next/swc-linux-arm64-gnu@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz#893da533d3fce4aec7116fe772d4f9b95232423c" + integrity sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ== + +"@next/swc-linux-arm64-musl@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz#d81ddcf95916310b8b0e4ad32b637406564244c0" + integrity sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g== + +"@next/swc-linux-x64-gnu@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz#18967f100ec19938354332dcb0268393cbacf581" + integrity sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ== + +"@next/swc-linux-x64-musl@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz#77077cd4ba8dda8f349dc7ceb6230e68ee3293cf" + integrity sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg== + +"@next/swc-win32-arm64-msvc@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz#5f0b8cf955644104621e6d7cc923cad3a4c5365a" + integrity sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ== + +"@next/swc-win32-ia32-msvc@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz#21f4de1293ac5e5a168a412b139db5d3420a89d0" + integrity sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw== + +"@next/swc-win32-x64-msvc@14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz#e561fb330466d41807123d932b365cf3d33ceba2" + integrity sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg== + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -3290,6 +3340,13 @@ "@svgr/hast-util-to-babel-ast" "^6.5.1" svg-parser "^2.0.4" +"@swc/helpers@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" + integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== + dependencies: + tslib "^2.4.0" + "@testing-library/dom@^8.0.0": version "8.20.1" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f" @@ -3487,6 +3544,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^20": + version "20.11.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.1.tgz#6a93f94abeda166f688d3d2aca18012afbe5f850" + integrity sha512-DsXojJUES2M+FE8CpptJTKpg+r54moV9ZEncPstni1WHFmTcCzeFLnMFfyhCVS8XNOy/OQG+8lVxRLRrVHmV5A== + dependencies: + undici-types "~5.26.4" + "@types/pako@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.3.tgz#2e61c2b02020b5f44e2e5e946dfac74f4ec33c58" @@ -3521,6 +3585,13 @@ dependencies: "@types/react" "^17" +"@types/react-dom@^18": + version "18.2.18" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd" + integrity sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@18.0.15": version "18.0.15" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.15.tgz#d355644c26832dc27f3e6cbf0c4f4603fc4ab7fe" @@ -3539,6 +3610,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^18": + version "18.2.48" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.48.tgz#11df5664642d0bd879c1f58bc1d37205b064e8f1" + integrity sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/resize-observer-browser@0.1.7": version "0.1.7" resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz#294aaadf24ac6580b8fbd1fe3ab7b59fe85f9ef3" @@ -4669,6 +4749,13 @@ builtin-modules@^3.1.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== +busboy@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + bytes-iec@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/bytes-iec/-/bytes-iec-3.1.1.tgz#94cd36bf95c2c22a82002c247df8772d1d591083" @@ -4707,6 +4794,11 @@ caniuse-lite@^1.0.30001449: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001478.tgz#0ef8a1cf8b16be47a0f9fc4ecfc952232724b32a" integrity sha512-gMhDyXGItTHipJj2ApIvR+iVB5hd0KP3svMWWXDvZOmjzJJassGLMfxRkQCSYgGd2gtdL/ReeiyvMSFD1Ss6Mw== +caniuse-lite@^1.0.30001579: + version "1.0.30001579" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz#45c065216110f46d6274311a4b3fcf6278e0852a" + integrity sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA== + canvas-roundrect-polyfill@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/canvas-roundrect-polyfill/-/canvas-roundrect-polyfill-0.0.1.tgz#70bf107ebe2037f26d839d7f809a26f4a95f5696" @@ -4849,6 +4941,11 @@ cli-truncate@^3.1.0: slice-ansi "^5.0.0" string-width "^5.0.0" +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -6617,7 +6714,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -8143,6 +8240,29 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +next@14.1: + version "14.1.0" + resolved "https://registry.yarnpkg.com/next/-/next-14.1.0.tgz#b31c0261ff9caa6b4a17c5af019ed77387174b69" + integrity sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q== + dependencies: + "@next/env" "14.1.0" + "@swc/helpers" "0.5.2" + busboy "1.6.0" + caniuse-lite "^1.0.30001579" + graceful-fs "^4.2.11" + postcss "8.4.31" + styled-jsx "5.1.1" + optionalDependencies: + "@next/swc-darwin-arm64" "14.1.0" + "@next/swc-darwin-x64" "14.1.0" + "@next/swc-linux-arm64-gnu" "14.1.0" + "@next/swc-linux-arm64-musl" "14.1.0" + "@next/swc-linux-x64-gnu" "14.1.0" + "@next/swc-linux-x64-musl" "14.1.0" + "@next/swc-win32-arm64-msvc" "14.1.0" + "@next/swc-win32-ia32-msvc" "14.1.0" + "@next/swc-win32-x64-msvc" "14.1.0" + node-fetch@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" @@ -8407,6 +8527,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path2d-polyfill@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz#24c554a738f42700d6961992bf5f1049672f2391" + integrity sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA== + pathe@^1.1.0, pathe@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" @@ -8571,6 +8696,15 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== +postcss@8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + postcss@^8.4.32, postcss@^8.4.7: version "8.4.32" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9" @@ -8751,7 +8885,7 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -react-dom@18.2.0: +react-dom@18.2.0, react-dom@^18: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -8807,7 +8941,7 @@ react-style-singleton@^2.2.1: invariant "^2.2.4" tslib "^2.0.0" -react@18.2.0: +react@18.2.0, react@^18: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== @@ -9441,6 +9575,11 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + string-argv@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" @@ -9591,6 +9730,13 @@ style-loader@3.3.3: resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.3.tgz#bba8daac19930169c0c9c96706749a597ae3acff" integrity sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw== +styled-jsx@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" + integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== + dependencies: + client-only "0.0.1" + stylis@^4.1.3: version "4.3.0" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.0.tgz#abe305a669fc3d8777e10eefcfc73ad861c5588c" @@ -9857,7 +10003,7 @@ tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0: +tslib@^2.0.0, tslib@^2.4.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -9922,6 +10068,11 @@ typescript@4.9.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +typescript@^5: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + typeson-registry@^1.0.0-alpha.20: version "1.0.0-alpha.39" resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.39.tgz#9e0f5aabd5eebfcffd65a796487541196f4b1211" From 966f9aead912615a8ea518911b2b51688012fcea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 19:28:11 +0530 Subject: [PATCH 14/31] build(deps-dev): bump vite from 5.0.6 to 5.0.12 in /examples/excalidraw/with-script-in-browser (#7603) build(deps-dev): bump vite Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.6 to 5.0.12. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.0.12/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.0.12/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/excalidraw/with-script-in-browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/excalidraw/with-script-in-browser/package.json b/examples/excalidraw/with-script-in-browser/package.json index 490b0f7961..d721ac162d 100644 --- a/examples/excalidraw/with-script-in-browser/package.json +++ b/examples/excalidraw/with-script-in-browser/package.json @@ -8,7 +8,7 @@ "@excalidraw/excalidraw": "*" }, "devDependencies": { - "vite": "5.0.6", + "vite": "5.0.12", "typescript": "^5" }, "scripts": { From 678bb2b8192975c935cf313f9cf9a7837dd03d37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 19:29:50 +0530 Subject: [PATCH 15/31] build(deps-dev): bump vite from 5.0.6 to 5.0.12 (#7586) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.6 to 5.0.12. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.0.12/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.0.12/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 148 ++------------------------------------------------- 2 files changed, 6 insertions(+), 144 deletions(-) diff --git a/package.json b/package.json index a440c97b74..350f1469fb 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "prettier": "2.6.2", "rewire": "6.0.0", "typescript": "4.9.4", - "vite": "5.0.6", + "vite": "5.0.12", "vite-plugin-checker": "0.6.1", "vite-plugin-ejs": "1.7.0", "vite-plugin-pwa": "0.17.4", diff --git a/yarn.lock b/yarn.lock index 83b861b95e..61def89e77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2000,221 +2000,111 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.10.tgz#ef31015416dd79398082409b77aaaa2ade4d531a" integrity sha512-1X4CClKhDgC3by7k8aOWZeBXQX8dHT5QAMCAQDArCLaYfkppoARvh0fit3X2Qs+MXDngKcHv6XXyQCpY0hkK1Q== -"@esbuild/android-arm64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz#fb7130103835b6d43ea499c3f30cfb2b2ed58456" - integrity sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA== - "@esbuild/android-arm@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.10.tgz#1c23c7e75473aae9fb323be5d9db225142f47f52" integrity sha512-7W0bK7qfkw1fc2viBfrtAEkDKHatYfHzr/jKAHNr9BvkYDXPcC6bodtm8AyLJNNuqClLNaeTLuwURt4PRT9d7w== -"@esbuild/android-arm@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.8.tgz#b46e4d9e984e6d6db6c4224d72c86b7757e35bcb" - integrity sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA== - "@esbuild/android-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.10.tgz#df6a4e6d6eb8da5595cfce16d4e3f6bc24464707" integrity sha512-O/nO/g+/7NlitUxETkUv/IvADKuZXyH4BHf/g/7laqKC4i/7whLpB0gvpPc2zpF0q9Q6FXS3TS75QHac9MvVWw== -"@esbuild/android-x64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.8.tgz#a13db9441b5a4f4e4fec4a6f8ffacfea07888db7" - integrity sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A== - "@esbuild/darwin-arm64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.10.tgz#8462a55db07c1b2fad61c8244ce04469ef1043be" integrity sha512-YSRRs2zOpwypck+6GL3wGXx2gNP7DXzetmo5pHXLrY/VIMsS59yKfjPizQ4lLt5vEI80M41gjm2BxrGZ5U+VMA== -"@esbuild/darwin-arm64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz#49f5718d36541f40dd62bfdf84da9c65168a0fc2" - integrity sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw== - "@esbuild/darwin-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.10.tgz#d1de20bfd41bb75b955ba86a6b1004539e8218c1" integrity sha512-alfGtT+IEICKtNE54hbvPg13xGBe4GkVxyGWtzr+yHO7HIiRJppPDhOKq3zstTcVf8msXb/t4eavW3jCDpMSmA== -"@esbuild/darwin-x64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz#75c5c88371eea4bfc1f9ecfd0e75104c74a481ac" - integrity sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q== - "@esbuild/freebsd-arm64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.10.tgz#16904879e34c53a2e039d1284695d2db3e664d57" integrity sha512-dMtk1wc7FSH8CCkE854GyGuNKCewlh+7heYP/sclpOG6Cectzk14qdUIY5CrKDbkA/OczXq9WesqnPl09mj5dg== -"@esbuild/freebsd-arm64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz#9d7259fea4fd2b5f7437b52b542816e89d7c8575" - integrity sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw== - "@esbuild/freebsd-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.10.tgz#8ad9e5ca9786ca3f1ef1411bfd10b08dcd9d4cef" integrity sha512-G5UPPspryHu1T3uX8WiOEUa6q6OlQh6gNl4CO4Iw5PS+Kg5bVggVFehzXBJY6X6RSOMS8iXDv2330VzaObm4Ag== -"@esbuild/freebsd-x64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz#abac03e1c4c7c75ee8add6d76ec592f46dbb39e3" - integrity sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg== - "@esbuild/linux-arm64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.10.tgz#d82cf2c590faece82d28bbf1cfbe36f22ae25bd2" integrity sha512-QxaouHWZ+2KWEj7cGJmvTIHVALfhpGxo3WLmlYfJ+dA5fJB6lDEIg+oe/0//FuyVHuS3l79/wyBxbHr0NgtxJQ== -"@esbuild/linux-arm64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz#c577932cf4feeaa43cb9cec27b89cbe0df7d9098" - integrity sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ== - "@esbuild/linux-arm@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.10.tgz#477b8e7c7bcd34369717b04dd9ee6972c84f4029" integrity sha512-j6gUW5aAaPgD416Hk9FHxn27On28H4eVI9rJ4az7oCGTFW48+LcgNDBN+9f8rKZz7EEowo889CPKyeaD0iw9Kg== -"@esbuild/linux-arm@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz#d6014d8b98b5cbc96b95dad3d14d75bb364fdc0f" - integrity sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ== - "@esbuild/linux-ia32@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.10.tgz#d55ff822cf5b0252a57112f86857ff23be6cab0e" integrity sha512-4ub1YwXxYjj9h1UIZs2hYbnTZBtenPw5NfXCRgEkGb0b6OJ2gpkMvDqRDYIDRjRdWSe/TBiZltm3Y3Q8SN1xNg== -"@esbuild/linux-ia32@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz#2379a0554307d19ac4a6cdc15b08f0ea28e7a40d" - integrity sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ== - "@esbuild/linux-loong64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.10.tgz#a9ad057d7e48d6c9f62ff50f6f208e331c4543c7" integrity sha512-lo3I9k+mbEKoxtoIbM0yC/MZ1i2wM0cIeOejlVdZ3D86LAcFXFRdeuZmh91QJvUTW51bOK5W2BznGNIl4+mDaA== -"@esbuild/linux-loong64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz#e2a5bbffe15748b49356a6cd7b2d5bf60c5a7123" - integrity sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ== - "@esbuild/linux-mips64el@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.10.tgz#b011a96924773d60ebab396fbd7a08de66668179" integrity sha512-J4gH3zhHNbdZN0Bcr1QUGVNkHTdpijgx5VMxeetSk6ntdt+vR1DqGmHxQYHRmNb77tP6GVvD+K0NyO4xjd7y4A== -"@esbuild/linux-mips64el@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz#1359331e6f6214f26f4b08db9b9df661c57cfa24" - integrity sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q== - "@esbuild/linux-ppc64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.10.tgz#5d8b59929c029811e473f2544790ea11d588d4dd" integrity sha512-tgT/7u+QhV6ge8wFMzaklOY7KqiyitgT1AUHMApau32ZlvTB/+efeCtMk4eXS+uEymYK249JsoiklZN64xt6oQ== -"@esbuild/linux-ppc64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz#9ba436addc1646dc89dae48c62d3e951ffe70951" - integrity sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg== - "@esbuild/linux-riscv64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.10.tgz#292b06978375b271bd8bc0a554e0822957508d22" integrity sha512-0f/spw0PfBMZBNqtKe5FLzBDGo0SKZKvMl5PHYQr3+eiSscfJ96XEknCe+JoOayybWUFQbcJTrk946i3j9uYZA== -"@esbuild/linux-riscv64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz#fbcf0c3a0b20f40b5fc31c3b7695f0769f9de66b" - integrity sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg== - "@esbuild/linux-s390x@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.10.tgz#d30af63530f8d4fa96930374c9dd0d62bf59e069" integrity sha512-pZFe0OeskMHzHa9U38g+z8Yx5FNCLFtUnJtQMpwhS+r4S566aK2ci3t4NCP4tjt6d5j5uo4h7tExZMjeKoehAA== -"@esbuild/linux-s390x@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz#989e8a05f7792d139d5564ffa7ff898ac6f20a4a" - integrity sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg== - "@esbuild/linux-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.10.tgz#898c72eeb74d9f2fb43acf316125b475548b75ce" integrity sha512-SpYNEqg/6pZYoc+1zLCjVOYvxfZVZj6w0KROZ3Fje/QrM3nfvT2llI+wmKSrWuX6wmZeTapbarvuNNK/qepSgA== -"@esbuild/linux-x64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz#b187295393a59323397fe5ff51e769ec4e72212b" - integrity sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg== - "@esbuild/netbsd-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.10.tgz#fd473a5ae261b43eab6dad4dbd5a3155906e6c91" integrity sha512-ACbZ0vXy9zksNArWlk2c38NdKg25+L9pr/mVaj9SUq6lHZu/35nx2xnQVRGLrC1KKQqJKRIB0q8GspiHI3J80Q== -"@esbuild/netbsd-x64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz#c1ec0e24ea82313cb1c7bae176bd5acd5bde7137" - integrity sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw== - "@esbuild/openbsd-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.10.tgz#96eb8992e526717b5272321eaad3e21f3a608e46" integrity sha512-PxcgvjdSjtgPMiPQrM3pwSaG4kGphP+bLSb+cihuP0LYdZv1epbAIecHVl5sD3npkfYBZ0ZnOjR878I7MdJDFg== -"@esbuild/openbsd-x64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz#0c5b696ac66c6d70cf9ee17073a581a28af9e18d" - integrity sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ== - "@esbuild/sunos-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.10.tgz#c16ee1c167f903eaaa6acf7372bee42d5a89c9bc" integrity sha512-ZkIOtrRL8SEJjr+VHjmW0znkPs+oJXhlJbNwfI37rvgeMtk3sxOQevXPXjmAPZPigVTncvFqLMd+uV0IBSEzqA== -"@esbuild/sunos-x64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz#2a697e1f77926ff09fcc457d8f29916d6cd48fb1" - integrity sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w== - "@esbuild/win32-arm64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.10.tgz#7e417d1971dbc7e469b4eceb6a5d1d667b5e3dcc" integrity sha512-+Sa4oTDbpBfGpl3Hn3XiUe4f8TU2JF7aX8cOfqFYMMjXp6ma6NJDztl5FDG8Ezx0OjwGikIHw+iA54YLDNNVfw== -"@esbuild/win32-arm64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz#ec029e62a2fca8c071842ecb1bc5c2dd20b066f1" - integrity sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg== - "@esbuild/win32-ia32@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.10.tgz#2b52dfec6cd061ecb36171c13bae554888b439e5" integrity sha512-EOGVLK1oWMBXgfttJdPHDTiivYSjX6jDNaATeNOaCOFEVcfMjtbx7WVQwPSE1eIfCp/CaSF2nSrDtzc4I9f8TQ== -"@esbuild/win32-ia32@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz#cbb9a3146bde64dc15543e48afe418c7a3214851" - integrity sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw== - "@esbuild/win32-x64@0.19.10": version "0.19.10" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.10.tgz#bd123a74f243d2f3a1f046447bb9b363ee25d072" integrity sha512-whqLG6Sc70AbU73fFYvuYzaE4MNMBIlR1Y/IrUeOXFrWHxBEjjbZaQ3IXIQS8wJdAzue2GwYZCjOrgrU1oUHoA== -"@esbuild/win32-x64@0.19.8": - version "0.19.8" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz#c8285183dbdb17008578dbacb6e22748709b4822" - integrity sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA== - "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -5890,7 +5780,7 @@ esbuild-sass-plugin@2.16.0: resolve "^1.22.6" sass "^1.7.3" -esbuild@0.19.10: +esbuild@0.19.10, esbuild@^0.19.3: version "0.19.10" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.10.tgz#55e83e4a6b702e3498b9f872d84bfb4ebcb6d16e" integrity sha512-S1Y27QGt/snkNYrRcswgRFqZjaTG5a5xM3EQo97uNBnH505pdzSNe/HLBq1v0RO7iK/ngdbhJB6mDAp0OK+iUA== @@ -5919,34 +5809,6 @@ esbuild@0.19.10: "@esbuild/win32-ia32" "0.19.10" "@esbuild/win32-x64" "0.19.10" -esbuild@^0.19.3: - version "0.19.8" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.8.tgz#ad05b72281d84483fa6b5345bd246c27a207b8f1" - integrity sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w== - optionalDependencies: - "@esbuild/android-arm" "0.19.8" - "@esbuild/android-arm64" "0.19.8" - "@esbuild/android-x64" "0.19.8" - "@esbuild/darwin-arm64" "0.19.8" - "@esbuild/darwin-x64" "0.19.8" - "@esbuild/freebsd-arm64" "0.19.8" - "@esbuild/freebsd-x64" "0.19.8" - "@esbuild/linux-arm" "0.19.8" - "@esbuild/linux-arm64" "0.19.8" - "@esbuild/linux-ia32" "0.19.8" - "@esbuild/linux-loong64" "0.19.8" - "@esbuild/linux-mips64el" "0.19.8" - "@esbuild/linux-ppc64" "0.19.8" - "@esbuild/linux-riscv64" "0.19.8" - "@esbuild/linux-s390x" "0.19.8" - "@esbuild/linux-x64" "0.19.8" - "@esbuild/netbsd-x64" "0.19.8" - "@esbuild/openbsd-x64" "0.19.8" - "@esbuild/sunos-x64" "0.19.8" - "@esbuild/win32-arm64" "0.19.8" - "@esbuild/win32-ia32" "0.19.8" - "@esbuild/win32-x64" "0.19.8" - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -10334,10 +10196,10 @@ vite-plugin-svgr@2.4.0: "@rollup/pluginutils" "^5.0.2" "@svgr/core" "^6.5.1" -vite@5.0.6, "vite@^5.0.0-beta.15 || ^5.0.0", "vite@^5.0.0-beta.19 || ^5.0.0": - version "5.0.6" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.6.tgz#f9e13503a4c5ccd67312c67803dec921f3bdea7c" - integrity sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ== +vite@5.0.12, "vite@^5.0.0-beta.15 || ^5.0.0", "vite@^5.0.0-beta.19 || ^5.0.0": + version "5.0.12" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.12.tgz#8a2ffd4da36c132aec4adafe05d7adde38333c47" + integrity sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w== dependencies: esbuild "^0.19.3" postcss "^8.4.32" From 2789d08154a384ae98d9e9d9f70a01334d45ade9 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 25 Jan 2024 20:26:48 +0530 Subject: [PATCH 16/31] docs: update the docs for next js integration (#7605) * docs: update the docs for next js integration * update * update * update docs with tabbed examples * fix --- .../@excalidraw/excalidraw/integration.mdx | 101 ++++++++++++++---- 1 file changed, 79 insertions(+), 22 deletions(-) diff --git a/dev-docs/docs/@excalidraw/excalidraw/integration.mdx b/dev-docs/docs/@excalidraw/excalidraw/integration.mdx index 87eb3777db..d6bf3fd0d1 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/integration.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/integration.mdx @@ -32,15 +32,9 @@ function App() { ### Next.js -Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`. +Since Excalidraw doesn't support `server side rendering` so it should be rendered only on `client`. The way to achieve this in next.js is using `next.js dynamic import`. -Here are two ways on how you can render **Excalidraw** on **Next.js**. - - - -1. Using **Next.js Dynamic** import [Recommended]. - -Since Excalidraw doesn't support server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`. +If you want to only import `Excalidraw` component you can do :point_down: ```jsx showLineNumbers import dynamic from "next/dynamic"; @@ -55,25 +49,88 @@ export default function App() { } ``` -Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2). +However the above component only works for named component exports. If you want to import some util / constant or something else apart from Excalidraw, then this approach will not work. Instead you can write a wrapper over Excalidraw and import the wrapper dynamically. +If you are using `pages router` then importing the wrapper dynamically would work, where as if you are using `app router` then you will have to also add `useClient` directive on top of the file in addition to dynamically importing the wrapper as shown :point_down: -2. Importing Excalidraw once **client** is rendered. + + -```jsx showLineNumbers -import { useState, useEffect } from "react"; -export default function App() { - const [Excalidraw, setExcalidraw] = useState(null); - useEffect(() => { - import("@excalidraw/excalidraw").then((comp) => - setExcalidraw(comp.Excalidraw), + ```jsx showLineNumbers + "use client"; + import { Excalidraw. convertToExcalidrawElements } from "@excalidraw/excalidraw"; + + import "@excalidraw/excalidraw/index.css"; + + const ExcalidrawWrapper: React.FC = () => { + console.info(convertToExcalidrawElements([{ + type: "rectangle", + id: "rect-1", + width: 186.47265625, + height: 141.9765625, + },])); + return ( +
+
); - }, []); - return <>{Excalidraw && }; -} -``` + }; + export default ExcalidrawWrapper; + ``` + +
+ + + + ```jsx showLineNumbers + import dynamic from "next/dynamic"; + + // Since client components get prerenderd on server as well hence importing + // the excalidraw stuff dynamically with ssr false + + const ExcalidrawWrapper = dynamic( + async () => (await import("../excalidrawWrapper")).default, + { + ssr: false, + }, + ); + + export default function Page() { + return ( + + ); + } + ``` + + + + + ```jsx showLineNumbers + import dynamic from "next/dynamic"; + + // Since client components get prerenderd on server as well hence importing + // the excalidraw stuff dynamically with ssr false + + const ExcalidrawWrapper = dynamic( + async () => (await import("../excalidrawWrapper")).default, + { + ssr: false, + }, + ); + + export default function Page() { + return ( + + ); + } + ``` + + +
+ + +Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/excalidraw/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs-gh6smrdnq-excalidraw.vercel.app/). -Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-5xb3d) The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm) From 10bd08ef19a049abcb4a7da96fc69a052c9520ee Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Fri, 26 Jan 2024 11:29:07 +0530 Subject: [PATCH 17/31] fix: make getBoundTextElement and related helpers pure (#7601) * fix: make getBoundTextElement pure * updating args * fix * pass boundTextElement to getBoundTextMaxWidth * fix labelled arrows * lint * pass elementsMap to removeElementsFromFrame * pass elementsMap to getMaximumGroups, alignElements and distributeElements * lint * pass allElementsMap to renderElement * lint * feat: make more typesafe * fix: remove unnecessary assertion * fix: remove unused params --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/excalidraw/actions/actionAlign.tsx | 7 +- .../excalidraw/actions/actionBoundText.tsx | 8 +- .../excalidraw/actions/actionDistribute.tsx | 6 +- .../actions/actionDuplicateSelection.tsx | 2 +- packages/excalidraw/actions/actionFlip.ts | 5 +- packages/excalidraw/actions/actionGroup.tsx | 6 +- .../excalidraw/actions/actionProperties.tsx | 50 ++++++++--- packages/excalidraw/actions/actionStyles.ts | 9 +- packages/excalidraw/align.ts | 9 +- packages/excalidraw/components/Actions.tsx | 8 +- packages/excalidraw/components/App.tsx | 57 ++++++++++-- .../components/canvases/StaticCanvas.tsx | 7 +- packages/excalidraw/data/transform.ts | 5 +- packages/excalidraw/distribute.ts | 5 +- packages/excalidraw/element/binding.ts | 11 ++- packages/excalidraw/element/bounds.ts | 27 ++++-- packages/excalidraw/element/collision.ts | 10 ++- packages/excalidraw/element/dragElements.ts | 5 +- .../excalidraw/element/linearElementEditor.ts | 25 ++++-- packages/excalidraw/element/newElement.ts | 2 +- packages/excalidraw/element/resizeElements.ts | 15 ++-- .../excalidraw/element/textElement.test.ts | 6 +- packages/excalidraw/element/textElement.ts | 44 ++++------ packages/excalidraw/element/textWysiwyg.tsx | 10 ++- packages/excalidraw/element/types.ts | 10 +++ packages/excalidraw/frame.ts | 11 ++- packages/excalidraw/groups.ts | 5 +- packages/excalidraw/renderer/renderElement.ts | 28 ++++-- packages/excalidraw/renderer/renderScene.ts | 16 +++- packages/excalidraw/scene/Scene.ts | 9 +- packages/excalidraw/scene/export.ts | 12 +-- packages/excalidraw/scene/types.ts | 2 + packages/excalidraw/snapping.ts | 8 +- .../tests/linearElementEditor.test.tsx | 88 +++++++++++++++---- 34 files changed, 385 insertions(+), 143 deletions(-) diff --git a/packages/excalidraw/actions/actionAlign.tsx b/packages/excalidraw/actions/actionAlign.tsx index 137f68ae9f..8d7d362172 100644 --- a/packages/excalidraw/actions/actionAlign.tsx +++ b/packages/excalidraw/actions/actionAlign.tsx @@ -40,8 +40,13 @@ const alignSelectedElements = ( alignment: Alignment, ) => { const selectedElements = app.scene.getSelectedElements(appState); + const elementsMap = arrayToMap(elements); - const updatedElements = alignElements(selectedElements, alignment); + const updatedElements = alignElements( + selectedElements, + elementsMap, + alignment, + ); const updatedElementsMap = arrayToMap(updatedElements); diff --git a/packages/excalidraw/actions/actionBoundText.tsx b/packages/excalidraw/actions/actionBoundText.tsx index b421695446..05dd9c786f 100644 --- a/packages/excalidraw/actions/actionBoundText.tsx +++ b/packages/excalidraw/actions/actionBoundText.tsx @@ -45,8 +45,9 @@ export const actionUnbindText = register({ }, perform: (elements, appState, _, app) => { const selectedElements = app.scene.getSelectedElements(appState); + const elementsMap = app.scene.getNonDeletedElementsMap(); selectedElements.forEach((element) => { - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { const { width, height, baseline } = measureText( boundTextElement.originalText, @@ -106,7 +107,10 @@ export const actionBindText = register({ if ( textElement && bindingContainer && - getBoundTextElement(bindingContainer) === null + getBoundTextElement( + bindingContainer, + app.scene.getNonDeletedElementsMap(), + ) === null ) { return true; } diff --git a/packages/excalidraw/actions/actionDistribute.tsx b/packages/excalidraw/actions/actionDistribute.tsx index bf51bedf4b..be48bc8708 100644 --- a/packages/excalidraw/actions/actionDistribute.tsx +++ b/packages/excalidraw/actions/actionDistribute.tsx @@ -32,7 +32,11 @@ const distributeSelectedElements = ( ) => { const selectedElements = app.scene.getSelectedElements(appState); - const updatedElements = distributeElements(selectedElements, distribution); + const updatedElements = distributeElements( + selectedElements, + app.scene.getNonDeletedElementsMap(), + distribution, + ); const updatedElementsMap = arrayToMap(updatedElements); diff --git a/packages/excalidraw/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx index ba079168e9..7126f549ef 100644 --- a/packages/excalidraw/actions/actionDuplicateSelection.tsx +++ b/packages/excalidraw/actions/actionDuplicateSelection.tsx @@ -139,7 +139,7 @@ const duplicateElements = ( continue; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, arrayToMap(elements)); const isElementAFrameLike = isFrameLikeElement(element); if (idsOfElementsToDuplicate.get(element.id)) { diff --git a/packages/excalidraw/actions/actionFlip.ts b/packages/excalidraw/actions/actionFlip.ts index 81476e2411..c760af44d6 100644 --- a/packages/excalidraw/actions/actionFlip.ts +++ b/packages/excalidraw/actions/actionFlip.ts @@ -5,6 +5,7 @@ import { ExcalidrawElement, NonDeleted, NonDeletedElementsMap, + NonDeletedSceneElementsMap, } from "../element/types"; import { resizeMultipleElements } from "../element/resizeElements"; import { AppState } from "../types"; @@ -67,7 +68,7 @@ export const actionFlipVertical = register({ const flipSelectedElements = ( elements: readonly ExcalidrawElement[], - elementsMap: NonDeletedElementsMap, + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap, appState: Readonly, flipDirection: "horizontal" | "vertical", ) => { @@ -96,7 +97,7 @@ const flipSelectedElements = ( const flipElements = ( selectedElements: NonDeleted[], - elementsMap: NonDeletedElementsMap, + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap, appState: AppState, flipDirection: "horizontal" | "vertical", ): ExcalidrawElement[] => { diff --git a/packages/excalidraw/actions/actionGroup.tsx b/packages/excalidraw/actions/actionGroup.tsx index 42bd26efea..44523857ae 100644 --- a/packages/excalidraw/actions/actionGroup.tsx +++ b/packages/excalidraw/actions/actionGroup.tsx @@ -105,7 +105,10 @@ export const actionGroup = register({ const frameElementsMap = groupByFrameLikes(selectedElements); frameElementsMap.forEach((elementsInFrame, frameId) => { - removeElementsFromFrame(elementsInFrame); + removeElementsFromFrame( + elementsInFrame, + app.scene.getNonDeletedElementsMap(), + ); }); } @@ -225,6 +228,7 @@ export const actionUngroup = register({ nextElements, getElementsInResizingFrame(nextElements, frame, appState), frame, + app, ); } }); diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index c2a47802ff..79e50aa68e 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -606,7 +606,7 @@ export const actionChangeFontSize = register({ perform: (elements, appState, value, app) => { return changeFontSize(elements, appState, app, () => value, value); }, - PanelComponent: ({ elements, appState, updateData }) => ( + PanelComponent: ({ elements, appState, updateData, app }) => (
{t("labels.fontSize")} - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, (hasSelection) => hasSelection ? null @@ -738,7 +745,7 @@ export const actionChangeFontFamily = register({ commitToHistory: true, }; }, - PanelComponent: ({ elements, appState, updateData }) => { + PanelComponent: ({ elements, appState, updateData, app }) => { const options: { value: FontFamilyValues; text: string; @@ -778,14 +785,21 @@ export const actionChangeFontFamily = register({ if (isTextElement(element)) { return element.fontFamily; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); if (boundTextElement) { return boundTextElement.fontFamily; } return null; }, (element) => - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, (hasSelection) => hasSelection ? null @@ -830,7 +844,8 @@ export const actionChangeTextAlign = register({ commitToHistory: true, }; }, - PanelComponent: ({ elements, appState, updateData }) => { + PanelComponent: ({ elements, appState, updateData, app }) => { + const elementsMap = app.scene.getNonDeletedElementsMap(); return (
{t("labels.textAlign")} @@ -863,14 +878,18 @@ export const actionChangeTextAlign = register({ if (isTextElement(element)) { return element.textAlign; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + elementsMap, + ); if (boundTextElement) { return boundTextElement.textAlign; } return null; }, (element) => - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement(element, elementsMap) !== null, (hasSelection) => hasSelection ? null : appState.currentItemTextAlign, )} @@ -913,7 +932,7 @@ export const actionChangeVerticalAlign = register({ commitToHistory: true, }; }, - PanelComponent: ({ elements, appState, updateData }) => { + PanelComponent: ({ elements, appState, updateData, app }) => { return (
@@ -945,14 +964,21 @@ export const actionChangeVerticalAlign = register({ if (isTextElement(element) && element.containerId) { return element.verticalAlign; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); if (boundTextElement) { return boundTextElement.verticalAlign; } return null; }, (element) => - isTextElement(element) || getBoundTextElement(element) !== null, + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, (hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE), )} onChange={(value) => updateData(value)} diff --git a/packages/excalidraw/actions/actionStyles.ts b/packages/excalidraw/actions/actionStyles.ts index 9c6589bbc7..25a6baf2a5 100644 --- a/packages/excalidraw/actions/actionStyles.ts +++ b/packages/excalidraw/actions/actionStyles.ts @@ -32,12 +32,15 @@ export let copiedStyles: string = "{}"; export const actionCopyStyles = register({ name: "copyStyles", trackEvent: { category: "element" }, - perform: (elements, appState) => { + perform: (elements, appState, formData, app) => { const elementsCopied = []; const element = elements.find((el) => appState.selectedElementIds[el.id]); elementsCopied.push(element); if (element && hasBoundTextElement(element)) { - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); elementsCopied.push(boundTextElement); } if (element) { @@ -59,7 +62,7 @@ export const actionCopyStyles = register({ export const actionPasteStyles = register({ name: "pasteStyles", trackEvent: { category: "element" }, - perform: (elements, appState) => { + perform: (elements, appState, formData, app) => { const elementsCopied = JSON.parse(copiedStyles); const pastedElement = elementsCopied[0]; const boundTextElement = elementsCopied[1]; diff --git a/packages/excalidraw/align.ts b/packages/excalidraw/align.ts index 06382838f7..90ecabb117 100644 --- a/packages/excalidraw/align.ts +++ b/packages/excalidraw/align.ts @@ -1,4 +1,4 @@ -import { ExcalidrawElement } from "./element/types"; +import { ElementsMap, ExcalidrawElement } from "./element/types"; import { newElementWith } from "./element/mutateElement"; import { BoundingBox, getCommonBoundingBox } from "./element/bounds"; import { getMaximumGroups } from "./groups"; @@ -10,10 +10,13 @@ export interface Alignment { export const alignElements = ( selectedElements: ExcalidrawElement[], + elementsMap: ElementsMap, alignment: Alignment, ): ExcalidrawElement[] => { - const groups: ExcalidrawElement[][] = getMaximumGroups(selectedElements); - + const groups: ExcalidrawElement[][] = getMaximumGroups( + selectedElements, + elementsMap, + ); const selectionBoundingBox = getCommonBoundingBox(selectedElements); return groups.flatMap((group) => { diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index d67c8893d3..c11d64d041 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -1,6 +1,10 @@ import { useState } from "react"; import { ActionManager } from "../actions/manager"; -import { ExcalidrawElementType, NonDeletedElementsMap } from "../element/types"; +import { + ExcalidrawElementType, + NonDeletedElementsMap, + NonDeletedSceneElementsMap, +} from "../element/types"; import { t } from "../i18n"; import { useDevice } from "./App"; import { @@ -47,7 +51,7 @@ export const SelectedShapeActions = ({ renderAction, }: { appState: UIAppState; - elementsMap: NonDeletedElementsMap; + elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap; renderAction: ActionManager["renderAction"]; }) => { const targetElements = getTargetElements(elementsMap, appState); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 1618cd2aed..30f86c24ee 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -1431,6 +1431,8 @@ class App extends React.Component { pendingImageElementId: this.state.pendingImageElementId, }); + const allElementsMap = this.scene.getNonDeletedElementsMap(); + const shouldBlockPointerEvents = !( this.state.editingElement && isLinearElement(this.state.editingElement) @@ -1628,6 +1630,7 @@ class App extends React.Component { canvas={this.canvas} rc={this.rc} elementsMap={elementsMap} + allElementsMap={allElementsMap} visibleElements={visibleElements} versionNonce={versionNonce} selectionNonce={ @@ -3869,7 +3872,11 @@ class App extends React.Component { if (!isTextElement(selectedElement)) { container = selectedElement as ExcalidrawTextContainer; } - const midPoint = getContainerCenter(selectedElement, this.state); + const midPoint = getContainerCenter( + selectedElement, + this.state, + this.scene.getNonDeletedElementsMap(), + ); const sceneX = midPoint.x; const sceneY = midPoint.y; this.startTextEditing({ @@ -4333,6 +4340,7 @@ class App extends React.Component { this.frameNameBoundsCache, x, y, + this.scene.getNonDeletedElementsMap(), ) ? allHitElements[allHitElements.length - 2] : elementWithHighestZIndex; @@ -4362,7 +4370,14 @@ class App extends React.Component { ); return getElementsAtPosition(elements, (element) => - hitTest(element, this.state, this.frameNameBoundsCache, x, y), + hitTest( + element, + this.state, + this.frameNameBoundsCache, + x, + y, + this.scene.getNonDeletedElementsMap(), + ), ).filter((element) => { // hitting a frame's element from outside the frame is not considered a hit const containingFrame = getContainingFrame(element); @@ -4399,7 +4414,10 @@ class App extends React.Component { container, ); if (container && parentCenterPosition) { - const boundTextElementToContainer = getBoundTextElement(container); + const boundTextElementToContainer = getBoundTextElement( + container, + this.scene.getNonDeletedElementsMap(), + ); if (!boundTextElementToContainer) { shouldBindToContainer = true; } @@ -4412,7 +4430,10 @@ class App extends React.Component { if (isTextElement(selectedElements[0])) { existingTextElement = selectedElements[0]; } else if (container) { - existingTextElement = getBoundTextElement(selectedElements[0]); + existingTextElement = getBoundTextElement( + selectedElements[0], + this.scene.getNonDeletedElementsMap(), + ); } else { existingTextElement = this.getTextElementAtPosition(sceneX, sceneY); } @@ -4621,7 +4642,11 @@ class App extends React.Component { [sceneX, sceneY], ) ) { - const midPoint = getContainerCenter(container, this.state); + const midPoint = getContainerCenter( + container, + this.state, + this.scene.getNonDeletedElementsMap(), + ); sceneX = midPoint.x; sceneY = midPoint.y; @@ -5257,8 +5282,8 @@ class App extends React.Component { const element = LinearElementEditor.getElement( linearElementEditor.elementId, ); - - const boundTextElement = getBoundTextElement(element); + const elementsMap = this.scene.getNonDeletedElementsMap(); + const boundTextElement = getBoundTextElement(element, elementsMap); if (!element) { return; @@ -5285,6 +5310,7 @@ class App extends React.Component { linearElementEditor, { x: scenePointerX, y: scenePointerY }, this.state, + this.scene.getNonDeletedElementsMap(), ); if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) { @@ -5300,6 +5326,7 @@ class App extends React.Component { this.frameNameBoundsCache, scenePointerX, scenePointerY, + elementsMap, ) ) { setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); @@ -5311,6 +5338,7 @@ class App extends React.Component { this.frameNameBoundsCache, scenePointerX, scenePointerY, + this.scene.getNonDeletedElementsMap(), ) ) { setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); @@ -6060,6 +6088,7 @@ class App extends React.Component { this.history, pointerDownState.origin, linearElementEditor, + this.scene.getNonDeletedElementsMap(), ); if (ret.hitElement) { pointerDownState.hit.element = ret.hitElement; @@ -6995,6 +7024,7 @@ class App extends React.Component { ); }, linearElementEditor, + this.scene.getNonDeletedElementsMap(), ); if (didDrag) { pointerDownState.lastCoords.x = pointerCoords.x; @@ -7713,7 +7743,10 @@ class App extends React.Component { groupIds: [], }); - removeElementsFromFrame([linearElement]); + removeElementsFromFrame( + [linearElement], + this.scene.getNonDeletedElementsMap(), + ); this.scene.informMutation(); } @@ -7866,6 +7899,7 @@ class App extends React.Component { this.state, ), frame, + this, ); } @@ -8093,6 +8127,7 @@ class App extends React.Component { this.frameNameBoundsCache, pointerDownState.origin.x, pointerDownState.origin.y, + this.scene.getNonDeletedElementsMap(), )) || (!hitElement && pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements)) @@ -9334,7 +9369,11 @@ class App extends React.Component { let elementCenterX = container.x + container.width / 2; let elementCenterY = container.y + container.height / 2; - const elementCenter = getContainerCenter(container, appState); + const elementCenter = getContainerCenter( + container, + appState, + this.scene.getNonDeletedElementsMap(), + ); if (elementCenter) { elementCenterX = elementCenter.x; elementCenterY = elementCenter.y; diff --git a/packages/excalidraw/components/canvases/StaticCanvas.tsx b/packages/excalidraw/components/canvases/StaticCanvas.tsx index 3dc5b91751..bfdb669e6c 100644 --- a/packages/excalidraw/components/canvases/StaticCanvas.tsx +++ b/packages/excalidraw/components/canvases/StaticCanvas.tsx @@ -7,13 +7,17 @@ import type { RenderableElementsMap, StaticCanvasRenderConfig, } from "../../scene/types"; -import type { NonDeletedExcalidrawElement } from "../../element/types"; +import type { + NonDeletedExcalidrawElement, + NonDeletedSceneElementsMap, +} from "../../element/types"; import { isRenderThrottlingEnabled } from "../../reactUtils"; type StaticCanvasProps = { canvas: HTMLCanvasElement; rc: RoughCanvas; elementsMap: RenderableElementsMap; + allElementsMap: NonDeletedSceneElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; versionNonce: number | undefined; selectionNonce: number | undefined; @@ -67,6 +71,7 @@ const StaticCanvas = (props: StaticCanvasProps) => { rc: props.rc, scale: props.scale, elementsMap: props.elementsMap, + allElementsMap: props.allElementsMap, visibleElements: props.visibleElements, appState: props.appState, renderConfig: props.renderConfig, diff --git a/packages/excalidraw/data/transform.ts b/packages/excalidraw/data/transform.ts index 7b5286923c..8ce842300c 100644 --- a/packages/excalidraw/data/transform.ts +++ b/packages/excalidraw/data/transform.ts @@ -24,6 +24,7 @@ import { normalizeText, } from "../element/textElement"; import { + ElementsMap, ExcalidrawArrowElement, ExcalidrawBindableElement, ExcalidrawElement, @@ -42,7 +43,7 @@ import { VerticalAlign, } from "../element/types"; import { MarkOptional } from "../utility-types"; -import { assertNever, cloneJSON, getFontString } from "../utils"; +import { arrayToMap, assertNever, cloneJSON, getFontString } from "../utils"; import { getSizeFromPoints } from "../points"; import { randomId } from "../random"; @@ -202,6 +203,7 @@ const DEFAULT_DIMENSION = 100; const bindTextToContainer = ( container: ExcalidrawElement, textProps: { text: string } & MarkOptional, + elementsMap: ElementsMap, ) => { const textElement: ExcalidrawTextElement = newTextElement({ x: 0, @@ -623,6 +625,7 @@ export const convertToExcalidrawElements = ( let [container, text] = bindTextToContainer( excalidrawElement, element?.label, + arrayToMap(elementStore.getElements()), ); elementStore.add(container); elementStore.add(text); diff --git a/packages/excalidraw/distribute.ts b/packages/excalidraw/distribute.ts index acad09b2de..368b2f24da 100644 --- a/packages/excalidraw/distribute.ts +++ b/packages/excalidraw/distribute.ts @@ -1,7 +1,7 @@ -import { ExcalidrawElement } from "./element/types"; import { newElementWith } from "./element/mutateElement"; import { getMaximumGroups } from "./groups"; import { getCommonBoundingBox } from "./element/bounds"; +import type { ElementsMap, ExcalidrawElement } from "./element/types"; export interface Distribution { space: "between"; @@ -10,6 +10,7 @@ export interface Distribution { export const distributeElements = ( selectedElements: ExcalidrawElement[], + elementsMap: ElementsMap, distribution: Distribution, ): ExcalidrawElement[] => { const [start, mid, end, extent] = @@ -18,7 +19,7 @@ export const distributeElements = ( : (["minY", "midY", "maxY", "height"] as const); const bounds = getCommonBoundingBox(selectedElements); - const groups = getMaximumGroups(selectedElements) + const groups = getMaximumGroups(selectedElements, elementsMap) .map((group) => [group, getCommonBoundingBox(group)] as const) .sort((a, b) => a[1][mid] - b[1][mid]); diff --git a/packages/excalidraw/element/binding.ts b/packages/excalidraw/element/binding.ts index 3f6cf0022d..66d29f3f63 100644 --- a/packages/excalidraw/element/binding.ts +++ b/packages/excalidraw/element/binding.ts @@ -321,9 +321,9 @@ export const updateBoundElements = ( const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( simultaneouslyUpdated, ); - + const scene = Scene.getScene(changedElement)!; getNonDeletedElements( - Scene.getScene(changedElement)!, + scene, boundLinearElements.map((el) => el.id), ).forEach((element) => { if (!isLinearElement(element)) { @@ -362,9 +362,12 @@ export const updateBoundElements = ( endBinding, changedElement as ExcalidrawBindableElement, ); - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement( + element, + scene.getNonDeletedElementsMap(), + ); if (boundText) { - handleBindTextResize(element, false); + handleBindTextResize(element, scene.getNonDeletedElementsMap(), false); } }); }; diff --git a/packages/excalidraw/element/bounds.ts b/packages/excalidraw/element/bounds.ts index 673649e5f4..f892089f75 100644 --- a/packages/excalidraw/element/bounds.ts +++ b/packages/excalidraw/element/bounds.ts @@ -6,6 +6,7 @@ import { NonDeleted, ExcalidrawTextElementWithContainer, ElementsMapOrArray, + ElementsMap, } from "./types"; import { distance2d, rotate, rotatePoint } from "../math"; import rough from "roughjs/bin/rough"; @@ -74,13 +75,16 @@ export class ElementBounds { ) { return cachedBounds.bounds; } - - const bounds = ElementBounds.calculateBounds(element); + const scene = Scene.getScene(element); + const bounds = ElementBounds.calculateBounds( + element, + scene?.getNonDeletedElementsMap() || new Map(), + ); // hack to ensure that downstream checks could retrieve element Scene // so as to have correctly calculated bounds // FIXME remove when we get rid of all the id:Scene / element:Scene mapping - const shouldCache = Scene.getScene(element); + const shouldCache = !!scene; if (shouldCache) { ElementBounds.boundsCache.set(element, { @@ -92,7 +96,10 @@ export class ElementBounds { return bounds; } - private static calculateBounds(element: ExcalidrawElement): Bounds { + private static calculateBounds( + element: ExcalidrawElement, + elementsMap: ElementsMap, + ): Bounds { let bounds: Bounds; const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element); @@ -111,7 +118,7 @@ export class ElementBounds { maxY + element.y, ]; } else if (isLinearElement(element)) { - bounds = getLinearElementRotatedBounds(element, cx, cy); + bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap); } else if (element.type === "diamond") { const [x11, y11] = rotate(cx, y1, cx, cy, element.angle); const [x12, y12] = rotate(cx, y2, cx, cy, element.angle); @@ -154,16 +161,17 @@ export const getElementAbsoluteCoords = ( element: ExcalidrawElement, includeBoundText: boolean = false, ): [number, number, number, number, number, number] => { + const elementsMap = + Scene.getScene(element)?.getElementsMapIncludingDeleted() || new Map(); if (isFreeDrawElement(element)) { return getFreeDrawElementAbsoluteCoords(element); } else if (isLinearElement(element)) { return LinearElementEditor.getElementAbsoluteCoords( element, + elementsMap, includeBoundText, ); } else if (isTextElement(element)) { - const elementsMap = - Scene.getScene(element)?.getElementsMapIncludingDeleted(); const container = elementsMap ? getContainerElement(element, elementsMap) : null; @@ -677,7 +685,10 @@ const getLinearElementRotatedBounds = ( element: ExcalidrawLinearElement, cx: number, cy: number, + elementsMap: ElementsMap, ): Bounds => { + const boundTextElement = getBoundTextElement(element, elementsMap); + if (element.points.length < 2) { const [pointX, pointY] = element.points[0]; const [x, y] = rotate( @@ -689,7 +700,6 @@ const getLinearElementRotatedBounds = ( ); let coords: Bounds = [x, y, x, y]; - const boundTextElement = getBoundTextElement(element); if (boundTextElement) { const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( element, @@ -714,7 +724,6 @@ const getLinearElementRotatedBounds = ( rotate(element.x + x, element.y + y, cx, cy, element.angle); const res = getMinMaxXYFromCurvePathOps(ops, transformXY); let coords: Bounds = [res[0], res[1], res[2], res[3]]; - const boundTextElement = getBoundTextElement(element); if (boundTextElement) { const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( element, diff --git a/packages/excalidraw/element/collision.ts b/packages/excalidraw/element/collision.ts index 709781b229..b8c07e3ab0 100644 --- a/packages/excalidraw/element/collision.ts +++ b/packages/excalidraw/element/collision.ts @@ -28,6 +28,7 @@ import { StrokeRoundness, ExcalidrawFrameLikeElement, ExcalidrawIframeLikeElement, + ElementsMap, } from "./types"; import { @@ -78,6 +79,7 @@ export const hitTest = ( frameNameBoundsCache: FrameNameBoundsCache, x: number, y: number, + elementsMap: ElementsMap, ): boolean => { // How many pixels off the shape boundary we still consider a hit const threshold = 10 / appState.zoom.value; @@ -95,7 +97,7 @@ export const hitTest = ( ); } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { const isHittingBoundTextElement = hitTest( boundTextElement, @@ -103,6 +105,7 @@ export const hitTest = ( frameNameBoundsCache, x, y, + elementsMap, ); if (isHittingBoundTextElement) { return true; @@ -122,15 +125,16 @@ export const isHittingElementBoundingBoxWithoutHittingElement = ( frameNameBoundsCache: FrameNameBoundsCache, x: number, y: number, + elementsMap: ElementsMap, ): boolean => { const threshold = 10 / appState.zoom.value; // So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element // eg for linear elements text can be outside the element bounding box - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if ( boundTextElement && - hitTest(boundTextElement, appState, frameNameBoundsCache, x, y) + hitTest(boundTextElement, appState, frameNameBoundsCache, x, y, elementsMap) ) { return false; } diff --git a/packages/excalidraw/element/dragElements.ts b/packages/excalidraw/element/dragElements.ts index ecec4d0831..0144f55a40 100644 --- a/packages/excalidraw/element/dragElements.ts +++ b/packages/excalidraw/element/dragElements.ts @@ -57,7 +57,10 @@ export const dragSelectedElements = ( // skip arrow labels since we calculate its position during render !isArrowElement(element) ) { - const textElement = getBoundTextElement(element); + const textElement = getBoundTextElement( + element, + scene.getNonDeletedElementsMap(), + ); if (textElement) { updateElementCoords(pointerDownState, textElement, adjustedOffset); } diff --git a/packages/excalidraw/element/linearElementEditor.ts b/packages/excalidraw/element/linearElementEditor.ts index bf64ee7321..5c3c6acaa2 100644 --- a/packages/excalidraw/element/linearElementEditor.ts +++ b/packages/excalidraw/element/linearElementEditor.ts @@ -5,6 +5,7 @@ import { PointBinding, ExcalidrawBindableElement, ExcalidrawTextElementWithContainer, + ElementsMap, } from "./types"; import { distance2d, @@ -193,6 +194,7 @@ export class LinearElementEditor { pointSceneCoords: { x: number; y: number }[], ) => void, linearElementEditor: LinearElementEditor, + elementsMap: ElementsMap, ): boolean { if (!linearElementEditor) { return false; @@ -272,9 +274,9 @@ export class LinearElementEditor { ); } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { - handleBindTextResize(element, false); + handleBindTextResize(element, elementsMap, false); } // suggest bindings for first and last point if selected @@ -404,9 +406,10 @@ export class LinearElementEditor { static getEditorMidPoints = ( element: NonDeleted, + elementsMap: ElementsMap, appState: InteractiveCanvasAppState, ): typeof editorMidPointsCache["points"] => { - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement(element, elementsMap); // Since its not needed outside editor unless 2 pointer lines or bound text if ( @@ -465,6 +468,7 @@ export class LinearElementEditor { linearElementEditor: LinearElementEditor, scenePointer: { x: number; y: number }, appState: AppState, + elementsMap: ElementsMap, ) => { const { elementId } = linearElementEditor; const element = LinearElementEditor.getElement(elementId); @@ -503,7 +507,7 @@ export class LinearElementEditor { } let index = 0; const midPoints: typeof editorMidPointsCache["points"] = - LinearElementEditor.getEditorMidPoints(element, appState); + LinearElementEditor.getEditorMidPoints(element, elementsMap, appState); while (index < midPoints.length) { if (midPoints[index] !== null) { const distance = distance2d( @@ -581,6 +585,7 @@ export class LinearElementEditor { linearElementEditor: LinearElementEditor, appState: AppState, midPoint: Point, + elementsMap: ElementsMap, ) { const element = LinearElementEditor.getElement( linearElementEditor.elementId, @@ -588,7 +593,11 @@ export class LinearElementEditor { if (!element) { return -1; } - const midPoints = LinearElementEditor.getEditorMidPoints(element, appState); + const midPoints = LinearElementEditor.getEditorMidPoints( + element, + elementsMap, + appState, + ); let index = 0; while (index < midPoints.length) { if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) { @@ -605,6 +614,7 @@ export class LinearElementEditor { history: History, scenePointer: { x: number; y: number }, linearElementEditor: LinearElementEditor, + elementsMap: ElementsMap, ): { didAddPoint: boolean; hitElement: NonDeleted | null; @@ -630,6 +640,7 @@ export class LinearElementEditor { linearElementEditor, scenePointer, appState, + elementsMap, ); let segmentMidpointIndex = null; if (segmentMidpoint) { @@ -637,6 +648,7 @@ export class LinearElementEditor { linearElementEditor, appState, segmentMidpoint, + elementsMap, ); } if (event.altKey && appState.editingLinearElement) { @@ -1418,6 +1430,7 @@ export class LinearElementEditor { static getElementAbsoluteCoords = ( element: ExcalidrawLinearElement, + elementsMap: ElementsMap, includeBoundText: boolean = false, ): [number, number, number, number, number, number] => { let coords: [number, number, number, number, number, number]; @@ -1462,7 +1475,7 @@ export class LinearElementEditor { if (!includeBoundText) { return coords; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { coords = LinearElementEditor.getMinMaxXYWithBoundText( element, diff --git a/packages/excalidraw/element/newElement.ts b/packages/excalidraw/element/newElement.ts index 3158c064cf..447a07993f 100644 --- a/packages/excalidraw/element/newElement.ts +++ b/packages/excalidraw/element/newElement.ts @@ -342,7 +342,7 @@ export const refreshTextDimensions = ( text = wrapText( text, getFontString(textElement), - getBoundTextMaxWidth(container), + getBoundTextMaxWidth(container, textElement), ); } const dimensions = getAdjustedDimensions(textElement, text); diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/excalidraw/element/resizeElements.ts index 46b891aca5..deb5fead32 100644 --- a/packages/excalidraw/element/resizeElements.ts +++ b/packages/excalidraw/element/resizeElements.ts @@ -126,6 +126,7 @@ export const transformElements = ( rotateMultipleElements( originalElements, selectedElements, + elementsMap, pointerX, pointerY, shouldRotateWithDiscreteAngle, @@ -219,7 +220,7 @@ const measureFontSizeFromWidth = ( if (hasContainer) { const container = getContainerElement(element, elementsMap); if (container) { - width = getBoundTextMaxWidth(container); + width = getBoundTextMaxWidth(container, element); } } const nextFontSize = element.fontSize * (nextWidth / width); @@ -394,7 +395,7 @@ export const resizeSingleElement = ( let scaleY = atStartBoundsHeight / boundsCurrentHeight; let boundTextFont: { fontSize?: number; baseline?: number } = {}; - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (transformHandleDirection.includes("e")) { scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth; @@ -458,7 +459,7 @@ export const resizeSingleElement = ( const nextFont = measureFontSizeFromWidth( boundTextElement, elementsMap, - getBoundTextMaxWidth(updatedElement), + getBoundTextMaxWidth(updatedElement, boundTextElement), getBoundTextMaxHeight(updatedElement, boundTextElement), ); if (nextFont === null) { @@ -640,6 +641,7 @@ export const resizeSingleElement = ( } handleBindTextResize( element, + elementsMap, transformHandleDirection, shouldMaintainAspectRatio, ); @@ -882,7 +884,7 @@ export const resizeMultipleElements = ( newSize: { width, height }, }); - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement && boundTextFontSize) { mutateElement( boundTextElement, @@ -892,7 +894,7 @@ export const resizeMultipleElements = ( }, false, ); - handleBindTextResize(element, transformHandleType, true); + handleBindTextResize(element, elementsMap, transformHandleType, true); } } @@ -902,6 +904,7 @@ export const resizeMultipleElements = ( const rotateMultipleElements = ( originalElements: PointerDownState["originalElements"], elements: readonly NonDeletedExcalidrawElement[], + elementsMap: ElementsMap, pointerX: number, pointerY: number, shouldRotateWithDiscreteAngle: boolean, @@ -941,7 +944,7 @@ const rotateMultipleElements = ( ); updateBoundElements(element, { simultaneouslyUpdated: elements }); - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement(element, elementsMap); if (boundText && !isArrowElement(element)) { mutateElement( boundText, diff --git a/packages/excalidraw/element/textElement.test.ts b/packages/excalidraw/element/textElement.test.ts index b6221336d1..2f3a2dcc75 100644 --- a/packages/excalidraw/element/textElement.test.ts +++ b/packages/excalidraw/element/textElement.test.ts @@ -319,17 +319,17 @@ describe("Test measureText", () => { it("should return max width when container is rectangle", () => { const container = API.createElement({ type: "rectangle", ...params }); - expect(getBoundTextMaxWidth(container)).toBe(168); + expect(getBoundTextMaxWidth(container, null)).toBe(168); }); it("should return max width when container is ellipse", () => { const container = API.createElement({ type: "ellipse", ...params }); - expect(getBoundTextMaxWidth(container)).toBe(116); + expect(getBoundTextMaxWidth(container, null)).toBe(116); }); it("should return max width when container is diamond", () => { const container = API.createElement({ type: "diamond", ...params }); - expect(getBoundTextMaxWidth(container)).toBe(79); + expect(getBoundTextMaxWidth(container, null)).toBe(79); }); }); diff --git a/packages/excalidraw/element/textElement.ts b/packages/excalidraw/element/textElement.ts index da1348ec2d..b264c0d594 100644 --- a/packages/excalidraw/element/textElement.ts +++ b/packages/excalidraw/element/textElement.ts @@ -23,7 +23,6 @@ import { VERTICAL_ALIGN, } from "../constants"; import { MaybeTransformHandleType } from "./transformHandles"; -import Scene from "../scene/Scene"; import { isTextElement } from "."; import { isBoundToContainer, isArrowElement } from "./typeChecks"; import { LinearElementEditor } from "./linearElementEditor"; @@ -89,7 +88,7 @@ export const redrawTextBoundingBox = ( container, textElement as ExcalidrawTextElementWithContainer, ); - const maxContainerWidth = getBoundTextMaxWidth(container); + const maxContainerWidth = getBoundTextMaxWidth(container, textElement); if (!isArrowElement(container) && metrics.height > maxContainerHeight) { const nextHeight = computeContainerDimensionForBoundText( @@ -162,6 +161,7 @@ export const bindTextToShapeAfterDuplication = ( export const handleBindTextResize = ( container: NonDeletedExcalidrawElement, + elementsMap: ElementsMap, transformHandleType: MaybeTransformHandleType, shouldMaintainAspectRatio = false, ) => { @@ -170,25 +170,17 @@ export const handleBindTextResize = ( return; } resetOriginalContainerCache(container.id); - let textElement = Scene.getScene(container)!.getElement( - boundTextElementId, - ) as ExcalidrawTextElement; + const textElement = getBoundTextElement(container, elementsMap); if (textElement && textElement.text) { if (!container) { return; } - textElement = Scene.getScene(container)!.getElement( - boundTextElementId, - ) as ExcalidrawTextElement; let text = textElement.text; let nextHeight = textElement.height; let nextWidth = textElement.width; - const maxWidth = getBoundTextMaxWidth(container); - const maxHeight = getBoundTextMaxHeight( - container, - textElement as ExcalidrawTextElementWithContainer, - ); + const maxWidth = getBoundTextMaxWidth(container, textElement); + const maxHeight = getBoundTextMaxHeight(container, textElement); let containerHeight = container.height; let nextBaseLine = textElement.baseline; if ( @@ -243,10 +235,7 @@ export const handleBindTextResize = ( if (!isArrowElement(container)) { mutateElement( textElement, - computeBoundTextPosition( - container, - textElement as ExcalidrawTextElementWithContainer, - ), + computeBoundTextPosition(container, textElement), ); } } @@ -264,7 +253,7 @@ export const computeBoundTextPosition = ( } const containerCoords = getContainerCoords(container); const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement); - const maxContainerWidth = getBoundTextMaxWidth(container); + const maxContainerWidth = getBoundTextMaxWidth(container, boundTextElement); let x; let y; @@ -667,17 +656,18 @@ export const getBoundTextElementId = (container: ExcalidrawElement | null) => { : null; }; -export const getBoundTextElement = (element: ExcalidrawElement | null) => { +export const getBoundTextElement = ( + element: ExcalidrawElement | null, + elementsMap: ElementsMap, +) => { if (!element) { return null; } const boundTextElementId = getBoundTextElementId(element); + if (boundTextElementId) { - return ( - (Scene.getScene(element)?.getElement( - boundTextElementId, - ) as ExcalidrawTextElementWithContainer) || null - ); + return (elementsMap.get(boundTextElementId) || + null) as ExcalidrawTextElementWithContainer | null; } return null; }; @@ -699,6 +689,7 @@ export const getContainerElement = ( export const getContainerCenter = ( container: ExcalidrawElement, appState: AppState, + elementsMap: ElementsMap, ) => { if (!isArrowElement(container)) { return { @@ -718,6 +709,7 @@ export const getContainerCenter = ( const index = container.points.length / 2 - 1; let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints( container, + elementsMap, appState, )[index]; if (!midSegmentMidpoint) { @@ -877,9 +869,7 @@ export const computeContainerDimensionForBoundText = ( export const getBoundTextMaxWidth = ( container: ExcalidrawElement, - boundTextElement: ExcalidrawTextElement | null = getBoundTextElement( - container, - ), + boundTextElement: ExcalidrawTextElement | null, ) => { const { width } = container; if (isArrowElement(container)) { diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index 801f0c4405..d12d34f89a 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -34,6 +34,7 @@ import { computeContainerDimensionForBoundText, detectLineHeight, computeBoundTextPosition, + getBoundTextElement, } from "./textElement"; import { actionDecreaseFontSize, @@ -196,7 +197,8 @@ export const textWysiwyg = ({ } } - maxWidth = getBoundTextMaxWidth(container); + maxWidth = getBoundTextMaxWidth(container, updatedTextElement); + maxHeight = getBoundTextMaxHeight( container, updatedTextElement as ExcalidrawTextElementWithContainer, @@ -361,10 +363,14 @@ export const textWysiwyg = ({ fontFamily: app.state.currentItemFontFamily, }); if (container) { + const boundTextElement = getBoundTextElement( + container, + app.scene.getNonDeletedElementsMap(), + ); const wrappedText = wrapText( `${editable.value}${data}`, font, - getBoundTextMaxWidth(container), + getBoundTextMaxWidth(container, boundTextElement), ); const width = getTextWidth(wrappedText, font); editable.style.width = `${width}px`; diff --git a/packages/excalidraw/element/types.ts b/packages/excalidraw/element/types.ts index 7659ad1e90..f89e8d5f23 100644 --- a/packages/excalidraw/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -279,6 +279,16 @@ export type NonDeletedElementsMap = Map< export type SceneElementsMap = Map & MakeBrand<"SceneElementsMap">; +/** + * Map of all non-deleted Scene elements. + * Not a subset. Use this type when you need access to current Scene elements. + */ +export type NonDeletedSceneElementsMap = Map< + ExcalidrawElement["id"], + NonDeletedExcalidrawElement +> & + MakeBrand<"NonDeletedSceneElementsMap">; + export type ElementsMapOrArray = | readonly ExcalidrawElement[] | Readonly; diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index ecb70ef1e4..1457c4ecf7 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -444,6 +444,7 @@ export const addElementsToFrame = ( elementsToAdd: NonDeletedExcalidrawElement[], frame: ExcalidrawFrameLikeElement, ): T => { + const elementsMap = arrayToMap(allElements); const currTargetFrameChildrenMap = new Map(); for (const element of allElements.values()) { if (element.frameId === frame.id) { @@ -481,7 +482,7 @@ export const addElementsToFrame = ( finalElementsToAdd.push(element); } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if ( boundTextElement && !suppliedElementsToAddSet.has(boundTextElement.id) && @@ -506,6 +507,7 @@ export const addElementsToFrame = ( export const removeElementsFromFrame = ( elementsToRemove: ReadonlySetLike, + elementsMap: ElementsMap, ) => { const _elementsToRemove = new Map< ExcalidrawElement["id"], @@ -524,7 +526,7 @@ export const removeElementsFromFrame = ( const arr = toRemoveElementsByFrame.get(element.frameId) || []; arr.push(element); - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { _elementsToRemove.set(boundTextElement.id, boundTextElement); arr.push(boundTextElement); @@ -550,7 +552,7 @@ export const removeAllElementsFromFrame = ( frame: ExcalidrawFrameLikeElement, ) => { const elementsInFrame = getFrameChildren(allElements, frame.id); - removeElementsFromFrame(elementsInFrame); + removeElementsFromFrame(elementsInFrame, arrayToMap(allElements)); return allElements; }; @@ -558,6 +560,7 @@ export const replaceAllElementsInFrame = ( allElements: readonly T[], nextElementsInFrame: ExcalidrawElement[], frame: ExcalidrawFrameLikeElement, + app: AppClassProperties, ): T[] => { return addElementsToFrame( removeAllElementsFromFrame(allElements, frame), @@ -608,7 +611,7 @@ export const updateFrameMembershipOfSelectedElements = < }); if (elementsToRemove.size > 0) { - removeElementsFromFrame(elementsToRemove); + removeElementsFromFrame(elementsToRemove, elementsMap); } return allElements; }; diff --git a/packages/excalidraw/groups.ts b/packages/excalidraw/groups.ts index b0bedc4f95..f8c0eddb93 100644 --- a/packages/excalidraw/groups.ts +++ b/packages/excalidraw/groups.ts @@ -4,6 +4,7 @@ import { NonDeleted, NonDeletedExcalidrawElement, ElementsMapOrArray, + ElementsMap, } from "./element/types"; import { AppClassProperties, @@ -329,12 +330,12 @@ export const removeFromSelectedGroups = ( export const getMaximumGroups = ( elements: ExcalidrawElement[], + elementsMap: ElementsMap, ): ExcalidrawElement[][] => { const groups: Map = new Map< String, ExcalidrawElement[] >(); - elements.forEach((element: ExcalidrawElement) => { const groupId = element.groupIds.length === 0 @@ -344,7 +345,7 @@ export const getMaximumGroups = ( const currentGroupMembers = groups.get(groupId) || []; // Include bound text if present when grouping - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { currentGroupMembers.push(boundTextElement); } diff --git a/packages/excalidraw/renderer/renderElement.ts b/packages/excalidraw/renderer/renderElement.ts index 39e6c49749..5ab3f3ca52 100644 --- a/packages/excalidraw/renderer/renderElement.ts +++ b/packages/excalidraw/renderer/renderElement.ts @@ -6,6 +6,7 @@ import { ExcalidrawImageElement, ExcalidrawTextElementWithContainer, ExcalidrawFrameLikeElement, + NonDeletedSceneElementsMap, } from "../element/types"; import { isTextElement, @@ -190,6 +191,7 @@ const cappedElementCanvasSize = ( const generateElementCanvas = ( element: NonDeletedExcalidrawElement, + elementsMap: RenderableElementsMap, zoom: Zoom, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, @@ -247,7 +249,8 @@ const generateElementCanvas = ( zoomValue: zoom.value, canvasOffsetX, canvasOffsetY, - boundTextElementVersion: getBoundTextElement(element)?.version || null, + boundTextElementVersion: + getBoundTextElement(element, elementsMap)?.version || null, containingFrameOpacity: getContainingFrame(element)?.opacity || 100, }; }; @@ -407,6 +410,7 @@ export const elementWithCanvasCache = new WeakMap< const generateElementWithCanvas = ( element: NonDeletedExcalidrawElement, + elementsMap: RenderableElementsMap, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, ) => { @@ -416,7 +420,9 @@ const generateElementWithCanvas = ( prevElementWithCanvas && prevElementWithCanvas.zoomValue !== zoom.value && !appState?.shouldCacheIgnoreZoom; - const boundTextElementVersion = getBoundTextElement(element)?.version || null; + const boundTextElementVersion = + getBoundTextElement(element, elementsMap)?.version || null; + const containingFrameOpacity = getContainingFrame(element)?.opacity || 100; if ( @@ -428,6 +434,7 @@ const generateElementWithCanvas = ( ) { const elementWithCanvas = generateElementCanvas( element, + elementsMap, zoom, renderConfig, appState, @@ -445,6 +452,7 @@ const drawElementFromCanvas = ( context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, + allElementsMap: NonDeletedSceneElementsMap, ) => { const element = elementWithCanvas.element; const padding = getCanvasPadding(element); @@ -464,7 +472,8 @@ const drawElementFromCanvas = ( context.save(); context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio); - const boundTextElement = getBoundTextElement(element); + + const boundTextElement = getBoundTextElement(element, allElementsMap); if (isArrowElement(element) && boundTextElement) { const tempCanvas = document.createElement("canvas"); @@ -511,7 +520,6 @@ const drawElementFromCanvas = ( offsetY - padding * zoom; tempCanvasContext.translate(-shiftX, -shiftY); - // Clear the bound text area tempCanvasContext.clearRect( -(boundTextElement.width / 2 + BOUND_TEXT_PADDING) * @@ -573,6 +581,7 @@ const drawElementFromCanvas = ( ) { const textElement = getBoundTextElement( element, + allElementsMap, ) as ExcalidrawTextElementWithContainer; const coords = getContainerCoords(element); context.strokeStyle = "#c92a2a"; @@ -580,7 +589,7 @@ const drawElementFromCanvas = ( context.strokeRect( (coords.x + appState.scrollX) * window.devicePixelRatio, (coords.y + appState.scrollY) * window.devicePixelRatio, - getBoundTextMaxWidth(element) * window.devicePixelRatio, + getBoundTextMaxWidth(element, textElement) * window.devicePixelRatio, getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio, ); } @@ -616,6 +625,7 @@ export const renderSelectionElement = ( export const renderElement = ( element: NonDeletedExcalidrawElement, elementsMap: RenderableElementsMap, + allElementsMap: NonDeletedSceneElementsMap, rc: RoughCanvas, context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, @@ -687,6 +697,7 @@ export const renderElement = ( } else { const elementWithCanvas = generateElementWithCanvas( element, + elementsMap, renderConfig, appState, ); @@ -695,6 +706,7 @@ export const renderElement = ( context, renderConfig, appState, + allElementsMap, ); } @@ -737,7 +749,7 @@ export const renderElement = ( if (shouldResetImageFilter(element, renderConfig, appState)) { context.filter = "none"; } - const boundTextElement = getBoundTextElement(element); + const boundTextElement = getBoundTextElement(element, elementsMap); if (isArrowElement(element) && boundTextElement) { const tempCanvas = document.createElement("canvas"); @@ -820,6 +832,7 @@ export const renderElement = ( } else { const elementWithCanvas = generateElementWithCanvas( element, + elementsMap, renderConfig, appState, ); @@ -851,6 +864,7 @@ export const renderElement = ( context, renderConfig, appState, + allElementsMap, ); // reset @@ -1096,7 +1110,7 @@ export const renderElementToSvg = ( } case "line": case "arrow": { - const boundText = getBoundTextElement(element); + const boundText = getBoundTextElement(element, elementsMap); const maskPath = svgRoot.ownerDocument!.createElementNS(SVG_NS, "mask"); if (boundText) { maskPath.setAttribute("id", `mask-${element.id}`); diff --git a/packages/excalidraw/renderer/renderScene.ts b/packages/excalidraw/renderer/renderScene.ts index 6c358591b6..0fa56829fb 100644 --- a/packages/excalidraw/renderer/renderScene.ts +++ b/packages/excalidraw/renderer/renderScene.ts @@ -246,6 +246,7 @@ const renderLinearPointHandles = ( context: CanvasRenderingContext2D, appState: InteractiveCanvasAppState, element: NonDeleted, + elementsMap: RenderableElementsMap, ) => { if (!appState.selectedLinearElement) { return; @@ -269,6 +270,7 @@ const renderLinearPointHandles = ( //Rendering segment mid points const midPoints = LinearElementEditor.getEditorMidPoints( element, + elementsMap, appState, ).filter((midPoint) => midPoint !== null) as Point[]; @@ -485,7 +487,12 @@ const _renderInteractiveScene = ({ }); if (editingLinearElement) { - renderLinearPointHandles(context, appState, editingLinearElement); + renderLinearPointHandles( + context, + appState, + editingLinearElement, + elementsMap, + ); } // Paint selection element @@ -528,6 +535,7 @@ const _renderInteractiveScene = ({ context, appState, selectedElements[0] as NonDeleted, + elementsMap, ); } @@ -553,6 +561,7 @@ const _renderInteractiveScene = ({ context, appState, selectedElements[0] as ExcalidrawLinearElement, + elementsMap, ); } const selectionColor = renderConfig.selectionColor || oc.black; @@ -891,6 +900,7 @@ const _renderStaticScene = ({ canvas, rc, elementsMap, + allElementsMap, visibleElements, scale, appState, @@ -972,6 +982,7 @@ const _renderStaticScene = ({ renderElement( element, elementsMap, + allElementsMap, rc, context, renderConfig, @@ -982,6 +993,7 @@ const _renderStaticScene = ({ renderElement( element, elementsMap, + allElementsMap, rc, context, renderConfig, @@ -1005,6 +1017,7 @@ const _renderStaticScene = ({ renderElement( element, elementsMap, + allElementsMap, rc, context, renderConfig, @@ -1024,6 +1037,7 @@ const _renderStaticScene = ({ renderElement( label, elementsMap, + allElementsMap, rc, context, renderConfig, diff --git a/packages/excalidraw/scene/Scene.ts b/packages/excalidraw/scene/Scene.ts index 326f98c7fb..88c3d89963 100644 --- a/packages/excalidraw/scene/Scene.ts +++ b/packages/excalidraw/scene/Scene.ts @@ -4,8 +4,8 @@ import { NonDeleted, ExcalidrawFrameLikeElement, ElementsMapOrArray, - NonDeletedElementsMap, SceneElementsMap, + NonDeletedSceneElementsMap, } from "../element/types"; import { isNonDeletedElement } from "../element"; import { LinearElementEditor } from "../element/linearElementEditor"; @@ -27,7 +27,7 @@ type SelectionHash = string & { __brand: "selectionHash" }; const getNonDeletedElements = ( allElements: readonly T[], ) => { - const elementsMap = new Map() as NonDeletedElementsMap; + const elementsMap = new Map() as NonDeletedSceneElementsMap; const elements: T[] = []; for (const element of allElements) { if (!element.isDeleted) { @@ -120,8 +120,9 @@ class Scene { private callbacks: Set = new Set(); private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = []; - private nonDeletedElementsMap: NonDeletedElementsMap = - new Map() as NonDeletedElementsMap; + private nonDeletedElementsMap = toBrandedType( + new Map(), + ); private elements: readonly ExcalidrawElement[] = []; private nonDeletedFramesLikes: readonly NonDeleted[] = []; diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index 9f1f12a22f..d463e25971 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -4,6 +4,7 @@ import { ExcalidrawFrameLikeElement, ExcalidrawTextElement, NonDeletedExcalidrawElement, + NonDeletedSceneElementsMap, } from "../element/types"; import { Bounds, @@ -248,14 +249,15 @@ export const exportToCanvas = async ( files, }); - const elementsMap = toBrandedType( - arrayToMap(elementsForRender), - ); - renderStaticScene({ canvas, rc: rough.canvas(canvas), - elementsMap, + elementsMap: toBrandedType( + arrayToMap(elementsForRender), + ), + allElementsMap: toBrandedType( + arrayToMap(elements), + ), visibleElements: elementsForRender, scale, appState: { diff --git a/packages/excalidraw/scene/types.ts b/packages/excalidraw/scene/types.ts index 957b080b38..02aa3b7bf7 100644 --- a/packages/excalidraw/scene/types.ts +++ b/packages/excalidraw/scene/types.ts @@ -4,6 +4,7 @@ import { ExcalidrawTextElement, NonDeletedElementsMap, NonDeletedExcalidrawElement, + NonDeletedSceneElementsMap, } from "../element/types"; import { AppClassProperties, @@ -66,6 +67,7 @@ export type StaticSceneRenderConfig = { canvas: HTMLCanvasElement; rc: RoughCanvas; elementsMap: RenderableElementsMap; + allElementsMap: NonDeletedSceneElementsMap; visibleElements: readonly NonDeletedExcalidrawElement[]; scale: number; appState: StaticCanvasAppState; diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts index e7ff9b7876..7557145ae9 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -16,6 +16,7 @@ import { KEYS } from "./keys"; import { rangeIntersection, rangesOverlap, rotatePoint } from "./math"; import { getVisibleAndNonSelectedElements } from "./scene/selection"; import { AppState, KeyboardModifiersObject, Point } from "./types"; +import { arrayToMap } from "./utils"; const SNAP_DISTANCE = 8; @@ -286,7 +287,10 @@ export const getVisibleGaps = ( appState, ); - const referenceBounds = getMaximumGroups(referenceElements) + const referenceBounds = getMaximumGroups( + referenceElements, + arrayToMap(elements), + ) .filter( (elementsGroup) => !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), @@ -572,7 +576,7 @@ export const getReferenceSnapPoints = ( appState, ); - return getMaximumGroups(referenceElements) + return getMaximumGroups(referenceElements, arrayToMap(elements)) .filter( (elementsGroup) => !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), diff --git a/packages/excalidraw/tests/linearElementEditor.test.tsx b/packages/excalidraw/tests/linearElementEditor.test.tsx index f4ddeafd29..ce0e1c856b 100644 --- a/packages/excalidraw/tests/linearElementEditor.test.tsx +++ b/packages/excalidraw/tests/linearElementEditor.test.tsx @@ -24,6 +24,7 @@ import { import * as textElementUtils from "../element/textElement"; import { ROUNDNESS, VERTICAL_ALIGN } from "../constants"; import { vi } from "vitest"; +import { arrayToMap } from "../utils"; const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene"); const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); @@ -307,6 +308,7 @@ describe("Test Linear Elements", () => { const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); @@ -320,6 +322,7 @@ describe("Test Linear Elements", () => { const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints( h.elements[0] as ExcalidrawLinearElement, + h.app.scene.getNonDeletedElementsMap(), h.state, ); expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]); @@ -351,7 +354,11 @@ describe("Test Linear Elements", () => { const points = LinearElementEditor.getPointsGlobalCoordinates(line); expect([line.x, line.y]).toEqual(points[0]); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const startPoint = centerPoint(points[0], midPoints[0] as Point); const deltaX = 50; @@ -373,6 +380,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); expect(midPoints[0]).not.toEqual(newMidPoints[0]); @@ -458,7 +466,11 @@ describe("Test Linear Elements", () => { it("should update only the first segment midpoint when its point is dragged", async () => { const points = LinearElementEditor.getPointsGlobalCoordinates(line); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const hitCoords: Point = [points[0][0], points[0][1]]; @@ -478,6 +490,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); @@ -487,7 +500,11 @@ describe("Test Linear Elements", () => { it("should hide midpoints in the segment when points moved close", async () => { const points = LinearElementEditor.getPointsGlobalCoordinates(line); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const hitCoords: Point = [points[0][0], points[0][1]]; @@ -507,6 +524,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); // This midpoint is hidden since the points are too close @@ -526,7 +544,11 @@ describe("Test Linear Elements", () => { ]); expect(line.points.length).toEqual(4); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); // delete 3rd point deletePoint(points[2]); @@ -538,6 +560,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); expect(newMidPoints.length).toEqual(2); @@ -615,7 +638,11 @@ describe("Test Linear Elements", () => { it("should update all the midpoints when its point is dragged", async () => { const points = LinearElementEditor.getPointsGlobalCoordinates(line); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const hitCoords: Point = [points[0][0], points[0][1]]; @@ -630,6 +657,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); @@ -651,7 +679,11 @@ describe("Test Linear Elements", () => { it("should hide midpoints in the segment when points moved close", async () => { const points = LinearElementEditor.getPointsGlobalCoordinates(line); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const hitCoords: Point = [points[0][0], points[0][1]]; @@ -671,6 +703,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); // This mid point is hidden due to point being too close @@ -685,7 +718,11 @@ describe("Test Linear Elements", () => { ]); expect(line.points.length).toEqual(4); - const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state); + const midPoints = LinearElementEditor.getEditorMidPoints( + line, + h.app.scene.getNonDeletedElementsMap(), + h.state, + ); const points = LinearElementEditor.getPointsGlobalCoordinates(line); // delete 3rd point @@ -694,6 +731,7 @@ describe("Test Linear Elements", () => { const newMidPoints = LinearElementEditor.getEditorMidPoints( line, + h.app.scene.getNonDeletedElementsMap(), h.state, ); expect(newMidPoints.length).toEqual(2); @@ -762,7 +800,7 @@ describe("Test Linear Elements", () => { type: "text", x: 0, y: 0, - text: wrapText(text, font, getBoundTextMaxWidth(container)), + text: wrapText(text, font, getBoundTextMaxWidth(container, null)), containerId: container.id, width: 30, height: 20, @@ -986,8 +1024,13 @@ describe("Test Linear Elements", () => { collaboration made easy" `); - expect(LinearElementEditor.getElementAbsoluteCoords(container, true)) - .toMatchInlineSnapshot(` + expect( + LinearElementEditor.getElementAbsoluteCoords( + container, + h.app.scene.getNonDeletedElementsMap(), + true, + ), + ).toMatchInlineSnapshot(` [ 20, 20, @@ -1020,8 +1063,13 @@ describe("Test Linear Elements", () => { "Online whiteboard collaboration made easy" `); - expect(LinearElementEditor.getElementAbsoluteCoords(container, true)) - .toMatchInlineSnapshot(` + expect( + LinearElementEditor.getElementAbsoluteCoords( + container, + h.app.scene.getNonDeletedElementsMap(), + true, + ), + ).toMatchInlineSnapshot(` [ 20, 35, @@ -1121,7 +1169,11 @@ describe("Test Linear Elements", () => { expect(rect.x).toBe(400); expect(rect.y).toBe(0); expect( - wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)), + wrapText( + textElement.originalText, + font, + getBoundTextMaxWidth(arrow, null), + ), ).toMatchInlineSnapshot(` "Online whiteboard collaboration made easy" @@ -1140,11 +1192,17 @@ describe("Test Linear Elements", () => { expect(rect.x).toBe(200); expect(rect.y).toBe(0); expect(handleBindTextResizeSpy).toHaveBeenCalledWith( - h.elements[1], + h.elements[0], + arrayToMap(h.elements), + "nw", false, ); expect( - wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)), + wrapText( + textElement.originalText, + font, + getBoundTextMaxWidth(arrow, null), + ), ).toMatchInlineSnapshot(` "Online whiteboard collaboration made From 626fe252ab0c2d0cb295c849b887bd8c76133f40 Mon Sep 17 00:00:00 2001 From: Andran1k <91144891+Andran1k@users.noreply.github.com> Date: Mon, 29 Jan 2024 13:57:22 +0400 Subject: [PATCH 18/31] fix: frame name field (#7457) Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/excalidraw/components/App.tsx | 6 ++---- packages/excalidraw/frame.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 30f86c24ee..71bfaa5d55 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -1299,10 +1299,7 @@ class App extends React.Component { const FRAME_NAME_EDIT_PADDING = 6; const reset = () => { - if (f.name?.trim() === "") { - mutateElement(f, { name: null }); - } - + mutateElement(f, { name: f.name?.trim() || null }); this.setState({ editingFrame: null }); }; @@ -1325,6 +1322,7 @@ class App extends React.Component { name: e.target.value, }); }} + onFocus={(e) => e.target.select()} onBlur={() => reset()} onKeyDown={(event) => { // for some inexplicable reason, `onBlur` triggered on ESC diff --git a/packages/excalidraw/frame.ts b/packages/excalidraw/frame.ts index 1457c4ecf7..c4a5a259da 100644 --- a/packages/excalidraw/frame.ts +++ b/packages/excalidraw/frame.ts @@ -746,7 +746,7 @@ export const getFrameLikeTitle = ( element: ExcalidrawFrameLikeElement, frameIdx: number, ) => { - // TODO name frames AI only is specific to AI frames + // TODO name frames "AI" only if specific to AI frames return element.name === null ? isFrameElement(element) ? `Frame ${frameIdx}` From 2409c091fff0bd359c003e3e366de1834d0b7c92 Mon Sep 17 00:00:00 2001 From: Aashir Israr <63807168+aashirisrar@users.noreply.github.com> Date: Mon, 29 Jan 2024 19:27:07 +0500 Subject: [PATCH 19/31] feat: support roundness for images (#7558) Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/excalidraw/element/typeChecks.ts | 5 ++- packages/excalidraw/renderer/renderElement.ts | 36 +++++++++++++++++++ packages/excalidraw/scene/comparisons.ts | 3 +- .../tests/__snapshots__/export.test.tsx.snap | 2 +- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/excalidraw/element/typeChecks.ts b/packages/excalidraw/element/typeChecks.ts index ef1bcd3dbc..7193e251b3 100644 --- a/packages/excalidraw/element/typeChecks.ts +++ b/packages/excalidraw/element/typeChecks.ts @@ -214,7 +214,10 @@ export const isBoundToContainer = ( }; export const isUsingAdaptiveRadius = (type: string) => - type === "rectangle" || type === "embeddable" || type === "iframe"; + type === "rectangle" || + type === "embeddable" || + type === "iframe" || + type === "image"; export const isUsingProportionalRadius = (type: string) => type === "line" || type === "arrow" || type === "diamond"; diff --git a/packages/excalidraw/renderer/renderElement.ts b/packages/excalidraw/renderer/renderElement.ts index 5ab3f3ca52..de4bcfe533 100644 --- a/packages/excalidraw/renderer/renderElement.ts +++ b/packages/excalidraw/renderer/renderElement.ts @@ -344,6 +344,17 @@ const drawElementOnCanvas = ( ? renderConfig.imageCache.get(element.fileId)?.image : undefined; if (img != null && !(img instanceof Promise)) { + if (element.roundness && context.roundRect) { + context.beginPath(); + context.roundRect( + 0, + 0, + element.width, + element.height, + getCornerRadius(Math.min(element.width, element.height), element), + ); + context.clip(); + } context.drawImage( img, 0 /* hardcoded for the selection box*/, @@ -1301,6 +1312,31 @@ export const renderElementToSvg = ( }) rotate(${degree} ${cx} ${cy})`, ); + if (element.roundness) { + const clipPath = svgRoot.ownerDocument!.createElementNS( + SVG_NS, + "clipPath", + ); + clipPath.id = `image-clipPath-${element.id}`; + + const clipRect = svgRoot.ownerDocument!.createElementNS( + SVG_NS, + "rect", + ); + const radius = getCornerRadius( + Math.min(element.width, element.height), + element, + ); + clipRect.setAttribute("width", `${element.width}`); + clipRect.setAttribute("height", `${element.height}`); + clipRect.setAttribute("rx", `${radius}`); + clipRect.setAttribute("ry", `${radius}`); + clipPath.appendChild(clipRect); + addToRoot(clipPath, element); + + g.setAttributeNS(SVG_NS, "clip-path", `url(#${clipPath.id})`); + } + const clipG = maybeWrapNodesInFrameClipPath( element, root, diff --git a/packages/excalidraw/scene/comparisons.ts b/packages/excalidraw/scene/comparisons.ts index 551aa2e6e5..cb14d5810b 100644 --- a/packages/excalidraw/scene/comparisons.ts +++ b/packages/excalidraw/scene/comparisons.ts @@ -42,7 +42,8 @@ export const canChangeRoundness = (type: ElementOrToolType) => type === "embeddable" || type === "arrow" || type === "line" || - type === "diamond"; + type === "diamond" || + type === "image"; export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow"; diff --git a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap index 72b379b8a7..57dff6c1c5 100644 --- a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap @@ -21,5 +21,5 @@ exports[`export > exporting svg containing transformed images > svg export outpu - " + " `; From d426cc968d49071749c0d831490501cf572eb571 Mon Sep 17 00:00:00 2001 From: Milos Vetesnik Date: Mon, 29 Jan 2024 16:37:09 +0100 Subject: [PATCH 20/31] refactor: remove portal as it is no longer needed (#7623) Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- .env.development | 5 +---- .env.production | 7 ++----- excalidraw-app/collab/Collab.tsx | 9 ++------- excalidraw-app/data/index.ts | 29 ---------------------------- excalidraw-app/tests/collab.test.tsx | 11 ----------- 5 files changed, 5 insertions(+), 56 deletions(-) diff --git a/.env.development b/.env.development index 44955884f5..bab59ee075 100644 --- a/.env.development +++ b/.env.development @@ -5,10 +5,7 @@ VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries # collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room) -VITE_APP_WS_SERVER_URL=http://localhost:3002 - -# set this only if using the collaboration workflow we use on excalidraw.com -VITE_APP_PORTAL_URL= +VITE_APP_WS_SERVER_URL=http://localhost:3020 VITE_APP_PLUS_LP=https://plus.excalidraw.com VITE_APP_PLUS_APP=https://app.excalidraw.com diff --git a/.env.production b/.env.production index 26b46a52ab..0c715854a8 100644 --- a/.env.production +++ b/.env.production @@ -4,16 +4,13 @@ VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/ VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries -VITE_APP_PORTAL_URL=https://portal.excalidraw.com - VITE_APP_PLUS_LP=https://plus.excalidraw.com VITE_APP_PLUS_APP=https://app.excalidraw.com VITE_APP_AI_BACKEND=https://oss-ai.excalidraw.com -# Fill to set socket server URL used for collaboration. -# Meant for forks only: excalidraw.com uses custom VITE_APP_PORTAL_URL flow -VITE_APP_WS_SERVER_URL= +# socket server URL used for collaboration +VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}' diff --git a/excalidraw-app/collab/Collab.tsx b/excalidraw-app/collab/Collab.tsx index 92d94dbc9e..267dee66c0 100644 --- a/excalidraw-app/collab/Collab.tsx +++ b/excalidraw-app/collab/Collab.tsx @@ -36,7 +36,6 @@ import { import { generateCollaborationLinkData, getCollaborationLink, - getCollabServer, getSyncableElements, SocketUpdateDataSource, SyncableExcalidrawElement, @@ -452,13 +451,9 @@ class Collab extends PureComponent { this.fallbackInitializationHandler = fallbackInitializationHandler; try { - const socketServerData = await getCollabServer(); - this.portal.socket = this.portal.open( - socketIOClient(socketServerData.url, { - transports: socketServerData.polling - ? ["websocket", "polling"] - : ["websocket"], + socketIOClient(import.meta.env.VITE_APP_WS_SERVER_URL, { + transports: ["websocket", "polling"], }), roomId, roomKey, diff --git a/excalidraw-app/data/index.ts b/excalidraw-app/data/index.ts index 0f54ee880e..5699568b43 100644 --- a/excalidraw-app/data/index.ts +++ b/excalidraw-app/data/index.ts @@ -65,35 +65,6 @@ const generateRoomId = async () => { return bytesToHexString(buffer); }; -/** - * Right now the reason why we resolve connection params (url, polling...) - * from upstream is to allow changing the params immediately when needed without - * having to wait for clients to update the SW. - * - * If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks) - */ -export const getCollabServer = async (): Promise<{ - url: string; - polling: boolean; -}> => { - if (import.meta.env.VITE_APP_WS_SERVER_URL) { - return { - url: import.meta.env.VITE_APP_WS_SERVER_URL, - polling: true, - }; - } - - try { - const resp = await fetch( - `${import.meta.env.VITE_APP_PORTAL_URL}/collab-server`, - ); - return await resp.json(); - } catch (error) { - console.error(error); - throw new Error(t("errors.cannotResolveCollabServer")); - } -}; - export type EncryptedData = { data: ArrayBuffer; iv: Uint8Array; diff --git a/excalidraw-app/tests/collab.test.tsx b/excalidraw-app/tests/collab.test.tsx index 455316aed4..c3e94a5ef4 100644 --- a/excalidraw-app/tests/collab.test.tsx +++ b/excalidraw-app/tests/collab.test.tsx @@ -20,17 +20,6 @@ Object.defineProperty(window, "crypto", { }, }); -vi.mock("../../excalidraw-app/data/index.ts", async (importActual) => { - const module = (await importActual()) as any; - return { - __esmodule: true, - ...module, - getCollabServer: vi.fn(() => ({ - url: /* doesn't really matter */ "http://localhost:3002", - })), - }; -}); - vi.mock("../../excalidraw-app/data/firebase.ts", () => { const loadFromFirebase = async () => null; const saveToFirebase = () => {}; From e0fefa8025901ff73cb6b690bed3c73072c6f89a Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 31 Jan 2024 16:43:37 +0530 Subject: [PATCH 21/31] fix: don't bundle react-dom when importing from element (#7635) --- packages/excalidraw/components/App.tsx | 2 +- packages/excalidraw/element/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 71bfaa5d55..28daae36d2 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -115,7 +115,6 @@ import { newLinearElement, newTextElement, newImageElement, - textWysiwyg, transformElements, updateTextElement, redrawTextBoundingBox, @@ -409,6 +408,7 @@ import { AnimatedTrail } from "../animated-trail"; import { LaserTrails } from "../laser-trails"; import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils"; import { getRenderOpacity } from "../renderer/renderElement"; +import { textWysiwyg } from "../element/textWysiwyg"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); diff --git a/packages/excalidraw/element/index.ts b/packages/excalidraw/element/index.ts index 37d6a077b0..093ef48290 100644 --- a/packages/excalidraw/element/index.ts +++ b/packages/excalidraw/element/index.ts @@ -50,7 +50,6 @@ export { dragNewElement, } from "./dragElements"; export { isTextElement, isExcalidrawElement } from "./typeChecks"; -export { textWysiwyg } from "./textWysiwyg"; export { redrawTextBoundingBox } from "./textElement"; export { getPerfectElementSize, From 63b50b3586be121125db4feefbade4096120fd83 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 31 Jan 2024 16:50:35 +0530 Subject: [PATCH 22/31] fix: don't bundle react-dom when importing from transformHandles (#7634) * fix: don't bundle react when importing from transfromHandles * rename to DEFAULT_TRANSFORM_HANDLE_SPACING --- packages/excalidraw/constants.ts | 1 + packages/excalidraw/element/transformHandles.ts | 9 +++++---- packages/excalidraw/renderer/renderScene.ts | 13 ++++++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/excalidraw/constants.ts b/packages/excalidraw/constants.ts index c4df447975..021c706a99 100644 --- a/packages/excalidraw/constants.ts +++ b/packages/excalidraw/constants.ts @@ -142,6 +142,7 @@ export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil; export const DEFAULT_TEXT_ALIGN = "left"; export const DEFAULT_VERTICAL_ALIGN = "top"; export const DEFAULT_VERSION = "{version}"; +export const DEFAULT_TRANSFORM_HANDLE_SPACING = 2; export const CANVAS_ONLY_ACTIONS = ["selectAll"]; diff --git a/packages/excalidraw/element/transformHandles.ts b/packages/excalidraw/element/transformHandles.ts index 00ebfacfd7..19c60a93f1 100644 --- a/packages/excalidraw/element/transformHandles.ts +++ b/packages/excalidraw/element/transformHandles.ts @@ -9,7 +9,7 @@ import { rotate } from "../math"; import { InteractiveCanvasAppState, Zoom } from "../types"; import { isTextElement } from "."; import { isFrameLikeElement, isLinearElement } from "./typeChecks"; -import { DEFAULT_SPACING } from "../renderer/renderScene"; +import { DEFAULT_TRANSFORM_HANDLE_SPACING } from "../constants"; export type TransformHandleDirection = | "n" @@ -106,7 +106,8 @@ export const getTransformHandlesFromCoords = ( const width = x2 - x1; const height = y2 - y1; const dashedLineMargin = margin / zoom.value; - const centeringOffset = (size - DEFAULT_SPACING * 2) / (2 * zoom.value); + const centeringOffset = + (size - DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / (2 * zoom.value); const transformHandles: TransformHandles = { nw: omitSides.nw @@ -263,8 +264,8 @@ export const getTransformHandles = ( }; } const dashedLineMargin = isLinearElement(element) - ? DEFAULT_SPACING + 8 - : DEFAULT_SPACING; + ? DEFAULT_TRANSFORM_HANDLE_SPACING + 8 + : DEFAULT_TRANSFORM_HANDLE_SPACING; return getTransformHandlesFromCoords( getElementAbsoluteCoords(element, true), element.angle, diff --git a/packages/excalidraw/renderer/renderScene.ts b/packages/excalidraw/renderer/renderScene.ts index 0fa56829fb..d31d696506 100644 --- a/packages/excalidraw/renderer/renderScene.ts +++ b/packages/excalidraw/renderer/renderScene.ts @@ -64,7 +64,11 @@ import { } from "../element/transformHandles"; import { arrayToMap, throttleRAF } from "../utils"; import { UserIdleState } from "../types"; -import { FRAME_STYLE, THEME_FILTER } from "../constants"; +import { + DEFAULT_TRANSFORM_HANDLE_SPACING, + FRAME_STYLE, + THEME_FILTER, +} from "../constants"; import { EXTERNAL_LINK_IMG, getLinkHandleFromCoords, @@ -83,8 +87,6 @@ import { isElementInFrame, } from "../frame"; -export const DEFAULT_SPACING = 2; - const strokeRectWithRotation = ( context: CanvasRenderingContext2D, x: number, @@ -676,7 +678,8 @@ const _renderInteractiveScene = ({ ); } } else if (selectedElements.length > 1 && !appState.isRotating) { - const dashedLinePadding = (DEFAULT_SPACING * 2) / appState.zoom.value; + const dashedLinePadding = + (DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value; context.fillStyle = oc.white; const [x1, y1, x2, y2] = getCommonBounds(selectedElements); const initialLineDash = context.getLineDash(); @@ -1191,7 +1194,7 @@ const renderSelectionBorder = ( cy: number; activeEmbeddable: boolean; }, - padding = DEFAULT_SPACING * 2, + padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2, ) => { const { angle, From 1741c234a686983558f01d0ff449251f2810b41c Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 31 Jan 2024 21:17:41 +0530 Subject: [PATCH 23/31] fix: decouple container cache logic to containerCache. (#7637) --- .../excalidraw/actions/actionBoundText.tsx | 2 +- packages/excalidraw/element/containerCache.ts | 33 +++++++++++++++++ packages/excalidraw/element/textElement.ts | 5 ++- .../excalidraw/element/textWysiwyg.test.tsx | 2 +- packages/excalidraw/element/textWysiwyg.tsx | 37 ++----------------- 5 files changed, 42 insertions(+), 37 deletions(-) create mode 100644 packages/excalidraw/element/containerCache.ts diff --git a/packages/excalidraw/actions/actionBoundText.tsx b/packages/excalidraw/actions/actionBoundText.tsx index 05dd9c786f..722ad51115 100644 --- a/packages/excalidraw/actions/actionBoundText.tsx +++ b/packages/excalidraw/actions/actionBoundText.tsx @@ -17,7 +17,7 @@ import { getOriginalContainerHeightFromCache, resetOriginalContainerCache, updateOriginalContainerCache, -} from "../element/textWysiwyg"; +} from "../element/containerCache"; import { hasBoundTextElement, isTextBindableContainer, diff --git a/packages/excalidraw/element/containerCache.ts b/packages/excalidraw/element/containerCache.ts new file mode 100644 index 0000000000..c744f6c8e5 --- /dev/null +++ b/packages/excalidraw/element/containerCache.ts @@ -0,0 +1,33 @@ +import { ExcalidrawTextContainer } from "./types"; + +export const originalContainerCache: { + [id: ExcalidrawTextContainer["id"]]: + | { + height: ExcalidrawTextContainer["height"]; + } + | undefined; +} = {}; + +export const updateOriginalContainerCache = ( + id: ExcalidrawTextContainer["id"], + height: ExcalidrawTextContainer["height"], +) => { + const data = + originalContainerCache[id] || (originalContainerCache[id] = { height }); + data.height = height; + return data; +}; + +export const resetOriginalContainerCache = ( + id: ExcalidrawTextContainer["id"], +) => { + if (originalContainerCache[id]) { + delete originalContainerCache[id]; + } +}; + +export const getOriginalContainerHeightFromCache = ( + id: ExcalidrawTextContainer["id"], +) => { + return originalContainerCache[id]?.height ?? null; +}; diff --git a/packages/excalidraw/element/textElement.ts b/packages/excalidraw/element/textElement.ts index b264c0d594..fc4c15f2d1 100644 --- a/packages/excalidraw/element/textElement.ts +++ b/packages/excalidraw/element/textElement.ts @@ -31,11 +31,12 @@ import { isTextBindableContainer } from "./typeChecks"; import { getElementAbsoluteCoords } from "."; import { getSelectedElements } from "../scene"; import { isHittingElementNotConsideringBoundingBox } from "./collision"; + +import { ExtractSetType } from "../utility-types"; import { resetOriginalContainerCache, updateOriginalContainerCache, -} from "./textWysiwyg"; -import { ExtractSetType } from "../utility-types"; +} from "./containerCache"; export const normalizeText = (text: string) => { return ( diff --git a/packages/excalidraw/element/textWysiwyg.test.tsx b/packages/excalidraw/element/textWysiwyg.test.tsx index e6b0aa0b29..478fe5c1af 100644 --- a/packages/excalidraw/element/textWysiwyg.test.tsx +++ b/packages/excalidraw/element/textWysiwyg.test.tsx @@ -17,7 +17,7 @@ import { } from "./types"; import { API } from "../tests/helpers/api"; import { mutateElement } from "./mutateElement"; -import { getOriginalContainerHeightFromCache } from "./textWysiwyg"; +import { getOriginalContainerHeightFromCache } from "./containerCache"; import { getTextEditor, updateTextEditor } from "../tests/queries/dom"; // Unmount ReactDOM from root diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index d12d34f89a..1a628dd469 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -17,7 +17,6 @@ import { ExcalidrawLinearElement, ExcalidrawTextElementWithContainer, ExcalidrawTextElement, - ExcalidrawTextContainer, } from "./types"; import { AppState } from "../types"; import { bumpVersion, mutateElement } from "./mutateElement"; @@ -44,6 +43,10 @@ import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas"; import App from "../components/App"; import { LinearElementEditor } from "./linearElementEditor"; import { parseClipboard } from "../clipboard"; +import { + originalContainerCache, + updateOriginalContainerCache, +} from "./containerCache"; const getTransform = ( width: number, @@ -66,38 +69,6 @@ const getTransform = ( return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`; }; -const originalContainerCache: { - [id: ExcalidrawTextContainer["id"]]: - | { - height: ExcalidrawTextContainer["height"]; - } - | undefined; -} = {}; - -export const updateOriginalContainerCache = ( - id: ExcalidrawTextContainer["id"], - height: ExcalidrawTextContainer["height"], -) => { - const data = - originalContainerCache[id] || (originalContainerCache[id] = { height }); - data.height = height; - return data; -}; - -export const resetOriginalContainerCache = ( - id: ExcalidrawTextContainer["id"], -) => { - if (originalContainerCache[id]) { - delete originalContainerCache[id]; - } -}; - -export const getOriginalContainerHeightFromCache = ( - id: ExcalidrawTextContainer["id"], -) => { - return originalContainerCache[id]?.height ?? null; -}; - export const textWysiwyg = ({ id, onChange, From 90ad885446314bfb9efa62e46988354f1dc2daaa Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 1 Feb 2024 17:56:55 +0530 Subject: [PATCH 24/31] feat: support onPointerUp prop (#7638) * feat: support onPointerUp prop * update changelog * Update packages/excalidraw/CHANGELOG.md Co-authored-by: David Luzar <5153846+dwelle@users.noreply.github.com> --------- Co-authored-by: David Luzar <5153846+dwelle@users.noreply.github.com> --- packages/excalidraw/CHANGELOG.md | 5 ++++- packages/excalidraw/components/App.tsx | 1 + packages/excalidraw/index.tsx | 2 ++ packages/excalidraw/types.ts | 4 ++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/excalidraw/CHANGELOG.md b/packages/excalidraw/CHANGELOG.md index 9f59bd4afc..d2c40c25eb 100644 --- a/packages/excalidraw/CHANGELOG.md +++ b/packages/excalidraw/CHANGELOG.md @@ -13,8 +13,11 @@ Please add the latest change on the top under the correct section. ## Unreleased +### Features + +- Add `onPointerUp` prop [#7638](https://github.com/excalidraw/excalidraw/pull/7638). + - Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450) -- Remove `ExcalidrawEmbeddableElement.validated` attribute. [#7539](https://github.com/excalidraw/excalidraw/pull/7539) ### Breaking Changes diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 28daae36d2..462e803f13 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -7564,6 +7564,7 @@ class App extends React.Component { this.setState({ pendingImageElementId: null }); } + this.props?.onPointerUp?.(activeTool, pointerDownState); this.onPointerUpEmitter.trigger( this.state.activeTool, pointerDownState, diff --git a/packages/excalidraw/index.tsx b/packages/excalidraw/index.tsx index b450846936..f7be8affcc 100644 --- a/packages/excalidraw/index.tsx +++ b/packages/excalidraw/index.tsx @@ -44,6 +44,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { generateIdForFile, onLinkOpen, onPointerDown, + onPointerUp, onScrollChange, children, validateEmbeddable, @@ -131,6 +132,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { generateIdForFile={generateIdForFile} onLinkOpen={onLinkOpen} onPointerDown={onPointerDown} + onPointerUp={onPointerUp} onScrollChange={onScrollChange} validateEmbeddable={validateEmbeddable} renderEmbeddable={renderEmbeddable} diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 201a186baa..ddd799fb95 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -456,6 +456,10 @@ export interface ExcalidrawProps { activeTool: AppState["activeTool"], pointerDownState: PointerDownState, ) => void; + onPointerUp?: ( + activeTool: AppState["activeTool"], + pointerDownState: PointerDownState, + ) => void; onScrollChange?: (scrollX: number, scrollY: number, zoom: Zoom) => void; onUserFollow?: (payload: OnUserFollowedPayload) => void; children?: React.ReactNode; From 1c39bd57816f1de2b51c35e73a5d0e21b1a74fab Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 1 Feb 2024 18:24:17 +0530 Subject: [PATCH 25/31] fix: don't bundle react and jotai when importing from scene (#7640) * don't bundle react and jotai when importing from scene * fix --- packages/excalidraw/components/App.tsx | 2 +- packages/excalidraw/scene/index.ts | 1 - packages/excalidraw/types.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 462e803f13..c357b4ca3c 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -216,7 +216,6 @@ import { getNormalizedZoom, getSelectedElements, hasBackground, - isOverScrollBars, isSomeElementSelected, } from "../scene"; import Scene from "../scene/Scene"; @@ -409,6 +408,7 @@ import { LaserTrails } from "../laser-trails"; import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils"; import { getRenderOpacity } from "../renderer/renderElement"; import { textWysiwyg } from "../element/textWysiwyg"; +import { isOverScrollBars } from "../scene/scrollbars"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); diff --git a/packages/excalidraw/scene/index.ts b/packages/excalidraw/scene/index.ts index 5a7b9028a3..33399d79ef 100644 --- a/packages/excalidraw/scene/index.ts +++ b/packages/excalidraw/scene/index.ts @@ -1,4 +1,3 @@ -export { isOverScrollBars } from "./scrollbars"; export { isSomeElementSelected, getElementsWithinSelection, diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index ddd799fb95..e29bb9f89e 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -31,7 +31,7 @@ import type { throttleRAF } from "./utils"; import { Spreadsheet } from "./charts"; import { Language } from "./i18n"; import { ClipboardData } from "./clipboard"; -import { isOverScrollBars } from "./scene"; +import { isOverScrollBars } from "./scene/scrollbars"; import { MaybeTransformHandleType } from "./element/transformHandles"; import Library from "./data/library"; import type { FileSystemHandle } from "./data/filesystem"; From 4888d9d355cb847803cfb15bb1563f99c960b4f7 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Thu, 1 Feb 2024 14:41:38 +0100 Subject: [PATCH 26/31] chore: change default port of collab server (#7641) --- .env.development | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.development b/.env.development index bab59ee075..95e21ff87c 100644 --- a/.env.development +++ b/.env.development @@ -5,7 +5,7 @@ VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries # collaboration WebSocket server (https://github.com/excalidraw/excalidraw-room) -VITE_APP_WS_SERVER_URL=http://localhost:3020 +VITE_APP_WS_SERVER_URL=http://localhost:3002 VITE_APP_PLUS_LP=https://plus.excalidraw.com VITE_APP_PLUS_APP=https://app.excalidraw.com From 0e0f34edd89ca16273b175d16c87831f72dd97b9 Mon Sep 17 00:00:00 2001 From: Milos Vetesnik Date: Thu, 1 Feb 2024 15:03:15 +0100 Subject: [PATCH 27/31] fix: follow mode border for hosts apps (#7642) --- .../components/FollowMode/FollowMode.tsx | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/packages/excalidraw/components/FollowMode/FollowMode.tsx b/packages/excalidraw/components/FollowMode/FollowMode.tsx index da91ad42e5..dc1746ca85 100644 --- a/packages/excalidraw/components/FollowMode/FollowMode.tsx +++ b/packages/excalidraw/components/FollowMode/FollowMode.tsx @@ -16,25 +16,20 @@ const FollowMode = ({ onDisconnect, }: FollowModeProps) => { return ( -
-
-
-
- Following{" "} - - {userToFollow.username} - -
- + {userToFollow.username} +
+
); From 0c3dffb082c85552758459444a4777dc50a33326 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Thu, 1 Feb 2024 21:12:10 +0530 Subject: [PATCH 28/31] fix: make getEmbedLink independent of t function (#7643) * fix: make getEmbedLink independent of t function * rename warning to error and make it type safe --- packages/excalidraw/components/App.tsx | 7 +++++-- packages/excalidraw/element/Hyperlink.tsx | 7 +++++-- packages/excalidraw/element/embeddable.ts | 7 +++---- packages/excalidraw/element/types.ts | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index c357b4ca3c..f965a76795 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -6501,8 +6501,11 @@ class App extends React.Component { return; } - if (embedLink.warning) { - this.setToast({ message: embedLink.warning, closable: true }); + if (embedLink.error instanceof URIError) { + this.setToast({ + message: t("toast.unrecognizedLinkFormat"), + closable: true, + }); } const element = newEmbeddableElement({ diff --git a/packages/excalidraw/element/Hyperlink.tsx b/packages/excalidraw/element/Hyperlink.tsx index a69fdeb836..930b877631 100644 --- a/packages/excalidraw/element/Hyperlink.tsx +++ b/packages/excalidraw/element/Hyperlink.tsx @@ -120,8 +120,11 @@ export const Hyperlink = ({ } else { const { width, height } = element; const embedLink = getEmbedLink(link); - if (embedLink?.warning) { - setToast({ message: embedLink.warning, closable: true }); + if (embedLink?.error instanceof URIError) { + setToast({ + message: t("toast.unrecognizedLinkFormat"), + closable: true, + }); } const ar = embedLink ? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h diff --git a/packages/excalidraw/element/embeddable.ts b/packages/excalidraw/element/embeddable.ts index f62b0f95f4..fb51c7283b 100644 --- a/packages/excalidraw/element/embeddable.ts +++ b/packages/excalidraw/element/embeddable.ts @@ -1,6 +1,5 @@ import { register } from "../actions/register"; import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants"; -import { t } from "../i18n"; import { ExcalidrawProps } from "../types"; import { getFontString, updateActiveTool } from "../utils"; import { setCursorForShape } from "../cursor"; @@ -107,8 +106,8 @@ export const getEmbedLink = ( const vimeoLink = link.match(RE_VIMEO); if (vimeoLink?.[1]) { const target = vimeoLink?.[1]; - const warning = !/^\d+$/.test(target) - ? t("toast.unrecognizedLinkFormat") + const error = !/^\d+$/.test(target) + ? new URIError("Invalid embed link format") : undefined; type = "video"; link = `https://player.vimeo.com/video/${target}?api=1`; @@ -120,7 +119,7 @@ export const getEmbedLink = ( intrinsicSize: aspectRatio, type, }); - return { link, intrinsicSize: aspectRatio, type, warning }; + return { link, intrinsicSize: aspectRatio, type, error }; } const figmaLink = link.match(RE_FIGMA); diff --git a/packages/excalidraw/element/types.ts b/packages/excalidraw/element/types.ts index f89e8d5f23..aae0a8a30c 100644 --- a/packages/excalidraw/element/types.ts +++ b/packages/excalidraw/element/types.ts @@ -104,7 +104,7 @@ export type ExcalidrawIframeLikeElement = export type IframeData = | { intrinsicSize: { w: number; h: number }; - warning?: string; + error?: Error; } & ( | { type: "video" | "generic"; link: string } | { type: "document"; srcdoc: (theme: Theme) => string } From d67eaa8710a431f3e9bb9cdf3cef6d900f5edded Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sat, 3 Feb 2024 11:53:35 +0100 Subject: [PATCH 29/31] fix: file save timing out with big file sizes (#7649) --- packages/excalidraw/data/filesystem.ts | 2 +- packages/excalidraw/data/index.ts | 32 ++++++++++++++++---------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/excalidraw/data/filesystem.ts b/packages/excalidraw/data/filesystem.ts index fa29604f45..11f64d23e5 100644 --- a/packages/excalidraw/data/filesystem.ts +++ b/packages/excalidraw/data/filesystem.ts @@ -76,7 +76,7 @@ export const fileOpen = (opts: { }; export const fileSave = ( - blob: Blob, + blob: Blob | Promise, opts: { /** supply without the extension */ name: string; diff --git a/packages/excalidraw/data/index.ts b/packages/excalidraw/data/index.ts index 0c63053a94..fa2ec9de61 100644 --- a/packages/excalidraw/data/index.ts +++ b/packages/excalidraw/data/index.ts @@ -100,7 +100,7 @@ export const exportCanvas = async ( throw new Error(t("alerts.cannotExportEmptyCanvas")); } if (type === "svg" || type === "clipboard-svg") { - const tempSvg = await exportToSvg( + const svgPromise = exportToSvg( elements, { exportBackground, @@ -113,9 +113,12 @@ export const exportCanvas = async ( files, { exportingFrame }, ); + if (type === "svg") { - return await fileSave( - new Blob([tempSvg.outerHTML], { type: MIME_TYPES.svg }), + return fileSave( + svgPromise.then((svg) => { + return new Blob([svg.outerHTML], { type: MIME_TYPES.svg }); + }), { description: "Export to SVG", name, @@ -124,7 +127,9 @@ export const exportCanvas = async ( }, ); } else if (type === "clipboard-svg") { - await copyTextToSystemClipboard(tempSvg.outerHTML); + await copyTextToSystemClipboard( + await svgPromise.then((svg) => svg.outerHTML), + ); return; } } @@ -137,17 +142,20 @@ export const exportCanvas = async ( }); if (type === "png") { - let blob = await canvasToBlob(tempCanvas); + let blob = canvasToBlob(tempCanvas); + if (appState.exportEmbedScene) { - blob = await ( - await import("./image") - ).encodePngMetadata({ - blob, - metadata: serializeAsJSON(elements, appState, files, "local"), - }); + blob = blob.then((blob) => + import("./image").then(({ encodePngMetadata }) => + encodePngMetadata({ + blob, + metadata: serializeAsJSON(elements, appState, files, "local"), + }), + ), + ); } - return await fileSave(blob, { + return fileSave(blob, { description: "Export to PNG", name, // FIXME reintroduce `excalidraw.png` when most people upgrade away From a289c42830ea6b458a520c861dc5aaa95299c726 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sat, 3 Feb 2024 14:53:31 +0100 Subject: [PATCH 30/31] feat: add loading state to FilledButton (#7650) --- excalidraw-app/collab/RoomDialog.tsx | 8 +- packages/excalidraw/actions/manager.tsx | 3 +- .../excalidraw/components/FilledButton.scss | 76 ++++++++++++++++--- .../excalidraw/components/FilledButton.tsx | 50 +++++++++--- .../components/ImageExportDialog.scss | 2 + .../components/ImageExportDialog.tsx | 6 +- .../components/ShareableLinkDialog.tsx | 2 +- packages/excalidraw/components/ToolButton.tsx | 3 +- 8 files changed, 119 insertions(+), 31 deletions(-) diff --git a/excalidraw-app/collab/RoomDialog.tsx b/excalidraw-app/collab/RoomDialog.tsx index 48bc124464..f2614674de 100644 --- a/excalidraw-app/collab/RoomDialog.tsx +++ b/excalidraw-app/collab/RoomDialog.tsx @@ -120,7 +120,7 @@ export const RoomModal = ({ size="large" variant="icon" label="Share" - startIcon={getShareIcon()} + icon={getShareIcon()} className="RoomDialog__active__share" onClick={shareRoomLink} /> @@ -130,7 +130,7 @@ export const RoomModal = ({ @@ -166,7 +166,7 @@ export const RoomModal = ({ variant="outlined" color="danger" label={t("roomDialog.button_stopSession")} - startIcon={playerStopFilledIcon} + icon={playerStopFilledIcon} onClick={() => { trackEvent("share", "room closed"); onRoomDestroy(); @@ -195,7 +195,7 @@ export const RoomModal = ({ { trackEvent("share", "room creation", `ui (${getFrame()})`); onRoomCreate(); diff --git a/packages/excalidraw/actions/manager.tsx b/packages/excalidraw/actions/manager.tsx index fc56d1bda6..90dfe6088b 100644 --- a/packages/excalidraw/actions/manager.tsx +++ b/packages/excalidraw/actions/manager.tsx @@ -10,6 +10,7 @@ import { import { ExcalidrawElement } from "../element/types"; import { AppClassProperties, AppState } from "../types"; import { trackEvent } from "../analytics"; +import { isPromiseLike } from "../utils"; const trackAction = ( action: Action, @@ -55,7 +56,7 @@ export class ActionManager { app: AppClassProperties, ) { this.updater = (actionResult) => { - if (actionResult && "then" in actionResult) { + if (isPromiseLike(actionResult)) { actionResult.then((actionResult) => { return updater(actionResult); }); diff --git a/packages/excalidraw/components/FilledButton.scss b/packages/excalidraw/components/FilledButton.scss index bfa443f896..5891698e86 100644 --- a/packages/excalidraw/components/FilledButton.scss +++ b/packages/excalidraw/components/FilledButton.scss @@ -10,11 +10,39 @@ background-color: var(--back-color); border-color: var(--border-color); + .Spinner { + --spinner-color: var(--color-surface-lowest); + position: absolute; + visibility: visible; + } + + &[disabled] { + pointer-events: none; + + .ExcButton__contents { + visibility: hidden; + } + } + + &__contents { + display: flex; + justify-content: center; + align-items: center; + flex-shrink: 0; + flex-wrap: nowrap; + // needed because of .Spinner + position: relative; + } + &--color-primary { &.ExcButton--variant-filled { --text-color: var(--color-surface-lowest); --back-color: var(--color-primary); + .Spinner { + --spinner-color: var(--text-color); + } + &:hover { --back-color: var(--color-brand-hover); } @@ -27,9 +55,13 @@ &.ExcButton--variant-outlined, &.ExcButton--variant-icon { --text-color: var(--color-primary); - --border-color: var(--color-border-outline); + --border-color: var(--color-primary); --back-color: transparent; + .Spinner { + --spinner-color: var(--text-color); + } + &:hover { --text-color: var(--color-brand-hover); --border-color: var(--color-brand-hover); @@ -47,6 +79,10 @@ --text-color: var(--color-danger-text); --back-color: var(--color-danger-dark); + .Spinner { + --spinner-color: var(--text-color); + } + &:hover { --back-color: var(--color-danger-darker); } @@ -62,6 +98,10 @@ --border-color: var(--color-danger); --back-color: transparent; + .Spinner { + --spinner-color: var(--text-color); + } + &:hover { --text-color: var(--color-danger-darkest); --border-color: var(--color-danger-darkest); @@ -79,6 +119,10 @@ --text-color: var(--island-bg-color); --back-color: var(--color-gray-50); + .Spinner { + --spinner-color: var(--text-color); + } + &:hover { --back-color: var(--color-gray-60); } @@ -94,6 +138,10 @@ --border-color: var(--color-muted); --back-color: var(--island-bg-color); + .Spinner { + --spinner-color: var(--text-color); + } + &:hover { --text-color: var(--color-muted-background-darker); --border-color: var(--color-muted-darker); @@ -111,6 +159,10 @@ --text-color: black; --back-color: var(--color-warning-dark); + .Spinner { + --spinner-color: var(--text-color); + } + &:hover { --back-color: var(--color-warning-darker); } @@ -126,6 +178,10 @@ --border-color: var(--color-warning-dark); --back-color: var(--input-bg-color); + .Spinner { + --spinner-color: var(--text-color); + } + &:hover { --text-color: var(--color-warning-darker); --border-color: var(--color-warning-darker); @@ -138,17 +194,11 @@ } } - display: flex; - justify-content: center; - align-items: center; - flex-shrink: 0; - flex-wrap: nowrap; - border-radius: 0.5rem; border-width: 1px; border-style: solid; - font-family: "Assistant"; + font-family: var(--font-family); user-select: none; @@ -159,9 +209,12 @@ font-size: 0.875rem; min-height: 3rem; padding: 0.5rem 1.5rem; - gap: 0.75rem; letter-spacing: 0.4px; + + .ExcButton__contents { + gap: 0.75rem; + } } &--size-medium { @@ -169,9 +222,12 @@ font-size: 0.75rem; min-height: 2.5rem; padding: 0.5rem 1rem; - gap: 0.5rem; letter-spacing: normal; + + .ExcButton__contents { + gap: 0.5rem; + } } &--variant-icon { diff --git a/packages/excalidraw/components/FilledButton.tsx b/packages/excalidraw/components/FilledButton.tsx index 3f844cf373..ff17db623f 100644 --- a/packages/excalidraw/components/FilledButton.tsx +++ b/packages/excalidraw/components/FilledButton.tsx @@ -1,7 +1,10 @@ -import React, { forwardRef } from "react"; +import React, { forwardRef, useState } from "react"; import clsx from "clsx"; import "./FilledButton.scss"; +import { AbortError } from "../errors"; +import Spinner from "./Spinner"; +import { isPromiseLike } from "../utils"; export type ButtonVariant = "filled" | "outlined" | "icon"; export type ButtonColor = "primary" | "danger" | "warning" | "muted"; @@ -11,7 +14,7 @@ export type FilledButtonProps = { label: string; children?: React.ReactNode; - onClick?: () => void; + onClick?: (event: React.MouseEvent) => void; variant?: ButtonVariant; color?: ButtonColor; @@ -19,14 +22,14 @@ export type FilledButtonProps = { className?: string; fullWidth?: boolean; - startIcon?: React.ReactNode; + icon?: React.ReactNode; }; export const FilledButton = forwardRef( ( { children, - startIcon, + icon, onClick, label, variant = "filled", @@ -37,6 +40,27 @@ export const FilledButton = forwardRef( }, ref, ) => { + const [isLoading, setIsLoading] = useState(false); + + const _onClick = async (event: React.MouseEvent) => { + const ret = onClick?.(event); + + if (isPromiseLike(ret)) { + try { + setIsLoading(true); + await ret; + } catch (error: any) { + if (!(error instanceof AbortError)) { + throw error; + } else { + console.warn(error); + } + } finally { + setIsLoading(false); + } + } + }; + return ( ); }, diff --git a/packages/excalidraw/components/ImageExportDialog.scss b/packages/excalidraw/components/ImageExportDialog.scss index c998365994..ea9e74f805 100644 --- a/packages/excalidraw/components/ImageExportDialog.scss +++ b/packages/excalidraw/components/ImageExportDialog.scss @@ -12,6 +12,8 @@ flex-direction: row; justify-content: space-between; + user-select: none; + & h3 { font-family: "Assistant"; font-style: normal; diff --git a/packages/excalidraw/components/ImageExportDialog.tsx b/packages/excalidraw/components/ImageExportDialog.tsx index d0df35193b..7ca54e9851 100644 --- a/packages/excalidraw/components/ImageExportDialog.tsx +++ b/packages/excalidraw/components/ImageExportDialog.tsx @@ -271,7 +271,7 @@ const ImageExportModal = ({ exportingFrame, }) } - startIcon={downloadIcon} + icon={downloadIcon} > {t("imageExportDialog.button.exportToPng")} @@ -283,7 +283,7 @@ const ImageExportModal = ({ exportingFrame, }) } - startIcon={downloadIcon} + icon={downloadIcon} > {t("imageExportDialog.button.exportToSvg")} @@ -296,7 +296,7 @@ const ImageExportModal = ({ exportingFrame, }) } - startIcon={copyIcon} + icon={copyIcon} > {t("imageExportDialog.button.copyPngToClipboard")} diff --git a/packages/excalidraw/components/ShareableLinkDialog.tsx b/packages/excalidraw/components/ShareableLinkDialog.tsx index 7a53a4a82c..cb8ba4cefc 100644 --- a/packages/excalidraw/components/ShareableLinkDialog.tsx +++ b/packages/excalidraw/components/ShareableLinkDialog.tsx @@ -66,7 +66,7 @@ export const ShareableLinkDialog = ({ diff --git a/packages/excalidraw/components/ToolButton.tsx b/packages/excalidraw/components/ToolButton.tsx index ffe9a382cf..2dace89d7b 100644 --- a/packages/excalidraw/components/ToolButton.tsx +++ b/packages/excalidraw/components/ToolButton.tsx @@ -6,6 +6,7 @@ import { useExcalidrawContainer } from "./App"; import { AbortError } from "../errors"; import Spinner from "./Spinner"; import { PointerType } from "../element/types"; +import { isPromiseLike } from "../utils"; export type ToolButtonSize = "small" | "medium"; @@ -65,7 +66,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => { const onClick = async (event: React.MouseEvent) => { const ret = "onClick" in props && props.onClick?.(event); - if (ret && "then" in ret) { + if (isPromiseLike(ret)) { try { setIsLoading(true); await ret; From 0513b647ec13bc2688eaf75d41c2b45049f2624b Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sat, 3 Feb 2024 15:04:23 +0100 Subject: [PATCH 31/31] feat: change collab trigger & add share dialog (#7647) --- excalidraw-app/App.tsx | 88 ++++-- excalidraw-app/collab/Collab.tsx | 91 +++--- excalidraw-app/components/AppMainMenu.tsx | 4 +- .../components/AppWelcomeScreen.tsx | 4 +- .../ShareDialog.scss} | 27 +- excalidraw-app/share/ShareDialog.tsx | 290 ++++++++++++++++++ packages/excalidraw/assets/lock.svg | 20 -- packages/excalidraw/components/Button.tsx | 1 + .../excalidraw/components/FilledButton.scss | 1 + .../components/JSONExportDialog.tsx | 2 +- .../components/ShareableLinkDialog.scss | 4 +- packages/excalidraw/components/TextField.tsx | 17 +- .../LiveCollaborationTrigger.scss | 8 +- .../LiveCollaborationTrigger.tsx | 8 +- packages/excalidraw/css/variables.module.scss | 1 + packages/excalidraw/locales/en.json | 7 +- packages/excalidraw/types.ts | 1 - packages/excalidraw/utils.ts | 2 +- 18 files changed, 440 insertions(+), 136 deletions(-) rename excalidraw-app/{collab/RoomDialog.scss => share/ShareDialog.scss} (82%) create mode 100644 excalidraw-app/share/ShareDialog.tsx delete mode 100644 packages/excalidraw/assets/lock.svg diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx index 4a3d42847e..e38dd7a946 100644 --- a/excalidraw-app/App.tsx +++ b/excalidraw-app/App.tsx @@ -1,6 +1,6 @@ import polyfill from "../packages/excalidraw/polyfill"; import LanguageDetector from "i18next-browser-languagedetector"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { trackEvent } from "../packages/excalidraw/analytics"; import { getDefaultAppState } from "../packages/excalidraw/appState"; import { ErrorDialog } from "../packages/excalidraw/components/ErrorDialog"; @@ -54,7 +54,6 @@ import { import Collab, { CollabAPI, collabAPIAtom, - collabDialogShownAtom, isCollaboratingAtom, isOfflineAtom, } from "./collab/Collab"; @@ -104,6 +103,7 @@ import { ShareableLinkDialog } from "../packages/excalidraw/components/Shareable import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState"; import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm"; import Trans from "../packages/excalidraw/components/Trans"; +import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog"; polyfill(); @@ -305,8 +305,8 @@ const ExcalidrawWrapper = () => { const [excalidrawAPI, excalidrawRefCallback] = useCallbackRefState(); + const [, setShareDialogState] = useAtom(shareDialogStateAtom); const [collabAPI] = useAtom(collabAPIAtom); - const [, setCollabDialogShown] = useAtom(collabDialogShownAtom); const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => { return isCollaborationLink(window.location.href); }); @@ -607,37 +607,38 @@ const ExcalidrawWrapper = () => { exportedElements: readonly NonDeletedExcalidrawElement[], appState: Partial, files: BinaryFiles, - canvas: HTMLCanvasElement, ) => { if (exportedElements.length === 0) { throw new Error(t("alerts.cannotExportEmptyCanvas")); } - if (canvas) { - try { - const { url, errorMessage } = await exportToBackend( - exportedElements, - { - ...appState, - viewBackgroundColor: appState.exportBackground - ? appState.viewBackgroundColor - : getDefaultAppState().viewBackgroundColor, - }, - files, - ); + try { + const { url, errorMessage } = await exportToBackend( + exportedElements, + { + ...appState, + viewBackgroundColor: appState.exportBackground + ? appState.viewBackgroundColor + : getDefaultAppState().viewBackgroundColor, + }, + files, + ); - if (errorMessage) { - throw new Error(errorMessage); - } + if (errorMessage) { + throw new Error(errorMessage); + } - if (url) { - setLatestShareableLink(url); - } - } catch (error: any) { - if (error.name !== "AbortError") { - const { width, height } = canvas; - console.error(error, { width, height }); - throw new Error(error.message); - } + if (url) { + setLatestShareableLink(url); + } + } catch (error: any) { + if (error.name !== "AbortError") { + const { width, height } = appState; + console.error(error, { + width, + height, + devicePixelRatio: window.devicePixelRatio, + }); + throw new Error(error.message); } } }; @@ -666,6 +667,11 @@ const ExcalidrawWrapper = () => { const isOffline = useAtomValue(isOfflineAtom); + const onCollabDialogOpen = useCallback( + () => setShareDialogState({ isOpen: true, type: "collaborationOnly" }), + [setShareDialogState], + ); + // browsers generally prevent infinite self-embedding, there are // cases where it still happens, and while we disallow self-embedding // by not whitelisting our own origin, this serves as an additional guard @@ -741,18 +747,20 @@ const ExcalidrawWrapper = () => { return ( setCollabDialogShown(true)} + onSelect={() => + setShareDialogState({ isOpen: true, type: "share" }) + } /> ); }} > @@ -848,6 +856,24 @@ const ExcalidrawWrapper = () => { {excalidrawAPI && !isCollabDisabled && ( )} + + { + if (excalidrawAPI) { + try { + await onExportToBackend( + excalidrawAPI.getSceneElements(), + excalidrawAPI.getAppState(), + excalidrawAPI.getFiles(), + ); + } catch (error: any) { + setErrorMessage(error.message); + } + } + }} + /> + {errorMessage && ( setErrorMessage("")}> {errorMessage} diff --git a/excalidraw-app/collab/Collab.tsx b/excalidraw-app/collab/Collab.tsx index 267dee66c0..14538b674e 100644 --- a/excalidraw-app/collab/Collab.tsx +++ b/excalidraw-app/collab/Collab.tsx @@ -52,7 +52,6 @@ import { saveUsernameToLocalStorage, } from "../data/localStorage"; import Portal from "./Portal"; -import RoomDialog from "./RoomDialog"; import { t } from "../../packages/excalidraw/i18n"; import { UserIdleState } from "../../packages/excalidraw/types"; import { @@ -77,23 +76,24 @@ import { import { decryptData } from "../../packages/excalidraw/data/encryption"; import { resetBrowserStateVersions } from "../data/tabSync"; import { LocalData } from "../data/LocalData"; -import { atom, useAtom } from "jotai"; +import { atom } from "jotai"; import { appJotaiStore } from "../app-jotai"; import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types"; import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds"; import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils"; export const collabAPIAtom = atom(null); -export const collabDialogShownAtom = atom(false); export const isCollaboratingAtom = atom(false); export const isOfflineAtom = atom(false); interface CollabState { - errorMessage: string; + errorMessage: string | null; username: string; - activeRoomLink: string; + activeRoomLink: string | null; } +export const activeRoomLinkAtom = atom(null); + type CollabInstance = InstanceType; export interface CollabAPI { @@ -104,19 +104,20 @@ export interface CollabAPI { stopCollaboration: CollabInstance["stopCollaboration"]; syncElements: CollabInstance["syncElements"]; fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"]; - setUsername: (username: string) => void; + setUsername: CollabInstance["setUsername"]; + getUsername: CollabInstance["getUsername"]; + getActiveRoomLink: CollabInstance["getActiveRoomLink"]; + setErrorMessage: CollabInstance["setErrorMessage"]; } -interface PublicProps { +interface CollabProps { excalidrawAPI: ExcalidrawImperativeAPI; } -type Props = PublicProps & { modalIsShown: boolean }; - -class Collab extends PureComponent { +class Collab extends PureComponent { portal: Portal; fileManager: FileManager; - excalidrawAPI: Props["excalidrawAPI"]; + excalidrawAPI: CollabProps["excalidrawAPI"]; activeIntervalId: number | null; idleTimeoutId: number | null; @@ -124,12 +125,12 @@ class Collab extends PureComponent { private lastBroadcastedOrReceivedSceneVersion: number = -1; private collaborators = new Map(); - constructor(props: Props) { + constructor(props: CollabProps) { super(props); this.state = { - errorMessage: "", + errorMessage: null, username: importUsernameFromLocalStorage() || "", - activeRoomLink: "", + activeRoomLink: null, }; this.portal = new Portal(this); this.fileManager = new FileManager({ @@ -194,6 +195,9 @@ class Collab extends PureComponent { fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase, stopCollaboration: this.stopCollaboration, setUsername: this.setUsername, + getUsername: this.getUsername, + getActiveRoomLink: this.getActiveRoomLink, + setErrorMessage: this.setErrorMessage, }; appJotaiStore.set(collabAPIAtom, collabAPI); @@ -341,9 +345,7 @@ class Collab extends PureComponent { this.fileManager.reset(); if (!opts?.isUnload) { this.setIsCollaborating(false); - this.setState({ - activeRoomLink: "", - }); + this.setActiveRoomLink(null); this.collaborators = new Map(); this.excalidrawAPI.updateScene({ collaborators: this.collaborators, @@ -409,7 +411,7 @@ class Collab extends PureComponent { if (!this.state.username) { import("@excalidraw/random-username").then(({ getRandomUsername }) => { const username = getRandomUsername(); - this.onUsernameChange(username); + this.setUsername(username); }); } @@ -624,9 +626,7 @@ class Collab extends PureComponent { this.initializeIdleDetector(); - this.setState({ - activeRoomLink: window.location.href, - }); + this.setActiveRoomLink(window.location.href); return scenePromise; }; @@ -909,41 +909,31 @@ class Collab extends PureComponent { { leading: false }, ); - handleClose = () => { - appJotaiStore.set(collabDialogShownAtom, false); - }; - setUsername = (username: string) => { this.setState({ username }); - }; - - onUsernameChange = (username: string) => { - this.setUsername(username); saveUsernameToLocalStorage(username); }; - render() { - const { username, errorMessage, activeRoomLink } = this.state; + getUsername = () => this.state.username; - const { modalIsShown } = this.props; + setActiveRoomLink = (activeRoomLink: string | null) => { + this.setState({ activeRoomLink }); + appJotaiStore.set(activeRoomLinkAtom, activeRoomLink); + }; + + getActiveRoomLink = () => this.state.activeRoomLink; + + setErrorMessage = (errorMessage: string | null) => { + this.setState({ errorMessage }); + }; + + render() { + const { errorMessage } = this.state; return ( <> - {modalIsShown && ( - this.startCollaboration(null)} - onRoomDestroy={this.stopCollaboration} - setErrorMessage={(errorMessage) => { - this.setState({ errorMessage }); - }} - /> - )} - {errorMessage && ( - this.setState({ errorMessage: "" })}> + {errorMessage != null && ( + this.setState({ errorMessage: null })}> {errorMessage} )} @@ -962,11 +952,6 @@ if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) { window.collab = window.collab || ({} as Window["collab"]); } -const _Collab: React.FC = (props) => { - const [collabDialogShown] = useAtom(collabDialogShownAtom); - return ; -}; - -export default _Collab; +export default Collab; export type TCollabClass = Collab; diff --git a/excalidraw-app/components/AppMainMenu.tsx b/excalidraw-app/components/AppMainMenu.tsx index 34a2ee3ae7..6806c969cb 100644 --- a/excalidraw-app/components/AppMainMenu.tsx +++ b/excalidraw-app/components/AppMainMenu.tsx @@ -4,7 +4,7 @@ import { MainMenu } from "../../packages/excalidraw/index"; import { LanguageList } from "./LanguageList"; export const AppMainMenu: React.FC<{ - setCollabDialogShown: (toggle: boolean) => any; + onCollabDialogOpen: () => any; isCollaborating: boolean; isCollabEnabled: boolean; }> = React.memo((props) => { @@ -17,7 +17,7 @@ export const AppMainMenu: React.FC<{ {props.isCollabEnabled && ( props.setCollabDialogShown(true)} + onSelect={() => props.onCollabDialogOpen()} /> )} diff --git a/excalidraw-app/components/AppWelcomeScreen.tsx b/excalidraw-app/components/AppWelcomeScreen.tsx index a5176c2ffa..f74bc14e26 100644 --- a/excalidraw-app/components/AppWelcomeScreen.tsx +++ b/excalidraw-app/components/AppWelcomeScreen.tsx @@ -6,7 +6,7 @@ import { isExcalidrawPlusSignedUser } from "../app_constants"; import { POINTER_EVENTS } from "../../packages/excalidraw/constants"; export const AppWelcomeScreen: React.FC<{ - setCollabDialogShown: (toggle: boolean) => any; + onCollabDialogOpen: () => any; isCollabEnabled: boolean; }> = React.memo((props) => { const { t } = useI18n(); @@ -52,7 +52,7 @@ export const AppWelcomeScreen: React.FC<{ {props.isCollabEnabled && ( props.setCollabDialogShown(true)} + onSelect={() => props.onCollabDialogOpen()} /> )} {!isExcalidrawPlusSignedUser && ( diff --git a/excalidraw-app/collab/RoomDialog.scss b/excalidraw-app/share/ShareDialog.scss similarity index 82% rename from excalidraw-app/collab/RoomDialog.scss rename to excalidraw-app/share/ShareDialog.scss index 61624664b5..87fde84914 100644 --- a/excalidraw-app/collab/RoomDialog.scss +++ b/excalidraw-app/share/ShareDialog.scss @@ -1,7 +1,7 @@ @import "../../packages/excalidraw/css/variables.module.scss"; .excalidraw { - .RoomDialog { + .ShareDialog { display: flex; flex-direction: column; gap: 1.5rem; @@ -10,8 +10,25 @@ height: calc(100vh - 5rem); } + &__separator { + border-top: 1px solid var(--dialog-border-color); + text-align: center; + display: flex; + justify-content: center; + align-items: center; + margin-top: 1em; + + span { + background: var(--island-bg-color); + padding: 0px 0.75rem; + transform: translateY(-1ch); + display: inline-flex; + line-height: 1; + } + } + &__popover { - @keyframes RoomDialog__popover__scaleIn { + @keyframes ShareDialog__popover__scaleIn { from { opacity: 0; } @@ -50,10 +67,10 @@ } transform-origin: var(--radix-popover-content-transform-origin); - animation: RoomDialog__popover__scaleIn 150ms ease-out; + animation: ShareDialog__popover__scaleIn 150ms ease-out; } - &__inactive { + &__picker { font-family: "Assistant"; &__illustration { @@ -95,7 +112,7 @@ } } - &__start_session { + &__button { display: flex; align-items: center; diff --git a/excalidraw-app/share/ShareDialog.tsx b/excalidraw-app/share/ShareDialog.tsx new file mode 100644 index 0000000000..2fa92dff86 --- /dev/null +++ b/excalidraw-app/share/ShareDialog.tsx @@ -0,0 +1,290 @@ +import { useRef, useState } from "react"; +import * as Popover from "@radix-ui/react-popover"; +import { copyTextToSystemClipboard } from "../../packages/excalidraw/clipboard"; +import { trackEvent } from "../../packages/excalidraw/analytics"; +import { getFrame } from "../../packages/excalidraw/utils"; +import { useI18n } from "../../packages/excalidraw/i18n"; +import { KEYS } from "../../packages/excalidraw/keys"; +import { Dialog } from "../../packages/excalidraw/components/Dialog"; +import { + copyIcon, + LinkIcon, + playerPlayIcon, + playerStopFilledIcon, + share, + shareIOS, + shareWindows, + tablerCheckIcon, +} from "../../packages/excalidraw/components/icons"; +import { TextField } from "../../packages/excalidraw/components/TextField"; +import { FilledButton } from "../../packages/excalidraw/components/FilledButton"; +import { activeRoomLinkAtom, CollabAPI } from "../collab/Collab"; +import { atom, useAtom, useAtomValue } from "jotai"; + +import "./ShareDialog.scss"; + +type OnExportToBackend = () => void; +type ShareDialogType = "share" | "collaborationOnly"; + +export const shareDialogStateAtom = atom< + { isOpen: false } | { isOpen: true; type: ShareDialogType } +>({ isOpen: false }); + +const getShareIcon = () => { + const navigator = window.navigator as any; + const isAppleBrowser = /Apple/.test(navigator.vendor); + const isWindowsBrowser = navigator.appVersion.indexOf("Win") !== -1; + + if (isAppleBrowser) { + return shareIOS; + } else if (isWindowsBrowser) { + return shareWindows; + } + + return share; +}; + +export type ShareDialogProps = { + collabAPI: CollabAPI | null; + handleClose: () => void; + onExportToBackend: OnExportToBackend; + type: ShareDialogType; +}; + +const ActiveRoomDialog = ({ + collabAPI, + activeRoomLink, + handleClose, +}: { + collabAPI: CollabAPI; + activeRoomLink: string; + handleClose: () => void; +}) => { + const { t } = useI18n(); + const [justCopied, setJustCopied] = useState(false); + const timerRef = useRef(0); + const ref = useRef(null); + const isShareSupported = "share" in navigator; + + const copyRoomLink = async () => { + try { + await copyTextToSystemClipboard(activeRoomLink); + + setJustCopied(true); + + if (timerRef.current) { + window.clearTimeout(timerRef.current); + } + + timerRef.current = window.setTimeout(() => { + setJustCopied(false); + }, 3000); + } catch (error: any) { + collabAPI.setErrorMessage(error.message); + } + + ref.current?.select(); + }; + + const shareRoomLink = async () => { + try { + await navigator.share({ + title: t("roomDialog.shareTitle"), + text: t("roomDialog.shareTitle"), + url: activeRoomLink, + }); + } catch (error: any) { + // Just ignore. + } + }; + + return ( + <> +

+ {t("labels.liveCollaboration").replace(/\./g, "")} +

+ event.key === KEYS.ENTER && handleClose()} + /> +
+ + {isShareSupported && ( + + )} + + + + + event.preventDefault()} + onCloseAutoFocus={(event) => event.preventDefault()} + className="ShareDialog__popover" + side="top" + align="end" + sideOffset={5.5} + > + {tablerCheckIcon} copied + + +
+
+

+ + {t("roomDialog.desc_privacy")} +

+

{t("roomDialog.desc_exitSession")}

+
+ +
+ { + trackEvent("share", "room closed"); + collabAPI.stopCollaboration(); + if (!collabAPI.isCollaborating()) { + handleClose(); + } + }} + /> +
+ + ); +}; + +const ShareDialogPicker = (props: ShareDialogProps) => { + const { t } = useI18n(); + + const { collabAPI } = props; + + const startCollabJSX = collabAPI ? ( + <> +
+ {t("labels.liveCollaboration").replace(/\./g, "")} +
+ +
+
{t("roomDialog.desc_intro")}
+ {t("roomDialog.desc_privacy")} +
+ +
+ { + trackEvent("share", "room creation", `ui (${getFrame()})`); + collabAPI.startCollaboration(null); + }} + /> +
+ + {props.type === "share" && ( +
+ {t("shareDialog.or")} +
+ )} + + ) : null; + + return ( + <> + {startCollabJSX} + + {props.type === "share" && ( + <> +
+ {t("exportDialog.link_title")} +
+
+ {t("exportDialog.link_details")} +
+ +
+ { + await props.onExportToBackend(); + props.handleClose(); + }} + /> +
+ + )} + + ); +}; + +const ShareDialogInner = (props: ShareDialogProps) => { + const activeRoomLink = useAtomValue(activeRoomLinkAtom); + + return ( + +
+ {props.collabAPI && activeRoomLink ? ( + + ) : ( + + )} +
+
+ ); +}; + +export const ShareDialog = (props: { + collabAPI: CollabAPI | null; + onExportToBackend: OnExportToBackend; +}) => { + const [shareDialogState, setShareDialogState] = useAtom(shareDialogStateAtom); + + if (!shareDialogState.isOpen) { + return null; + } + + return ( + setShareDialogState({ isOpen: false })} + collabAPI={props.collabAPI} + onExportToBackend={props.onExportToBackend} + type={shareDialogState.type} + > + ); +}; diff --git a/packages/excalidraw/assets/lock.svg b/packages/excalidraw/assets/lock.svg deleted file mode 100644 index aa9dbf1701..0000000000 --- a/packages/excalidraw/assets/lock.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/excalidraw/components/Button.tsx b/packages/excalidraw/components/Button.tsx index 43b6de9e11..779cee5828 100644 --- a/packages/excalidraw/components/Button.tsx +++ b/packages/excalidraw/components/Button.tsx @@ -1,4 +1,5 @@ import clsx from "clsx"; +import React from "react"; import { composeEventHandlers } from "../utils"; import "./Button.scss"; diff --git a/packages/excalidraw/components/FilledButton.scss b/packages/excalidraw/components/FilledButton.scss index 5891698e86..70f75cbbb8 100644 --- a/packages/excalidraw/components/FilledButton.scss +++ b/packages/excalidraw/components/FilledButton.scss @@ -24,6 +24,7 @@ } } + &, &__contents { display: flex; justify-content: center; diff --git a/packages/excalidraw/components/JSONExportDialog.tsx b/packages/excalidraw/components/JSONExportDialog.tsx index b5cea4af63..95f4117fcb 100644 --- a/packages/excalidraw/components/JSONExportDialog.tsx +++ b/packages/excalidraw/components/JSONExportDialog.tsx @@ -78,7 +78,7 @@ const JSONExportModal = ({ onClick={async () => { try { trackEvent("export", "link", `ui (${getFrame()})`); - await onExportToBackend(elements, appState, files, canvas); + await onExportToBackend(elements, appState, files); onCloseRequest(); } catch (error: any) { setAppState({ errorMessage: error.message }); diff --git a/packages/excalidraw/components/ShareableLinkDialog.scss b/packages/excalidraw/components/ShareableLinkDialog.scss index 2b89f09d6e..2429d50ca2 100644 --- a/packages/excalidraw/components/ShareableLinkDialog.scss +++ b/packages/excalidraw/components/ShareableLinkDialog.scss @@ -22,7 +22,7 @@ } &__popover { - @keyframes RoomDialog__popover__scaleIn { + @keyframes ShareableLinkDialog__popover__scaleIn { from { opacity: 0; } @@ -61,7 +61,7 @@ } transform-origin: var(--radix-popover-content-transform-origin); - animation: RoomDialog__popover__scaleIn 150ms ease-out; + animation: ShareableLinkDialog__popover__scaleIn 150ms ease-out; } &__linkRow { diff --git a/packages/excalidraw/components/TextField.tsx b/packages/excalidraw/components/TextField.tsx index 10b3d9b533..44a7c25ff3 100644 --- a/packages/excalidraw/components/TextField.tsx +++ b/packages/excalidraw/components/TextField.tsx @@ -13,8 +13,6 @@ import { Button } from "./Button"; import { eyeIcon, eyeClosedIcon } from "./icons"; type TextFieldProps = { - value?: string; - onChange?: (value: string) => void; onClick?: () => void; onKeyDown?: (event: KeyboardEvent) => void; @@ -26,12 +24,11 @@ type TextFieldProps = { label?: string; placeholder?: string; isRedacted?: boolean; -}; +} & ({ value: string } | { defaultValue: string }); export const TextField = forwardRef( ( { - value, onChange, label, fullWidth, @@ -40,6 +37,7 @@ export const TextField = forwardRef( selectOnRender, onKeyDown, isRedacted = false, + ...rest }, ref, ) => { @@ -73,10 +71,17 @@ export const TextField = forwardRef( > onChange?.(event.target.value)} diff --git a/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.scss b/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.scss index edbcf198ff..573fbccce6 100644 --- a/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.scss +++ b/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.scss @@ -3,7 +3,7 @@ .excalidraw { .collab-button { --button-bg: var(--color-primary); - --button-color: white; + --button-color: var(--color-surface-lowest); --button-border: var(--color-primary); --button-width: var(--lg-button-size); @@ -35,12 +35,6 @@ } } - &.theme--dark { - .collab-button { - color: var(--color-gray-90); - } - } - .CollabButton.is-collaborating { background-color: var(--button-special-active-bg-color); diff --git a/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx b/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx index 3111680cb0..a22bc523ac 100644 --- a/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx +++ b/packages/excalidraw/components/live-collaboration/LiveCollaborationTrigger.tsx @@ -1,5 +1,5 @@ import { t } from "../../i18n"; -import { usersIcon } from "../icons"; +import { share } from "../icons"; import { Button } from "../Button"; import clsx from "clsx"; @@ -17,16 +17,18 @@ const LiveCollaborationTrigger = ({ } & React.ButtonHTMLAttributes) => { const appState = useUIAppState(); + const showIconOnly = appState.width < 830; + return ( . Please include information below by copying and pasting into the GitHub issue.", "sceneContent": "Scene content:" }, + "shareDialog": { + "or": "Or" + }, "roomDialog": { - "desc_intro": "You can invite people to your current scene to collaborate with you.", - "desc_privacy": "Don't worry, the session uses end-to-end encryption, so whatever you draw will stay private. Not even our server will be able to see what you come up with.", + "desc_intro": "Invite people to collaborate on your drawing.", + "desc_privacy": "Don't worry, the session is end-to-end encrypted, and fully private. Not even our server can see what you draw.", "button_startSession": "Start session", "button_stopSession": "Stop session", "desc_inProgressIntro": "Live-collaboration session is now in progress.", diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index e29bb9f89e..89b121b2f4 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -495,7 +495,6 @@ export type ExportOpts = { exportedElements: readonly NonDeletedExcalidrawElement[], appState: UIAppState, files: BinaryFiles, - canvas: HTMLCanvasElement, ) => void; renderCustomUI?: ( exportedElements: readonly NonDeletedExcalidrawElement[], diff --git a/packages/excalidraw/utils.ts b/packages/excalidraw/utils.ts index 47fa52311c..525652e6bc 100644 --- a/packages/excalidraw/utils.ts +++ b/packages/excalidraw/utils.ts @@ -845,7 +845,7 @@ export const composeEventHandlers = ( if ( !checkForDefaultPrevented || - !(event as unknown as Event).defaultPrevented + !(event as unknown as Event)?.defaultPrevented ) { return ourEventHandler?.(event); }

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�?qd(5Hh*uAIxK&qTYu3V< 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 PfG>3wZRJu$%i#Y78=43z literal 0 HcmV?d00001 diff --git a/examples/excalidraw/with-script-in-browser/public/images/excalibot.png b/examples/excalidraw/with-script-in-browser/public/images/excalibot.png new file mode 100644 index 0000000000000000000000000000000000000000..7928ec325b9ab27d04a434ff69a3a60e068a727d GIT binary patch literal 30330 zcmXVY1yq$?)ApesDQONRN_R>~H%KFmbeEK*NOwr5ba%JXNS8Fy-QCT_Z$b`rc2n6k;l-MT-1ZL>v_kXb9lbNR1V-Se5@JBIW71z{*G#71^ z-kF}DCHLcC6&M&{OeNv&Km=SB3>+23O6+wj&!YS!bHhpV@G7wmCd&g3I&pXy;iw4d zGhkk6g-K+lo{<5;NLREd1@Rz^C>FDU7qoeEU>Ta5tSkQ(P zZtAnNoUD**LyKcGGAc?+=Euij^_aA{U6z^=mi8YM_dE3=;Ief^#l<|1JAcb{e&2ao z%+*@>`1lYK64E#tu(W{dD#j-zC7BE+n3$QpcJyg!@vyUFF%vgn318vRM5TO%f|A1d zh0w37xTx5*%z@+mUq5D15fQa=?GUNAh@x;Q41wR8zg-TDj#gx4`FJX-s=ECsn1BDf zUzgt9<7%PltBs9Ku(2TvelV_bc%rPHkC!uPye@30pgmoHd7%A$QGHk%ab8~DT`MIQv)6UH-|+D8$>yIB?Bm7X)a2w{ zm@WVRe<3TXs2Cd=#m2^Vu{^DpKU-K9dOWlcH5`>z6qGH=r}1#Se;@aSjWx-$-KrW| z9UB#;Z(-5DQpv~17j*Uf@G%GhLVRmf(U=4c@9OMSE7R)c%!A4bf>n+BnVI>Pp1veI zn=J73aQ-1TmjcNxmOSVxG$f?+=HOoqy;`Y$oh+)UP-VYi-(W@C)35KhXIEFwcfxPp zqz@A{%gObi?y7l_+^NdOZPnm2>uT3q6Uf9HvcUPy7_(BCT<%T5z`&sT#l&DCT_0|( z6K!qir!AW?^2h(eW7d5*ZhK~9#P+*|2LY9vlbvmNI9JDIyOc}1vAz8@$>6x&nw8yU zWTcZ_M) zceph`>wV3@B39Zix03KWv!K$^^;MiYaIsAq9oAIWd}T(|q28)_^X84&Xu8QEIScgV zNYKvKT1-TfOF|KyBqJR(tD`p2QThi)_@5Mx?@9{_%$7YkQ3Kt;6Jsdr=p63vyWgIf z1fFWL7YrwJ=*TDh(5%$_*ChB@oxx3JYG&qgz9mOnA^=Nm?_t(sjWTOuN>4)<=(18! zVqjo++nbvgCMM$dzWG7$Y9*Ng5e@0}n~Uq~W8SCkh%##}|3_}C8C&vcwC3h!`3!+! zH#BHDd`%U{R3{EFffL`KAFn+~ z)KpY9ZuIUkc^Duo8AeEMZWe8r&kyILcbEH^p&fQ;u2f;G#L3CY%bxd#x98ggJ;TGY z4-GeDSeG!zY&8n$0+OlwKRK+X`(vr?_3TmEU$RLs3O>`#-QD!m6l*x|P~}$_C#w9Y zzJAy9qq$37vGh)`n@7uSkyGJdqzV@n!y=9XKn*BjT5twdvpzt3f3Yh&Yc z%G~yF){U>1spkKVW!-@9xZT@YrPftRlnVbzcr@8sp8Tn=| zH~M3*uC8_`3UcrQ-Z6t%GYBQ(vSH9|Zals@%L^mkwmXa-UyB)^@WBtcLqtL+-f^jT z?bpGsiK^`H@9*mF&OZ4atoTxMN6KKre^3m3eD=Q+!-LG;xB9P11>zDC(z&Bk+DwKj zJBaj>3>>zG2+Yj3TZfGn5|~-J*o7%VztYeR+?SrRywBvYKe@gZbUk@%sRU*7mBV8< z{+W{EaxlBPyQ`_G*?&TrNQrWgFDsH8HqkdY2+Bs@Tx?6A2i^oTs4494MMOmf?+gRLhOR`#zrs$l>6|A3Q$Vsj6~(-`(9M0H+$ny%;zqs6I7jqvcw4#m6^8 z1kP{h6S5V>@LKP#wjX>&phg+pe?b~y*NBOUk(HCXk8m$@NHgUk2q1Gf9WLi@a=T^e z*+yf5f4MgeEG+Dulg3P5SY?-p=fY(`Jzg0RR5zm=1Kj2NPf+Q~bz6QN--KHzZ#f(r z%o?0LHt;%0Sad+|84!IFQCKqCJ33au0ru$S=TA_`;6w-@tFAd8Bn_33HVB_FV+Cgh zE+#p7cYFK#e>E^oNeet~#Ppa%ZrFi}ipml14gQ$=0?2AKG*U`Rm~~93gy6{BQBzPD z4<;~%goL23qim1b>++FC)6iA9EU(5*Izs3F{R{B-2jLk9Mji zXp0C=@(K#JL0X89Ys{j2y%h_V+dC>MYB+_9nv#+=yrh`M_Uv;>e!j01euMp%_TShQ z7U=KazcYAU(Cf%hsoTYWOY(p-TcTbuGBRQc<$C}3^XeX-^k?j7CZ<2urS{6o6TgS9 z1^52^5fc{fpjEcxM7s8qa~3;aj})|-t1SXuk-@@WA=^tjuCjF5@2Dj#90U{7(^p$x zs1APg^!A#WnFWDh1DzO2)5F;yv!2(Z)AH%58lLBVv$1imQt+B*crVo1$tlmQ836*1 zgaijUI6PDe>#W4It|pHID|mB%e@&uSYgb;->i}8;oNNhXiNL#V%Dl=aYjbmRNlBz5 zm%(mueS6Sxxx2X?{;MfCPUI}rTW5ju`nCygBo}VEzxUPQ3<5DQFc1+DarH>}+);oc zfoEZ1At530=(!uY>>?v0W3N3HyyYf#aq2bFKD)f^_5Ab*c3k82$PJ;e_HXAhy`VZ4 zE`ES0mN@ z4G3JDya#D%Xh2eEsH!rs(*UoZQAc0Z731G6A2AZPjdF4h2YI^PCgtV*-Q3JKnaxrP zGiuzF92^`BVx`UX)Ym{;N!W9L%W2QDIM~tPj+*0eIE$Q`kcbEdQeR)cy1M%EhMS6dZr3RBKr2X^-MO%9vLmf}#;4ywVr zS#WqjK|z&I<{NXbLn0GT5`OveY=3)ku_eSiHg@6%5f@k)76jecO%Y2=OJZVTaJ2Y( zlV=V3BcrpHzS%0gdSwr32f?~MoD!sk8Aw~%??41n6elOAM~bULRh{nEfxi@vsX3Hj z53^zUYF|0`>z>h0W4Fy>rfJ_%h3V>hb4}sQqns=xdeBX_wQvD6wIJ1YHAM) zUOB$a#AHgOipt9O2e)A>Q731i13u-^iLq&>f^H+FyAO-o$r9W@e#m*IxU_4it8)nm z=*r5X*3k_8ZKoQf1Q-AT5s?k`CzQx$u~uGD5hI$0kb|Qx(YIW(Ihf$#GduzU|AO{G zzPw=pljYyJxxMvkf^bF#22iZ%7=M%~etKnHz0kfjoC2C4g+O6Ak40t|GMia-Wo7rL z@u|X{#?=-bIXRQAyQv#KTS>{@pH80*tc{H?ZcdFn;6W`%{{$p#Q3*-FxAn~@$aA8Na}~Vxw(sV&?`ie3JQR-(^bCs&35!7=lVb9 zTEwd zBO~J<&?zn8>wRB6Jml|-{DoRVDwBpk;$l3E?*8?wjO;;)%W?Jm-29|UoEguoa-N9W zLW#fN6CaF`xOn&T)4eUobA{pt0%)9?#aK8v7q{CP?8c2@At56fyc70EvW~ynZ^P6{ z`O$rx?I|hcK9cAT|n4g~aOF6#2WP!}D zkdb*t?4d@vwwVf7O$OgZ#y3m2kWs>5Af26^_)OXw&91B#sMgW=ywi4i68@%gaIU>}S^e#5b$J6{PS-xhv3S&e0j zqdu=+TuK~2d}p6CQusp**4lYAogehfrcddYmzU?;qd_ucpE>U|BG`$>=Z_cM9rs5> z4Oc~^r1}O12LAj3ZL1*I1U|l0PdPXUi(5?H=v@5Jpeen0HzkZ26Q+PW09Ky#2u4z{ zoSLDS7#Nk`E-eFv8Raf7m(ndQ>4L>z?gl0h5fQgX(rT)!4X4tWb$)GZZ6QLUV&b?R zR}wqgPC*0RaKz`$N=mvsqvzG)xckY-$Y{-XV`GC}y&OQlpe4&PhGCECQXD)g^)*De zJd@4mQ~`yXgJgCywQ`F4gnn_MZ%tK;Q1gD%8CVNp2#NL-n^_fwc5uqWYTE`kqp zV`DIo*#_H;oE&l#TN^_|EG(=TkWj|QE%^>dfkkp3bkr%Q4k)Z z1l4Z(wjYbHJ~R1rx_!EWp~X6Vxjz%+@1K;Ca_w%fYroy?r579)_Ix#O`$u2e@@~wi zpv1~jlOlr=>{IFK!6DHGC<@m{I=P`NkA8_LG(vK6KYst_?W&0K_rEbLaC26sIz2zn zioHs2IIi}5xV^q6-OkHpV`GDwhB0JiX134HHsQ7`*DGhuMs&?T~D zVoJ*I`Kz=P7Lr+B{se#!gJv~6$Y@9_o3O|%PJ5G4Q5bial#~G(&K?$-6*7TkCx^Uk zem$VS&Ck#8iI9|%mhS292INO`MxojaYVG4CAKTqYH1-L0ZBmj3z}snQI&JEnms9HJ z+sw@#=h9X(X;1#lUs6eF2uf|u%^63J`5m>@)t~vu209nSv_hLbzMGijnthO!k(Dni zE-o)C1CZR(+B)?E_9lS8^L&-6tSnA+_UmA;w8?+}Ug0AL z+C!aB%q_+e#5&2d9URW@b)%BwJsvg)p!Bq~E#Ge)GHdR@rh;aSEQY7~yYC%#>tx2w z)crENz`K@LyanaKSa`}X-a$dzkK2BrJ0jvbt~yRJ#9j7mO5@(R2hioqTJotrlp>uOk{v4}X}s1Re*r`d=02L*Z^eIbPpeNg zG&RK|B?WKJ{kJUQ4ZoJ6qM}}W`FqPZjvBBZ-LUWDPpgY#fBoVy9}QI)8W=HHyzXFX zU1*Y%QHJZ0j3C~1hez;3wyvh2r{oBL^iPESvNK3e6sLF=rpqfC?$_22K@%HQwf zqN6b}&6?eMPo|)ocA;R9gBF{kemI?H#KL_C1_O+W<3-4aaj#lB4a_ zOgX8SQMXIGGv^l#`WF!9?d=UXv@A7$m|5eG1CRID!$U(mVO^pBp)jWkZnxVCl1*L0 zq@|^8Giih4m>3g-jEub2jy+SM_u4OR@&e@i;L~7y)&e+Y2;|}6;b^Jl{xm!soIfN) zMCpMPS?%u%QYtX$YIqWHn*zJ$w!X{=0Va$j%)x_KBLUCX!wQFo8B$Quh-}}7Bhpqv zGvUvbtqz-^;KTr^WL>?n-rTed2*Z1qKGnC$r@>Hxt(tMx$_QV%e{DBH-iW-(hm7 zZpwKsfr!}=yBw|AR{Hm$3F3`QuV#1F^vAfq*eqY+p!sq*U|+`3(NR+~pjd!$Lh%0b zbVxl2TCNl?^;tiyh;A7H0yU}9lqz*`yH;62!1ExrHrq2!874BHax*kCDv(bDz%@*3 z@;`(W2F?He`%hm(nEn0VbFEENm7&<+RDqxrLmbvgF);o9}?7-LY*0HG;~Zp zw5kAHY@AF;M&>`tf{yrj>Hc*OV6FVzY%MK|Q+dibHEhO<0{0qF@ptz3yVeYT(lw*x zpS5Mo?s-ND;W{2t(YL-ruQJafuX>%3lEP~;5a)6XeQl~g3woD}hszl~)Hke)o&#p{ z4{`At85sk7y9Bn+?jkZ_XQ!tI%AqroZ{_Fk@e{=*TKrGnMg|CY+{Gm-?6e5aY;EPG z?z#)>=#XG4ux-yMnyZZ4vuN`dmGIoY>4+yA9{OX;nf1%7>swqKJCh0n9PaHdY*few7H=Y$g{>{CxB)G zZ=K>wOG}GscgB)j!Y_Qp?}`eiprDXo?RI;2$E4k;IPWkcdpvaenb1GCx1_ij-3dFj zkn{c{e1M6Y>(X;dMTzWfpUJCNuYigW`G)_SZ7(G=3k zmiDw$xg93jD}xFR^+x<_G~hoys~Hve?YN8eNqhn9w0w<0S0G6L*vyQ*$%l7a&rx)Q z3~f)IP(;w9UR__Co!Mk~J>3r9S1D_Esa^_jlbakmis~B8^&TJZ>`3K~?(OcPd0HeT z>NUC(^7ICq<#Uv*ZU{p(;3@U1_XB|_{@WsVNK~1ui_1wHX5nG7yAsE}YG zwY}x!5&#^K=OAiNQ3~!Aho*43V&e{beE)7oOK{~=?RO#7)!Q4C#0J>Aic?2w1I`(t zROC#PI7=CEYiQFn3^r}*d3=7 z6m0W35?YS2I&pNahk?Mu2i0Hc#JH*V8pf6uGd}9OxplL^M;?@Xsp`5S zva57nM-7OO+Czr@H9rib7;!K=Cnp(i|rtPluh$m?+?EM4j{`6PTb7y zk@8>>;Nc;VCO3rJTZV*QH^RM=wmZG!1R-N%it~~~C}L%QH3>5_Gg*if0Q+T$zkmG# zSzdH~%t(t!lAWD>E3&tzrw6vZ;=QG=vImS(TfF|`-K7zZNM1aHmDN5Vw+A+ApypQP zHnwHkAOR#2RDO_atL0Sh5YS(gGq6yeoB6n)Xd8f3tJ{JlX8lIs+eI*m+@d7gtnt4= zz}CMCN_%hrz%1xd#`_Hr2cqPhOg^(88|v##p8^3-ADXj;99Dvpt7lXGoJqL`dcmis z`{P(imz15sq8&y9H(ZrBZxnS1kswh@Zv=C>rsJvMApYIU^CcrRTCpdAfr!%sq9UIG zmXSGJoaZV~$Uv8bg@dD(`w2QYA1%T}084)WMFXp#4s?NES|STlQyIWxZ0(#*h&@E7 z6?Q1At5eh3qCyJkLJe=m!wJ~+SNN1=wIwQSQ*^1KV`EE>JPR}X{ys@gs<*fhH~z`W zVV5qx7m~UdZm%akON#|S5IGxITgyJgK6)Hq zi}jjXDyc93ot{p?*SE15dEl3kmR^zYvY7S=WX8q8X%`X+4+>I$3qa^??^q5HZvVZq zGDl;)%qe2RhrFdIo^3I2KR#MThtM!GdJ#T^*CAtKov$SaBx8CUetrG=bt+a5->k8w z!spK^KYtP~l{NR?KGh?>?^>$zs{Fb2=clLhGllkjm35W+Mcn#6%6Wnu8|LE!!+Q45 zEF(T4fecq1=xo(Vp!WbW4fg!nlGOqN0)k=~p@0}HY-vM#jC*}uT?^O0U!K3RK(T5U zlByIHl0t?6VYVkGNJQcXp{gp*qh~&YEdw2gf4BXtQgosMWG^u)b2IYKpH?z2{P}m$ z7^K)(SZvM*8aJO*RqNbyu-LzYo5ld;awzo!J2&^uqxQ#-rzy-0pCcQ@7(;0t9LzsF z0ZoUYNL}dfxZ?P6V`QYlHSMIMFs7TvXhy(mAVQTfpxT!9udS}CzG<;LvBkmheAe`j zRDeQWn58w@eJnLCt@NJ*szhQ=bsS2V3IKJZ=;7hv@qGBO+WgrRFjj0#I=_td^&!s( zR+fdwkGG!(NGSAIEKb;GO%Wk>cNZ2Td>23^0jG<%-WC^2X~+4N7;i~iL!+^zhF|cI z3?ihvXmeugA2B+}3=6@)#-13Tu0QGzr=wl3m#E~JQcdBysNciD!!zAE4k5q-TicZ- z8nm#m05mZOgl<4u@W^nO+NxGX?eYp8`u#(P0xXfsxf79E8xSyoX65Ky>#Xo5nJ%Us z!Z12J?Xjb(w9;KumxHymM}0+^FLdP>x< zA8Qh7HB(MNKmhQX1z>gT#6+~ej~>}7?i-?sVZ&2XQ{yeEiHfd2eplQw`e57q&{@eJ z9v+U0LJDvvkHvWI!p6iG#=7HYvwlLfvmfKq>jq442fsz1^TN5p7okP+KoU%45-Ke% zjT*=Tk(=E4KPL6CFM4TYeBXmFn$D2mox$k-zD3aC_2GPPCvQnnk&|<6sEpTvEKysg z*k>8sK{-4K3uuTVuJW7XgdpC{J^JdXH@~bXL_XSS6;vqPlM8$O`N7NWb?PVfTi6-4 z2Nj-EGe1+1t2nQy_4_WFa%_iugp|ChDjMegUiDcNv!I}tjyPS0ccJf=w+seWwM5wu z6TjmqZr6*8i(kK|OyMB!<(iSs@jPMXG+~ppQ53j8@baRdyy`ejUHCp7Du(h18ypc4 zfwN2G97SwhEi2siPT$6Kk2}Z&CDFX7!uIVP&J(CYr7cYW-fJFt4Wo6Vq9N~MV#E~F zMI^+Al%vqf##XVu>h5Vmyo+1I^80~nAroZQ zD?RGOAeUOoG8R=CQC3h96B1&0M`D}>j>Ruz62ip9RUD6tG(aT<#8$3DBMf-`tn6%& zKUMap7xM^lfbhUW1c~B=1%eWtkWgP!V`Wty7sWBj>a^eR#`kO|fWxFB0SFu?`16VTj3!`M0H6sI-YyXM_Hg(d2*pBsPF#eASpe&ojBK$EV!9Y`0=bR^62Kh~I_k;jDlV-I)$u+1xOv9d&Q&R?p>(}SZ z?)BFHM4nYKU!e&r1yrX#2Y(>9ijIo|;sqllvzi4KkA;u;4-$UZeYy(KmsDhKPyN(TfQ$6}WD`;yX=F(9ElChdXlyVAL0jEja zEtO0!%Ed5t82d3Sn~r#>4Fe27-5~9oHSdTUISs}^!l1v>bFX&TTr5UX63|$rDs&$`t5G| z$DdTkUJd6lk?!u-Ut5ctn%p$G0DP0Rv~zLE5cKk#{1H%jiZ5pS@cfTku+rXBpUT}Pb?DC{a}r2_I1l`UOIa_=D^5*QRxU1T zHB>@EB5c~FnB1~5#_WRTETH-Y_j&hM$gUk6G@A?t-bn5fT77lBg^zdID%xSf!cv8W zPy{_K%kr*!ri45`UN5Wvx&<12(v02KP;&mS=1LUKjOJ=S=G>OLtyv|B{nhpL?alQ7 z%}J_33u)omK!7V9rV zIW`Q|U-rIKq!D-luBA^XLltnl`jsB{&RYloe1g52Qq6Dv*0{B<-ukUkOWrB_QlsZ8 z3^{aQ&9GM4e3V>sSvS$>;hG{Y82A(HpUo){ko2a}bdpi|)H z=Em38w^ju~lv%bYCOZZ>`bK6K*KeQe2MF$RywK-(=5d2 z%@$`SiYdDk_3cUuH=`HWGuTZd#J}a9e-?x zy2^*8pEduj4Yp-uC@c6!bM3)QN}3TdVh-Kj!mZRMGAk@9v$D>v;}5#EhT{3kB_ITc zgkbiI6j|&RZn7YZrRW18he1Jf2j9S86*w0hos@NTZH&&Va6cJR4Qc_{=(O4ykUy9D z_iMsbcgybQTMPUgTSi34pO){ef8BB2(WR9W2ZdW=F93y2KGhjux7J`zAw)cXw6kMv zX^Dr4DN$yglzTCGOKs-EYbclSa-N5Qe`dYs$2BQKX~`jDgprBK<<04OWV2Fr0WKQk zEj_PGTs>6^PIXAaw3;D@rih5|qLmmqhIMsmM}NNs5Eob?Trt^==5QuM14Tg|`AAFe zZdvuR6NcHt=)lgxtk;?fEI6A3xHoU*P#66(@~TX3q^$Rry>`yq`?8w0>r->VGCeeFKiBm)CQLUPY$V z;im7OjiuK}puS1f*-^X7C78%2i?C0pF7Qoi1%8x4DPpHRT4)k|j`5H|67@|i{~k^= zAAwsF*(st`1)G%SqlJzSHo}C4{`yE@W?G4@tv-Il_^4cvkwsN-H|;biFq$^lCX9wu>+B_D426s8m#I7pT(Cp$YNtEP^~b2m2&6 z2@e0JoIb~V-%oBnN$PMfW5u_9~rj~^^R2}h(Kuq z))ENhcain(L>D?8U5VkhZ*Gh_zf=^X41>sMkibPmbC`56r=Yy_qyk=#zvP7W+uJ$Vm+2=bc6Ymilv46}dF0HW>#Z+##tJv> z1Xb>Og>k1#cfUZVmX;z^2BScLVB#0#sQ|%KKhIJ=CLsZ2368bA=2X!3z6S^l^7idp zNYQ&m9UZyn#E^%Ljja~>T$6#}pByN4shBXFn$x0=A0d$3{5D~*G8#z}S9Ve{4TLy2 zV$ThLY>NgcrRbjT*ar>?1KG;Prqym07NS3}p>X}1@)aQRI@%W((^^yb13g~eZf$K1 z_;pz#LfithMxvr)^BC!4v%U+vyWazpup_AwhYJHCBm|CvfdT2eczE=$mwT#7WUOxE z@G69X1MBJKwQ!ekJ(ZwIwf4EeUl4-LAmlXW)<3yk_L@-F-(4!dT`~6w3PS1?AA9R5 z0E|k7?@{Bk*RWg*z2S7&SV2;<_7Mon&#p!;N_fZK(+5EWc+ah!T-A@4*-Ie((AMZBkD2r^q=Gy7`$wP&ZhleMf zKL^(f0~d2_QY8rJxn%{@fUOP|hXbTkU!Roxfcx3TD^whu!ZC-f(VXXYc=$W=Kqxw3 zB6JvZeoeM}*EbU2UlZ1)W*M;C^DzjMK%v3XU4U=}=?-b#;wsi8HxJ4LsG`R`6fb4$ zx$o2S^S>J#li25~YifY5jbfChG?(f|!yXrxu(`Db2LTR|_Z+leixYW4x5osG6d{O% zaV3oOjFcAq9ax-|YY)`+ee1oE25;%e4EZ~%tE+!44+1M_r${;AVVRhiQj3CJ0jbug z#xnAqL7SZF!%X?aH^ntkgjA3Ph~C}J^t9#c<|Qd<)!s(5wFz#YPXkGHBt!7^dVl@2 z*3Ugv)v3of32W=?^UWRZM+XED@9oiy(KO!cFy>Z$D-pXdUk-t(MW656dxZR|MUw_Y zgO!7WaIL{$;1nl%H;%{4%S)T6$J=k}tL0Law6(P*m%?d(v#XFH5P%|i;iae+7Shm|Rx=`uh=^a-CKIrmqI$rPqnd#g zkr)CS%fO&dj5jj&gnvnivZ8dpdNNFM(bQ`#a30A}=bNJ1+- ze4r6Ho6G>Avow01zp-Q#HSN5yU;nlTb^Z3ufx%~LZjNy4EkhEQ`}m*J#qJ^V9~P>r zaY)}1!^1DA+UnBZ42DDlr?9z&#V~lYfH`172D=4;d^H_T68M6Qay&%!>C=S4>CG!_ zDk$Nn{5RxWAo5$n3B`Gf(CJ-2p*KDvR8>}*fB9l^jjf}t4OAr$xlzu^K=aXSTeYs2Zp2-ftwsYoJ88QlclMn&PN)_k4uIqXRMd%fV$8#F}*Mrg3k}J2@+Nxz$nU5-MFfVNb2f9eZBLtTt=5)kI(Z%|4_r<@$oZ0 zHXd#YKCTM=lVe~}addIYQWVtc>w5NGEysT%kwi%coOL!a{!m3;dB3#GN1Kb!Ux5Lk zGYfnf7)@=KTfYGP-|Oi+7^Ub~p#JdT7TB_rFl&U6~ckCZ(icUhp!hmqc6WIjp@HhNC?Cj@{ zktspcD;oxr4oTU$xd%^O)T^Uaz&#(j5hU8(^o1OyYCXkzks42NebQ0B->{fXbC29f`7uG>K5A>k z?&-76(0$}-I7!Asb=0?9pzm9qp8m%2134jI>?XXtynN**DgMJpWS@riqF@2UWKKlJT9k}*4mvc zNBWkAwVhM965{s|UH zduPC8U($*Va3#WGV`BrKY!cJunJO%P5`&nUn(3fK#*qXE8kj)8xQ6;# z@C&rU^)8N5(o#U=7ut?Vj3u!(VbwC@?k-V@i=RD_yC8e_Zpmf-N9e+@^USKES67b` zWeA`lRvt=b-v<8nMKm356^);@(_ z4`pRv3O5x)vdQN9wVg}*9S9CdNUAE#Wj0(1Ww5}@{s)Y*8q7^ic>z++f4;{6?@P@1 zC+K?e0d+`3$l$@qS#@1qH?{Fcz{`zPxt7keVK#Z zBo@=TQ$Gy>sQ^5u-@^mhBo>XRsha%f`FqXhP+a<8s&wefuG0V$D@*YAQ>;2X)CSBe z23XFE%d<0)GK2{Ldm=y?_tSz^%~Jd!tkUd}h=%fHX{DT_bo(Em)g(cb`v{8|vRN`t zdehLpeMjq~g;Ep}9PZNrs;QDy$9EulM37CtfLVI#50q$i#K=+eHK&yH{WfCnCNV>%590wU2V)mumeYy%<4;?O@mRN!0L z38I$LGK0a$%`q|D*jZkop)xBO*&*1}p=*c2XJBt%1_HV=v#L%hM=Bm^E($^@Pi37U z`sB=kPv_9HmuJ(^)WjswMVc_Oq!684_FY7cwtPohR#sMlt*n0l*c&bMeA1Vj0{8_m z3Dj(RnDP*MU`0wJ1|q$Bsb`Ype|93Sm|y3VdD~2F{&CHp&%|Owl?XZ4;K>(g?0Lfpur{t92A_N`m2z_x+wg+3512QwWvA_ z!3-cv=1lt%U@S;sq5*F=3joO_rKJh+-Mg}nCt3XtJO8P>xrqxKRApy3mxeoj{fffy z=EhT?a}chOko+o{3=>#Sy;?)EEywzAkA%SPQ7AzBvf z7#FV&oI4#FV9ba83ud;_TEv6L-ug`6V82!b`vgH=URF8ag*#zXAF*zI)i9}8STN!d zhxEbq5P>Zb2-!K3k+6`xy?v#@cjULX2x6tVg+l2+DuA~*=oz8yd2@9;#r(@=t$um7 zFJBM`Nao^>DWzb4z?4ByM#PJ35}j38_@7_xMFV@joH%2UBM$zxvxsukwjzqSsAx+? z+X+5MQur@_09zY91B0}j*VoOtvR3?t1{Z(9NILqG8;5Pqkv?)MY59PT)vllq59ULP zVAuy0P~FhnycXfIfbjP&iSfQPc9z97wC{v7+UBRG?l{@+j~<^_WN1J3gPtDvG?gdV zz`j~qTD>r|wy&R^9L^DKM`++7Dk^H?E*bPd0;;L3T$%m)zgS^=>FVFVKRY`EK72*1 zcf6?;?w>w=!o$TTx+Tl6EQE*XX9~_u{&7w4BUz{-He+?(LcRa&@`lg-2^Mm~#`&cB zJv*R@)~r^%_s`ViWanO0svB7#0WiDxMMl~JpX158uA6vXGjWgrrjfu1V5j|OJ}R-= z|IgydDuIYituZH;_Qx(6gupf7YdE;Y*n$xl2<)iNR#HDzbAH`aJCzI=3(WSXad&-Q zU5}@?YubFhvf-%@*gBAO9A&lT<#n}s=8q7Nn?BU!TUl8^clMG0`w#V^hqfMDubaB3 zhf+mVHAqrSF8$9HaqQp9;K^E4h<9*sUmUIS>?1eoxqhs8|1YO~=c9#xQ&X2WAEAlK z*5K-2LGhI-2@bBb8_imI7oC`>@aOyY?+mJvIb~&!rxSmJBGa+*z{u4fKD9>KG`5dY zc9r67eEbQ$J(LiLd^%4rOe|Vtux|%QE)tP%hL3s;k^s(;2C^BjkvRZNKPjRi>M3k6 zThmvV)VTL%G&?9UQBqlS(KUS4__LKN?xw<0S)>1vqmM5!iW9KGP&Uh>zjA-N(!o1? zI&;7-V>~>hdTlf&Auat$Mwy?Qs}0x+nA?Ozb$$eVA_xxumVCNJ5^itGsS4s83Ifl( z8i)r8>qClz1Z*G(Uq7RGKD;k2E+&M$rKaxc9Rx^DbFqb8RKd(l-C17OS^mvJDLxj~ z2D%IXB2^Tb+D3J{_rlD?jOZL;|ca9 z-8MI74o1JkM3?M36H!s4PoFHsyY-0@>uPJMsPZVN@>1c{1>86uB$|M_?9!iQ)GZRyq zU}7btYOK=s?Bm}4eg^ymGSS+U26Bh!g<|L1tfnSSIk~dxYAguG&1gp9=6Bzx+wuD4OLBZzL(+zRjHZp+7ocDk&)m2Ei=2RIPX37M14C&dq84 z<4<|w2JNJ@w6uf6aX}Ao#haLzD4vkE9phn>JNcDXp7eRiX9TG(kdcvrCRmBS6BU^+ zRy@42@&t&wwu`?IWMcc+Pn(j;!0YGdoA7-jW*afI78j2%4t4l0f>p{Qq)q^Oj&;4lMZ^Gd=P0PC)>c;Y&%I-|tqTIB8E8Sl zp%DQ-1qIa38py!qvSaV!()K?hX-uTITRxZfHYq;so`LV|;|cp0Ml^`4rIppaJQU1F zUS3=@x3mPE4rZjK9UdGITFQ*c~_5lqElpDaJ0a*Y%CNr~<+z9vpfYm=e zJ?Y%`*_D-1Pcnmv%s^bL2c@^Vnot%G4m6q%jNB*lk-5b+-R+hcj6lr*2y1L?3^+AG z*MbOI{p{X_1>&F9H|O}q?Eki-$Ecq^0GW!Oo}7`1N$t}oDn>@}q*8D1_Pwc+$A<@i z<%1so1BAj@j-+U88nFKX!zki86AX+G!lL!!FDFp(s;a8~BzoSgB6)Z`cA(ffIySps zGeK64{Cert8AztNI-J*ZTaFG6;30sI!Qo}71M^&C>|f^IL*vv{vZv2rfKk5@$1yE3 zvZAi8t|?E|rFLZI`99p!b{nYUU_RHV$ABP--~CE)nT(uVba_b{U^OXp6v)ZhnXm9h zSfZ#0Pj92+CJ$=9LPmrZim5pm0sgilS6tfco4hW~u(5S`|Gd=5EB(2qsK~9fsOmX^ z7EElPbaB|DmCWVTUrhQY^Eh!&_3fBB+x_CA2xfXgreM&2qo_zqQB_BI+{4nfC!7Fu zm|zV95y2V;Wn^UR=($%EtYB-U!wcSD_uMP|1*6vPE?zaW{a{8IPE3o19Sn*E1&qm4 z5fBmrrxn-&kh|Wsh-)`D5a0YBvZg|)^aJyn1}m$ppsh3Q(~qS@_}pL<&@mBZH>o9L zqx+QnH4z3#h+UFzk7b&P^|z3$lZO)VoM z<<5Nyh<9=_K~=TS%Y@Cx4zQ0(GCPz!TeyIG817ygsXtbak2kODd1Yi62*i}cxVZKt z)jXyBKwX~kq3{I6PoK1Uu+SJNcIM=uP=YGf379T{%KjMGJLNpF3^NLF+FL!YSHbL| z*@uhebOk*HB*dYV(zdqeuAtW;iP%)K`e%Vrz&@ST8W7;_!a&`)-Gw%ilSc#m=di5F zp*t2)y>4JFnDm;M$jJCyuEN38QfzE|eDs^80*7xvv~oUK5x$Eys5q%_Xqadl8XP3r zBLT|~=QkidZI;8@wV#Vardw_N>Jwv<0gxORFz69MM#>q-xLq?5HaI; z&Cg%@?PBZtT3TLysmcf`JcN6PXNyXDNiMg zWYunPrX(iq*`=4Vw4{5K2@E;Ze~Z->^{|0N#!FpMT1wo*qJtnR4i*wYNl8hg&VN;u z%nS@X-`vg!iiwK>Cca zIdI9E?TWZWbuj%whn~$`dU<32r>PuNVbGoK@9rLgpA!K_gaCZJglqWm3Kf+YWJ{1> zU?4vUEst!NvdPG*tH(ilJ}K#%cm@Q@cw4jmx`dL_Skw^)w&-W1#8`5fLtjv8)(o|# zQUsrF;OdBnPC82XC|;a@FB8}{wo~nADSB=8E~nXUkDjy)d~DqBi6Jaj63^7g(5S@3 zOU;_IohoGxey2Jx*8_g~ggO)j1tpS%cNz>0d6EN8v7}3 z!9jjLzLkOM>X__H*THxEX&w}Le?8GJ!Y-k708{k^fS(O zPED{f3_cJ!!IXG1|5rlx$ifQSzCmVxGsK^twZH`0{58s#SsDpJx>H(eaRCWwT;K~xgLHRG*ByTMbFZKM_=oP! z`_8;G&pdOUbI$AE(gK_~?t}r{>M&n_x7(W&{_K5;+G7#F^80tGlg8=k=|}ej;HXfe z5tLl?6PRB7i}Aw1rwd^M5HCQSXhj?i+MKG(%dby|rGes4?Yqf$onPkX+%S?xEW7|( z=M6?~<;OY$IO?i^^QM8Ih)9ql=M-3-|Thxe9uldEpEQvhbCsx!tzk! zhUwzKv=o=ke0w=?=(K3rab|(} z`IFMA2|Hj$jUp-UmzA>bUu{!OF8s{P+Zu54y0~EhjtjURnqp$QfP*TkVQ83`a$n!t zn*A~RHw@+x zXOX@+z(iC5tUExwZdNwq0;Uf&=NmxE2E>W-p<#8t=bBP|Di{*^ImLB*3UYGZQTyF~ z__V<82BbS-Kg-rd>(EXuhtq}bn9%Ai%)d>He;DB6eA-V4(5Yz^>O+|M<6%H+zzPYd zRLOY_?QCs<*Nj*|hD$1uto#un?`W;2~*i+9kV8uY7g+WS6549+!LCw`p601R@8vtJgx^s&0v(FvDKNGnYpOa zZic;8JDxz$3_fdFgM{ELrb@B+a18B`9k*nb20ARS1L@#rRX=-#mfT?GsP#>@4i2#~ zj81ma!tdVCHq#H(!c7v6m_@(m=Mx?vW@c*lAav+FOuKZ?H0iIWh-P+~B1T7D$BE)! zawj=W*B})anKVM|VdkM**=UTvxW&Q*WU0VL2YO8ZcEb%9$YbTc7b_GH{k3P|xx+P}1mzS) z&0Vv{vn311=;)6qq|cwT4ica#Fju`ql$J<;wb|2a#LI@6+q^zin!G3Ci4>7oTxTe?V5gp*ep87$UGOIe&L8`p~}Dw=I4 zIKf=82Xm#2VU%kZ04;XQs21*Ga7M;X!14$#-O_o?L-HnrP07I+;JWAu3vwgF$<)-f zCM6PSFiWt9j2e+cfVMgR|9$0a7cG%6V-!VF^2LxjDe2ESb34%Pjy0WnB&sydBcwVG z?#9p8x07Sx^;WNz`fDx&una1`cdx_m??wG_(19q7Sqa&+5g*K-h}UmkduPY!Vb7M8 z=AEr3q$I2zRGv3AT?2)uiT~d8GZ0$sK|q;dz$*npaRA1+y?*niAf?^N%*?o>y}eyV z*XP@K$J2E1z$JiX381Fg8z9^AWXWo1Aj475Uww$1Zwr(LPwyOz6Y?Xel6C4!sDc z!J7c0=6c+20FvsAY!4$b883Y&XYhUij@B};jYaN!+^g+qgoJaxHxmL-QDT$CB6JPO zSe-`Ewv3Rn$V7kO9A@TE+o;)B|PgnQpH@)ds0RdeDjouW*nduZ&02~qeGcv-k zav3TI{!iXQQ;2ZDrLZqtQEq*c92o-ew%Wcg)IjEr4mNa(n9y6jH*a_i5?Np^Sy@?t zN~S*0;rzrNmK4P9M*+CTwmkNV>go_UDp_Jy7P@pW5cl%x*W$f zaA-&kJX=|&XJWYMz*>q%(pL~b_d{U*JO_9sp$mXPGM+Osd*GQ zH_i9-paB%U1Sl7ub@%ijK|q89)l79eQPZ$dKt8Qgpcnqae5< zEhtw)px=^ywB+=EGeD=KOTDdAQBrGtSbAm<8y5SQpNW9?YU)LS&QDhs93vN-B8n;o z4lx>ZfXxCVIUWqTFM$=0zWyw@UEF`VonR{hV-$0J9iBF=&EGrm@lo$l7E#W*|NXwg zo41TaHurr6{V+?j>es)wzuuUD&};5ZmjTmTIK zVw4%$&BdW24@L-xfJx>6rsVfdO^?;X#xAWYV=!}2^#DxfuEZs)GMlH)XCDEz20U$b zuy7LqP~VYPkU*6YIyEIi@XU)kfh9`W9|)&G2MEBD{QDv<($UFDu`*b9XG=BZI>H=C z%T1N_fbPGb7~it{v5odZnDag#_br;O*nb+v#&5fol(0ZQp)oNHAP1{S4+HjXUTX#(ObPEY{s z_BFoUWgd6c2?=iDw#EF<^v8>y{WS4mUGSOZj*5nchQhoh zAVP0|-^R&_Ed?8h7Tyg!WhEs|UMEe4Kmf=KRE8h~FoQq^fA_vn^-e=W092=0L1t(u zxoKWDP$_|L>U?=*3bwGd$1l0Kx(1&X@9a=M5)$RnL`aChX$^>mqoawld7nL+$JrK^ zmWukF+f!)28?zeVDmS6$<_-mS@HsFWRbtB?Braf3x51$L0LabcXsgWxG$m`nGH5O| zUe>hhA-=?j<7_IcVhnmfyK=hSdT=n76tY!NT2vIIH{c8=E3Slu{X&Z_m^vV!{jVj< z;hf4mnJdcgj&8`j9}D+JjIY zuLi1ryd3~$L{G~6Pots2=ih)f052gBP}q7LV9-vg%-TRixP0E^bJU$3Dm}f0o21ZC zQ9yGkEGlBTwxEWX?2MOzrd|UvFKQaY9{kv zGhC=F>EKNK84Kq%3#1*y(aA|NFxF*YI9b3>4XA}3mq;Ee^*?{2p##NsDI6!L@p5W^ zp9TU2)MaeUckn9QZ~;&VrDbmwEmg`Enu&_)2Zl8VA+0Bkdt{GxQ0uf>-*sMgR}*uj z7;`Z*N9uK&_<%UoYd*+hHMpOtr2PmW>Ox`G| zg=Bhwy-psi5nNDKB+V3YVqIAqcmJk`j`pInNG5xf|H1v@4q(8Bb(XlGG26$+==Fd} zsm2&$p{>5I0z2i=15csSLLInFbnJcp>9%=tva(3?UtarzdLu4C!YSh0sHYz4yx5LZ zHGLM_A7(CC2S-_W!ct(QoDy9HdrZ6h&x6G~xj3b|a6kpoZo?zH z|Hz_+pszeH@NcrIq+H2)^yEBv*iB3j=$Sx~93R_T>~r6$mAz#5W#1**B(#FJZN2Jy_*-FN08Jg{S!`V$#wmVDkeJ7K4`9 z6D;l%!)zKjRfzh!cxett0D zfV=(KpXN6e)xQdws7bu}%|jCXvd2Iv&e;dKe87h8N~Q9-c(6j@JHr7(8i`L)_8@_k z;^mFkB2#Qd4Yzl8UA!5!g|~Xj^Cs7^aV3YpH+-!K%HVyyz$mG#3Jr}JXJ%=<+F07F z`Fl&&sNUZxYe8EPZPIYD(_;r&@*l(P z0w#!GcJ$OCUGcbjU`^yWsah(!ywP=eGc!MgkDuDjURY4bxbCVO`ihp2K^tK~jrV)X zlJF*J+Fh!-Wj5YFeHX4D#|N> zkGz5$_$bJ~_N$q$AKdU=Xz-GHudK!iftKX>MuEF9dAy69n}D;dLCcCNElur;szrqD zO|!eWA#Y;;`t!06D|={M-uk|pXBZom2;&$uj@ z<~B1hi-VWLp*lKusB^r~EM)e|;9!lTYoZX(=GGZh8E=cz* zkjA2~H8f+;dP7$<)YaRUgY%vPQ6!McBwzkcM3M)jAglJWF_~VPMqg~frj3wr1q2ss zaAQIEYjZv#Oc~aoBmQ_L8P;$^MU(AA87nB&)g7<#uNZva{2VZoXeGQ3+z&DCSJ`Tu zStro0!Y9xP@W~Z!nrc#7^lYm?Px(_H}!5?(rF^*)zfhM z_{;TF<6^aJCAHb>6}p;R({k(`G|CwD3Umia>$C3aq4ggbx(;$!c4z8~OTOE<&LvMq zvrnOlvDDXNZ>4+Ax8G_Wvs;7sjRMyL_9I)t!N`!yJx!e}(Frs(m<3Jkb9>*Inl0ZaT~v`D#6w-$2LRvnT` z>x<>lh0&Eu=K){zJ8T6`KvCGQf^Z&kpR0r7pdB!^q>K&sm-zVNyLZw~Mld3Cxbv`> z5I0sqz_c^;#OUW+N~|XoupnDRJOjSuzxbQ|(*gEM)~(R!<$(s74Fh^^0K>Af_L}gk z8*Fs~n%+c{L*lsZf#@vHPSnZVuNBt++d)6&3%LVGsk=5y)6-eNHVeGA5tBU;3R} zR>je1P6Mg>9sC1$;yi2>#(f{J%85qCkUA!1ruA9-3TigZiwAS4amyEahi}AiPao!1 z5Vu#S4UY3s1Mc~1;G&;_-1ub(`p=6qWT-v|zi7~CM)1JuuBnw$G!u9VOJM8OAfot-uF=Q#l@2!+_rQ2FdmFlPa-^XOeX@AO{7^w32K z8;chQ-M_hSf($FHy5*!l9G8l{3KI>Pn)iwDu;fLSU&utL)X|_HKP{V=Z^#IaKyI*` zcH}vz$N`?lD-U^getsihj9G3cvmgjk`dS@-;1}uowsPQUyJ^r6MB|zBf$6Z;_^S;X z@1v(vz6)ytgJUIcOBz{8=xf-hvVb4y3&2^e|Gn)_Ar`td`-KiqfK}#4aMTL&;Ad<( zx=9B>D;HIVHh~CWEqS$;2rOB4`_q@rC^DXWjOe&4{CtGly-yyn`OhNlTJCfflnzw0 z#gqGk)$_oXuLnpFUE#Q6w($9TR14G?rQi`+Q|I8_9^6`_h&wjZ5y~1-ItdE!h`}1! zZZzj>QUsBG59q;we5iiLe&J4sjQF~zJC?K)tn|;1o7Ej%6`ABS=^!K(t7eAdJRMz?eu`zN zQjRSn_1nDuQvVA%5tyldBI?e6CK%U5hXg2yBFKRkUyE#T8E@zC4o8zT* z+dz7_!iuKe#Fdq2*awxCP=ChYuosMl>&tRZO@`*YsoFn@QU)!ZW?*4|`HxqFmn3!8GCaM__Hy!O=)WWZs2TWYa@4#kHF!anR5y*lk^4k@78i zwbL&x7&}wVJ)lF#wgNgWzOoD)9FKguhW;<4r{GV)+xXIEddcoj-zZ=@(s9)%B9PRy0uPFZA|s zcJ2eT2gws-n)=Vuy962f*J^5({1i$>u9M$&LD}@s^5qkFkmStLZxeo^)z#;99McaK zKafVpdA|I6Se;%I5`g5bIa^*+VN~M1)<%iOdpCFBFBQE|ID%h^R%MqrgriV++!=;2Xy;+mBarC(6&=WyfR)b4sX(8nxEqG2opqjXm=1wAA0mg30H=_TxU~Tx4_lj3`YfHZ;xNj*u%)PJF+jR zP+zXOk(;ub`n7(k94EjtQPJgKr!Yl?qvo*3*k3x>cHlak=CSE_W(Ks*W02!=b&wr)^3KIvLV5PqKegHgt<|+vo`jpWN z3!96F8<(E6Uy!_pSK+;ZV$t!3;8CHn&aW+}B-VjA4Ji zIp)=FVdBT_EdSP{G?3Im!|43WO}6R*I3x$2uaME=dh?gUL6;i+8Ky88GP#kmlMyrG zaLn=IuT&f7*@4DyrT%C*DWzZK-&D>VoQhcy$&Z5?+q~22>S}a^8e)XzjFiF|@WnKmh@9f4H%#_sB{JA5eQ;;zN89tG> zDudlWPb<%)KfEz`Zf!i$9uoWTyZOtk>w>1-hof9A3^DikOK;*6ErtqpcK^rgJr1Zb z#YRP(BmdyHUGy)}9(3%IzNahE(tl$v_yzLvHN0*|9_q!}Ej#`_d2-=_={CK?-?e91CqU^i-$@GSliM;L5=ihCA$R|CIf6wAQk4)p|>7>Ii=SXtNkh#4pQZ~13!$G&BxH*Sx|#iZtq>oD<_Lfd_h z`QoOLzq!mK+>iQy?Mq);YvFEYUp9;jg?-NfoZ1`)#d*fXCkL}qInL4`lk6`qN{M;_we!ZdNwG= z?rfuUJb{Dd#p&&jMfUIGm}%YvFXcc6!p1^nnSi5iJDkoer(L&uK@Mq`lV zt1}QCl{L8w;`h#3=02F0yaH!*R1`uucaiJ<;$;U07}7u1NkcC^`YR${p@>3Uck=t=ZblYjh@3G_KOx}`RW%Z+$ zlpA6y=Z#V0MNA@#z{e-i3XV2@dn?9JytLtAw>CvEEZ{qvn;7`c~VHMoN)MHl6fbOCv4JYG+gnLGC7VYA8QZK56c1=GFhW?hM6VEMzxkK5egGiq7yP4b!y zplfU9<;pT0n0_$C4QH&gAxf+&b}0V+7)nTr>d}j!J_{CUkq~l*{yG0qr0zZ(sAy2z z?)S1Xmj*QCHe=c_^u4}tYGXbQ{e>r~wwc6QVgb%8bieEUEP-~SpahS3R$EYga%;Bp zFYMZGS&^h4#E3AzobPXC>l=|qCzgH%3yN7@V&gxb_PXm#x}C$ZamATYs|fL}z_+s- zSjqczz>noi0n6{)xG~a?#jGmLxV0ra<_*gCiBH z8Ip%lf{-(2ayr4rh847bi?3uCw5q8daE|BiQLsv9^L^4-r(xC-2tHz8YA0#mls5{K z^ow4Q@cI)WUEzgupHSkVY$(V!p1#wpqxq!ya^uT@v*?}Uran$_G~ewX^?Wo)ecHv% zSF_@wz&IAf$Lp=Ls%#b)6|(>R7&byX>$5)D^|>KXcwG}NshVu$e*5*o7RH4Htsoep zVGYKDOYg2>g(?)%aR}wyD*xaH+H7Jka znB}`NP!~9FQ_gZ-Nw~VohM)Q}{{jOm&0Z}x=#9~XPUr#VJ<6AsxGv~2QRMQ=db|&o z)}^BlFtgtQyPxbPg%@UAzETIV#CrRvwe9TSg_ATnEFl(OT*}UAY4_N?o+}og3gCIyVt=`4**8#bav|nq z7_CsKn$_mry@r97aAbVG{~{L_p-@s{e`h{Y$9H&b{c-lR2lfOOBoV}v+xbj)&F%OV z1?A)lqTPacUC*I}xaFWPW6+rIb^yC!Y}iTf__dt<;-+w5>+M;bnty}G$c}azx99BX z`m+FcaT@SMg>ZD&@KNLldK^AwHr%z?o)R7!F*4R4R&zAR1vd0JHq3iH{KpZflkqPT z!(MBks8Cw+c2C)1IF;*Wd~$N-mS@dQ)PR5!A(Q_n%~?IYV;NdOY0ds2h@`zjBr6Fs z1m`aefK6j}tBPqker01q9s<^@jg89K`}(fWmS`{f1SxsSogRQV9sZbgSy|Pz0wpb>kpNGEpp^q`5TNjK{?rt022Pp(-q@bMW_ODFpywrIp;8+S4y4aMPR=Q>oLpU8Ko5XChSy6R@IdIM!KRXv ziGH0pguJP@;94iT6yD%B!BjG&l}z5eIAr|Ga-~K7>86^IRQ*H9$0^Yl9TjmZTumNb z0mu=M*a{?}p)-J^yf5ENi1x=QGUaWT&}b9Sa>EXEX+Q4<^vC z-nV3RU_GULT^W=qUL!{DU+Vf-WFYbWMLN&&+bh>}Zxhs?3_4AeA6WSe4&Zm&4Fk4B z#Zvz!@1rv{crR$OZcm5!hG^#Q0*$UWZvyWMwKR*g7d@^ir9MObab<;>aujq|tQm4;n?tv0o@1lCw+RW>d0b<`P>yu6Mw51|Z9zsWYu*R2d=qm;e( zXA4ULZ<`91hDx{HsN5?f*tXo!uiD;#H~GBfi&KvP`#`W#(aGjIqO8#${fNSO@o%~* zrt9#cX3PPVgdO{gV~vj~)loyr?fFtfwe7%39dFisAm2ms0J%!W1L7))t}0tf!986W z8&}++f{t*22|;B{0YgkZHZ1K2!^0pEf;`PLBYT3?ub*uqpsNZ$GnWWPxkHs~Ezr^& zG^-0U+m5J6300Jf%{`4sm;*uLF8g+VWi}l>U^nNxUk*1OIsN~}`F^jDa(`(dpk?$) z>b;wqTb=w6*?yZ*qLy5}-Jnt9O7~TCuJ_e^V5QCr-`*DKw$nCl;my>5#iqc9e`=nx znK|5iDoO_hkqpuPxSM_9S(Hqnwb0eN2sdu{EXr=Hy;fx|sP^KLJ___~FHqL)M2^A0RLStA)eD$0p{UMh37gbkW)DY#SKXPhB zsj(^urcg88>Nf8z{9hEZ&->s+Ol%18n$hd&h}?jyv)@*jo!zFFI`h-(wSH%f-2*6q4(G3ZJWa8Q0i`F5?CA2S*kk^%6s*{ZHy=rbTZ)CkJLqkU@U=1x#$yd8ruRnt$pg z9Uhy0VWeUfjcZAwWZ;Q?g%4VfA6N%&8iJIuco82D7h1t9w7|ALLoHW*Zbn1xZh3J3 z{I#}h(Q(erPj|{&we5E}11{BPi?LdoJZFFg0*EN)IlF9gn(*(m=t^vL;pFVguXwsYZz+9AMQ4H-{e?LV><;tzEYs~oF>Z3!_B$>5!6(FY z$%j?0x`>9E>}m0{KGt8V@SLDzbS=Xj(XgzwDgb5J0fY|l7CB&x1cx_t*_AMJN3IbW zYGY!X+nZcpW)fu&d3*d`j>|E;?%eddzenH<-xxyTU)cDnJaj15Kiu^2o!_6PhaIfO zRR!?qV>q=OD*#%HmewwdHQ@;_NQ%f=6Mq8gCCpGyugPgqDp?6ozv1ADRe_PcuUcMn zQzFFVE>hk^P~@ae2{X^^*T0>TyxYStzQ0&+yFWjE5QE(p5+-VYhLw1D}&TXq`Z@*WrS%IimCUf`qYiVYCF-h)Uc6i>)ogg|p z3n2ugq!WeMOyg)2ur1mrsKLK!{rAy9m)a;<1Fgemv2R zn{UQ12AM{VIM6w5j}uGpQc38cymJcMrCgE6TkSoePrx=cq|%IE6@Vk!_|4xrJ?pi( zTf9dfDr&168>YKIzDWv@UL#MPNYAul{qago4`J(@kTM}?801e`xj#mLWs$4aH2B1a zG@N@U+4@JvM^-#qf#a(NX>^FH>cp@_1NHsRSbr22JbkNA>*>o0BdaMlSKiNf^rj&( zxXYZ>`geY!moYklgF4mv+Fh)d54a(RF=|Awe#98R&7h=mtv391Kj7jK z^0V`aC&qY2e(^fd$M?l5S9^8C&Q_>vb`&snYfX}kd)T3V^ZcjHot}y^MXJg#E9jAw zFU%0rZ-6)kICDIJTPdp|M3V-6A_C#n!zy&1CWxN<+1UR~(1Sbr0pU@dX n{h!E^{r|tW9)LRi;X#ay=b-#Li5I+67V<`3O|I&dN$~#x=E*~b literal 0 HcmV?d00001 diff --git a/examples/excalidraw/with-script-in-browser/public/images/pika.jpeg b/examples/excalidraw/with-script-in-browser/public/images/pika.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..455ed52a638a0b060fba54892835e8923d671bfd GIT binary patch literal 6250 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 Vod0WA{QK4aPuTdMX5h>5{{x3Dqs0IK literal 0 HcmV?d00001 diff --git a/examples/excalidraw/with-script-in-browser/public/images/rocket.jpeg b/examples/excalidraw/with-script-in-browser/public/images/rocket.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..f17a74bd65c0705097ac832a6f63c6f3f6ca4e82 GIT binary patch literal 40368 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