From 35bb449a4bb66911a2e0d4b3a3a56d54b300f7ec Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Mon, 12 May 2025 23:55:36 +1000 Subject: [PATCH 1/9] fix: update cached segments when visible area changes (#9512) --- packages/excalidraw/lasso/index.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/excalidraw/lasso/index.ts b/packages/excalidraw/lasso/index.ts index 41e6b2080..163a8b7a9 100644 --- a/packages/excalidraw/lasso/index.ts +++ b/packages/excalidraw/lasso/index.ts @@ -17,7 +17,7 @@ import { selectGroupsForSelectedElements } from "@excalidraw/element"; import { getContainerElement } from "@excalidraw/element"; -import { arrayToMap, easeOut } from "@excalidraw/common"; +import { arrayToMap, easeOut, isShallowEqual } from "@excalidraw/common"; import type { ExcalidrawElement, @@ -33,11 +33,18 @@ import { getLassoSelectedElementIds } from "./utils"; import type App from "../components/App"; +type CanvasTranslate = { + scrollX: number; + scrollY: number; + zoom: number; +}; + export class LassoTrail extends AnimatedTrail { private intersectedElements: Set = new Set(); private enclosedElements: Set = new Set(); private elementsSegments: Map[]> | null = null; + private canvasTranslate: CanvasTranslate | null = null; private keepPreviousSelection: boolean = false; constructor(animationFrameHandler: AnimationFrameHandler, app: App) { @@ -169,7 +176,17 @@ export class LassoTrail extends AnimatedTrail { .getCurrentTrail() ?.originalPoints?.map((p) => pointFrom(p[0], p[1])); - if (!this.elementsSegments) { + const currentCanvasTranslate: CanvasTranslate = { + scrollX: this.app.state.scrollX, + scrollY: this.app.state.scrollY, + zoom: this.app.state.zoom.value, + }; + + if ( + !this.elementsSegments || + !isShallowEqual(currentCanvasTranslate, this.canvasTranslate ?? {}) + ) { + this.canvasTranslate = currentCanvasTranslate; this.elementsSegments = new Map(); const visibleElementsMap = arrayToMap(this.app.visibleElements); for (const element of this.app.visibleElements) { From 298812e1d0cd1494debb9d30e90427d37c500482 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Mon, 12 May 2025 18:09:37 +0200 Subject: [PATCH 2/9] fix: improve ctrl+alt lasso selecting (#9514) --- packages/excalidraw/components/App.tsx | 29 ++++++++++++++++---------- packages/excalidraw/snapping.ts | 2 ++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 29de62cf4..1fc19ac14 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -7276,8 +7276,13 @@ class App extends React.Component { }); // If we click on something } else if (hitElement != null) { + // == deep selection == // on CMD/CTRL, drill down to hit element regardless of groups etc. if (event[KEYS.CTRL_OR_CMD]) { + if (event.altKey) { + // ctrl + alt means we're lasso selecting + return false; + } if (!this.state.selectedElementIds[hitElement.id]) { pointerDownState.hit.wasAddedToSelection = true; } @@ -8636,17 +8641,19 @@ class App extends React.Component { pointerDownState.lastCoords.x = pointerCoords.x; pointerDownState.lastCoords.y = pointerCoords.y; if (event.altKey) { - this.setActiveTool( - { type: "lasso", fromSelection: true }, - event.shiftKey, - ); - this.lassoTrail.startPath( - pointerDownState.origin.x, - pointerDownState.origin.y, - event.shiftKey, - ); - this.setAppState({ - selectionElement: null, + flushSync(() => { + this.setActiveTool( + { type: "lasso", fromSelection: true }, + event.shiftKey, + ); + this.lassoTrail.startPath( + pointerDownState.origin.x, + pointerDownState.origin.y, + event.shiftKey, + ); + this.setAppState({ + selectionElement: null, + }); }); } else { this.maybeDragNewGenericElement(pointerDownState, event); diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts index 635ba065d..195ba8c81 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -173,6 +173,8 @@ export const isSnappingEnabled = ({ (app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) || (!app.state.objectsSnapModeEnabled && event[KEYS.CTRL_OR_CMD] && + // ctrl + alt means we're lasso selecting + !event.altKey && !isGridModeEnabled(app)) ); } From 4dfb8a3f8eaf7722f5e133e270927e297d7aaa0e Mon Sep 17 00:00:00 2001 From: zsviczian Date: Tue, 13 May 2025 19:48:26 +0200 Subject: [PATCH 3/9] feat: allow forms.microsoft.com domain for embeddables (#9519) * Update embeddable.ts * no need for same origin * The form does not load without allow same origin * automatically add embed=true to link if not present * fix link check --- packages/element/src/embeddable.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/element/src/embeddable.ts b/packages/element/src/embeddable.ts index e6d6b4af3..78dc26fe2 100644 --- a/packages/element/src/embeddable.ts +++ b/packages/element/src/embeddable.ts @@ -33,6 +33,8 @@ const RE_GH_GIST = /^https:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)/; const RE_GH_GIST_EMBED = /^ twitter embeds const RE_TWITTER = /(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/; @@ -69,6 +71,7 @@ const ALLOWED_DOMAINS = new Set([ "val.town", "giphy.com", "reddit.com", + "forms.microsoft.com", ]); const ALLOW_SAME_ORIGIN = new Set([ @@ -82,6 +85,7 @@ const ALLOW_SAME_ORIGIN = new Set([ "*.simplepdf.eu", "stackblitz.com", "reddit.com", + "forms.microsoft.com", ]); export const createSrcDoc = (body: string) => { @@ -206,6 +210,10 @@ export const getEmbedLink = ( }; } + if (RE_MSFORMS.test(link) && !link.includes("embed=true")) { + link += link.includes("?") ? "&embed=true" : "?embed=true"; + } + if (RE_TWITTER.test(link)) { const postId = link.match(RE_TWITTER)![1]; // the embed srcdoc still supports twitter.com domain only. From f7dcc893ea0f2d1061db614d3c2c7b43e256e7bc Mon Sep 17 00:00:00 2001 From: zsviczian Date: Wed, 14 May 2025 13:38:18 +0200 Subject: [PATCH 4/9] feat: transparent link background, scale link icon when zooming to below 100% (#9520) * Do not set link background color, dynamically scale down link icon size with zoom. * removed unnecessary change * use canvas bg color & reduce size and stroke width --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/element/src/renderElement.ts | 2 -- .../excalidraw/components/hyperlink/helpers.ts | 17 +++++++++-------- packages/excalidraw/renderer/staticScene.ts | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/element/src/renderElement.ts b/packages/element/src/renderElement.ts index c8091e8ed..e749bd90c 100644 --- a/packages/element/src/renderElement.ts +++ b/packages/element/src/renderElement.ts @@ -349,8 +349,6 @@ const generateElementCanvas = ( }; }; -export const DEFAULT_LINK_SIZE = 14; - const IMAGE_PLACEHOLDER_IMG = document.createElement("img"); IMAGE_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent( ``, diff --git a/packages/excalidraw/components/hyperlink/helpers.ts b/packages/excalidraw/components/hyperlink/helpers.ts index bdabbfc44..7d39b7ff7 100644 --- a/packages/excalidraw/components/hyperlink/helpers.ts +++ b/packages/excalidraw/components/hyperlink/helpers.ts @@ -4,8 +4,6 @@ import { MIME_TYPES } from "@excalidraw/common"; import { getElementAbsoluteCoords } from "@excalidraw/element"; import { hitElementBoundingBox } from "@excalidraw/element"; -import { DEFAULT_LINK_SIZE } from "@excalidraw/element"; - import type { GlobalPoint, Radians } from "@excalidraw/math"; import type { Bounds } from "@excalidraw/element"; @@ -16,9 +14,11 @@ import type { import type { AppState, UIAppState } from "../../types"; +export const DEFAULT_LINK_SIZE = 12; + export const EXTERNAL_LINK_IMG = document.createElement("img"); EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent( - ``, + ``, )}`; export const ELEMENT_LINK_IMG = document.createElement("img"); @@ -32,13 +32,14 @@ export const getLinkHandleFromCoords = ( appState: Pick, ): Bounds => { const size = DEFAULT_LINK_SIZE; - const linkWidth = size / appState.zoom.value; - const linkHeight = size / appState.zoom.value; - const linkMarginY = size / appState.zoom.value; + const zoom = appState.zoom.value > 1 ? appState.zoom.value : 1; + const linkWidth = size / zoom; + const linkHeight = size / zoom; + const linkMarginY = size / zoom; const centerX = (x1 + x2) / 2; const centerY = (y1 + y2) / 2; - const centeringOffset = (size - 8) / (2 * appState.zoom.value); - const dashedLineMargin = 4 / appState.zoom.value; + const centeringOffset = (size - 8) / (2 * zoom); + const dashedLineMargin = 4 / zoom; // Same as `ne` resize handle const x = x2 + dashedLineMargin - centeringOffset; diff --git a/packages/excalidraw/renderer/staticScene.ts b/packages/excalidraw/renderer/staticScene.ts index d63779464..18cf0092d 100644 --- a/packages/excalidraw/renderer/staticScene.ts +++ b/packages/excalidraw/renderer/staticScene.ts @@ -188,7 +188,7 @@ const renderLinkIcon = ( window.devicePixelRatio * appState.zoom.value, window.devicePixelRatio * appState.zoom.value, ); - linkCanvasCacheContext.fillStyle = "#fff"; + linkCanvasCacheContext.fillStyle = appState.viewBackgroundColor || "#fff"; linkCanvasCacheContext.fillRect(0, 0, width, height); if (canvasKey === "elementLink") { From 4ca5f53b1f74e7746c5d7b867f415cd9f5655a05 Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Wed, 14 May 2025 22:04:03 +1000 Subject: [PATCH 5/9] fix: alt + ctrl lasso selected elements not always kept (#9522) * fix: alt + ctrl lasso selected elements not always kept * Update packages/excalidraw/components/App.tsx --------- Co-authored-by: David Luzar <5153846+dwelle@users.noreply.github.com> --- packages/excalidraw/components/App.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 1fc19ac14..b0c43359b 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -9528,7 +9528,10 @@ class App extends React.Component { // if we're editing a line, pointerup shouldn't switch selection if // box selected (!this.state.editingLinearElement || - !pointerDownState.boxSelection.hasOccurred) + !pointerDownState.boxSelection.hasOccurred) && + // hitElement can be set when alt + ctrl to toggle lasso and we will + // just respect the selected elements from lasso instead + this.state.activeTool.type !== "lasso" ) { // when inside line editor, shift selects points instead if (childEvent.shiftKey && !this.state.editingLinearElement) { From 0a534f1bc688ad6c71954fef51a69407613efdbc Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Wed, 14 May 2025 14:04:40 +0200 Subject: [PATCH 6/9] fix: never show snap lines when lasso tool active (#9523) --- packages/excalidraw/snapping.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts index 195ba8c81..8dd1bd59a 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -170,12 +170,11 @@ export const isSnappingEnabled = ({ }) => { if (event) { return ( - (app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) || - (!app.state.objectsSnapModeEnabled && - event[KEYS.CTRL_OR_CMD] && - // ctrl + alt means we're lasso selecting - !event.altKey && - !isGridModeEnabled(app)) + app.state.activeTool.type !== "lasso" && + ((app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) || + (!app.state.objectsSnapModeEnabled && + event[KEYS.CTRL_OR_CMD] && + !isGridModeEnabled(app))) ); } From d92a84903887078449bf4e4bab9fa307169b28b6 Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Wed, 14 May 2025 16:01:43 +0200 Subject: [PATCH 7/9] fix: issues when importing package outside of browser (#9525) --- packages/common/package.json | 2 +- packages/common/src/constants.ts | 3 ++- packages/element/package.json | 2 +- packages/element/src/renderElement.ts | 14 ++++++++++++-- packages/excalidraw/components/PublishLibrary.tsx | 4 ++-- packages/excalidraw/data/json.ts | 6 +++--- packages/math/package.json | 2 +- packages/utils/package.json | 2 +- 8 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/common/package.json b/packages/common/package.json index 32cffc717..8fedd6742 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -13,7 +13,7 @@ "default": "./dist/prod/index.js" }, "./*": { - "types": "./../common/dist/types/common/src/*.d.ts" + "types": "./dist/types/common/src/*.d.ts" } }, "files": [ diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 04a473cd7..3850ed1c7 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -10,6 +10,7 @@ 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); export const isFirefox = + typeof window !== "undefined" && "netscape" in window && navigator.userAgent.indexOf("rv:") > 1 && navigator.userAgent.indexOf("Gecko") > 1; @@ -255,7 +256,7 @@ export const EXPORT_DATA_TYPES = { excalidrawClipboardWithAPI: "excalidraw-api/clipboard", } as const; -export const EXPORT_SOURCE = +export const getExportSource = () => window.EXCALIDRAW_EXPORT_SOURCE || window.location.origin; // time in milliseconds diff --git a/packages/element/package.json b/packages/element/package.json index 1eec60742..16b9a49e7 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -13,7 +13,7 @@ "default": "./dist/prod/index.js" }, "./*": { - "types": "./../element/dist/types/element/src/*.d.ts" + "types": "./dist/types/element/src/*.d.ts" } }, "files": [ diff --git a/packages/element/src/renderElement.ts b/packages/element/src/renderElement.ts index e749bd90c..2786f3f84 100644 --- a/packages/element/src/renderElement.ts +++ b/packages/element/src/renderElement.ts @@ -349,12 +349,22 @@ const generateElementCanvas = ( }; }; -const IMAGE_PLACEHOLDER_IMG = document.createElement("img"); +export const DEFAULT_LINK_SIZE = 14; + +const IMAGE_PLACEHOLDER_IMG = + typeof document !== "undefined" + ? document.createElement("img") + : ({ src: "" } as HTMLImageElement); // mock image element outside of browser + IMAGE_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent( ``, )}`; -const IMAGE_ERROR_PLACEHOLDER_IMG = document.createElement("img"); +const IMAGE_ERROR_PLACEHOLDER_IMG = + typeof document !== "undefined" + ? document.createElement("img") + : ({ src: "" } as HTMLImageElement); // mock image element outside of browser + IMAGE_ERROR_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent( ``, )}`; diff --git a/packages/excalidraw/components/PublishLibrary.tsx b/packages/excalidraw/components/PublishLibrary.tsx index 580b909d4..076b303d7 100644 --- a/packages/excalidraw/components/PublishLibrary.tsx +++ b/packages/excalidraw/components/PublishLibrary.tsx @@ -5,10 +5,10 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { EDITOR_LS_KEYS, EXPORT_DATA_TYPES, - EXPORT_SOURCE, MIME_TYPES, VERSIONS, chunk, + getExportSource, } from "@excalidraw/common"; import { EditorLocalStorage } from "../data/EditorLocalStorage"; @@ -281,7 +281,7 @@ const PublishLibrary = ({ const libContent: ExportedLibraryData = { type: EXPORT_DATA_TYPES.excalidrawLibrary, version: VERSIONS.excalidrawLibrary, - source: EXPORT_SOURCE, + source: getExportSource(), libraryItems: clonedLibItems, }; const content = JSON.stringify(libContent, null, 2); diff --git a/packages/excalidraw/data/json.ts b/packages/excalidraw/data/json.ts index 527c9e56e..52cbf9958 100644 --- a/packages/excalidraw/data/json.ts +++ b/packages/excalidraw/data/json.ts @@ -1,7 +1,7 @@ import { DEFAULT_FILENAME, EXPORT_DATA_TYPES, - EXPORT_SOURCE, + getExportSource, MIME_TYPES, VERSIONS, } from "@excalidraw/common"; @@ -56,7 +56,7 @@ export const serializeAsJSON = ( const data: ExportedDataState = { type: EXPORT_DATA_TYPES.excalidraw, version: VERSIONS.excalidraw, - source: EXPORT_SOURCE, + source: getExportSource(), elements: type === "local" ? clearElementsForExport(elements) @@ -142,7 +142,7 @@ export const serializeLibraryAsJSON = (libraryItems: LibraryItems) => { const data: ExportedLibraryData = { type: EXPORT_DATA_TYPES.excalidrawLibrary, version: VERSIONS.excalidrawLibrary, - source: EXPORT_SOURCE, + source: getExportSource(), libraryItems, }; return JSON.stringify(data, null, 2); diff --git a/packages/math/package.json b/packages/math/package.json index e9f5fd8da..5fac47bef 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -13,7 +13,7 @@ "default": "./dist/prod/index.js" }, "./*": { - "types": "./../math/dist/types/math/src/*.d.ts" + "types": "./dist/types/math/src/*.d.ts" } }, "files": [ diff --git a/packages/utils/package.json b/packages/utils/package.json index 2dc54c59c..6dc400c65 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -13,7 +13,7 @@ "default": "./dist/prod/index.js" }, "./*": { - "types": "./../utils/dist/types/utils/src/*.d.ts" + "types": "./dist/types/utils/src/*.d.ts" } }, "files": [ From 6b5fb30d699eaa4af5c82705a8bbe17c3727f6da Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Wed, 14 May 2025 16:02:01 +0200 Subject: [PATCH 8/9] fix: unify line height across default fonts (#9513) --- packages/common/src/font-metadata.ts | 4 ++-- packages/excalidraw/wysiwyg/textWysiwyg.test.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/common/src/font-metadata.ts b/packages/common/src/font-metadata.ts index f6005ee45..7382aa70f 100644 --- a/packages/common/src/font-metadata.ts +++ b/packages/common/src/font-metadata.ts @@ -46,7 +46,7 @@ export const FONT_METADATA: Record = { unitsPerEm: 1000, ascender: 1011, descender: -353, - lineHeight: 1.35, + lineHeight: 1.25, }, }, [FONT_FAMILY["Lilita One"]]: { @@ -116,7 +116,7 @@ export const FONT_METADATA: Record = { unitsPerEm: 1000, ascender: 880, descender: -144, - lineHeight: 1.15, + lineHeight: 1.25, }, fallback: true, }, diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx index fb142057f..e7cd97509 100644 --- a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx +++ b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx @@ -1324,7 +1324,7 @@ describe("textWysiwyg", () => { ).toEqual(FONT_FAMILY.Nunito); expect( (h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight, - ).toEqual(1.35); + ).toEqual(1.25); }); describe("should align correctly", () => { From 95d89a751a01c5dcf528fef6d7e5901a72a31f35 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Thu, 15 May 2025 13:22:26 +0200 Subject: [PATCH 9/9] refactor: decouple radio button selection from `.buttonList` wrapper (#9528) * refactor: decouple radio button selection from `.buttonList` * fix --- .../components/ExampleApp.scss | 2 +- .../excalidraw/actions/actionProperties.tsx | 726 +++++++++--------- packages/excalidraw/components/Actions.tsx | 2 +- .../excalidraw/components/ButtonSelect.tsx | 30 - .../components/FontPicker/FontPicker.tsx | 16 +- ...uttonIconSelect.tsx => RadioSelection.tsx} | 7 +- packages/excalidraw/css/styles.scss | 14 +- 7 files changed, 393 insertions(+), 404 deletions(-) delete mode 100644 packages/excalidraw/components/ButtonSelect.tsx rename packages/excalidraw/components/{ButtonIconSelect.tsx => RadioSelection.tsx} (88%) diff --git a/examples/with-script-in-browser/components/ExampleApp.scss b/examples/with-script-in-browser/components/ExampleApp.scss index e41a77ccc..77b921ea8 100644 --- a/examples/with-script-in-browser/components/ExampleApp.scss +++ b/examples/with-script-in-browser/components/ExampleApp.scss @@ -52,7 +52,7 @@ transform: none; } -.excalidraw .panelColumn { +.excalidraw .selected-shape-actions { text-align: left; } diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index c379de7f8..654676e90 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -76,7 +76,7 @@ import type { Scene } from "@excalidraw/element"; import type { CaptureUpdateActionType } from "@excalidraw/element"; import { trackEvent } from "../analytics"; -import { ButtonIconSelect } from "../components/ButtonIconSelect"; +import { RadioSelection } from "../components/RadioSelection"; import { ColorPicker } from "../components/ColorPicker/ColorPicker"; import { FontPicker } from "../components/FontPicker/FontPicker"; import { IconPicker } from "../components/IconPicker"; @@ -421,50 +421,52 @@ export const actionChangeFillStyle = register({ return (
{t("labels.fill")} - element.fillStyle, - (element) => element.hasOwnProperty("fillStyle"), - (hasSelection) => - hasSelection ? null : appState.currentItemFillStyle, - )} - onClick={(value, event) => { - const nextValue = - event.altKey && - value === "hachure" && - selectedElements.every((el) => el.fillStyle === "hachure") - ? "zigzag" - : value; +
+ element.fillStyle, + (element) => element.hasOwnProperty("fillStyle"), + (hasSelection) => + hasSelection ? null : appState.currentItemFillStyle, + )} + onClick={(value, event) => { + const nextValue = + event.altKey && + value === "hachure" && + selectedElements.every((el) => el.fillStyle === "hachure") + ? "zigzag" + : value; - updateData(nextValue); - }} - /> + updateData(nextValue); + }} + /> +
); }, @@ -488,38 +490,40 @@ export const actionChangeStrokeWidth = register({ PanelComponent: ({ elements, appState, updateData, app }) => (
{t("labels.strokeWidth")} - element.strokeWidth, - (element) => element.hasOwnProperty("strokeWidth"), - (hasSelection) => - hasSelection ? null : appState.currentItemStrokeWidth, - )} - onChange={(value) => updateData(value)} - /> +
+ element.strokeWidth, + (element) => element.hasOwnProperty("strokeWidth"), + (hasSelection) => + hasSelection ? null : appState.currentItemStrokeWidth, + )} + onChange={(value) => updateData(value)} + /> +
), }); @@ -543,35 +547,37 @@ export const actionChangeSloppiness = register({ PanelComponent: ({ elements, appState, updateData, app }) => (
{t("labels.sloppiness")} - element.roughness, - (element) => element.hasOwnProperty("roughness"), - (hasSelection) => - hasSelection ? null : appState.currentItemRoughness, - )} - onChange={(value) => updateData(value)} - /> +
+ element.roughness, + (element) => element.hasOwnProperty("roughness"), + (hasSelection) => + hasSelection ? null : appState.currentItemRoughness, + )} + onChange={(value) => updateData(value)} + /> +
), }); @@ -594,35 +600,37 @@ export const actionChangeStrokeStyle = register({ PanelComponent: ({ elements, appState, updateData, app }) => (
{t("labels.strokeStyle")} - element.strokeStyle, - (element) => element.hasOwnProperty("strokeStyle"), - (hasSelection) => - hasSelection ? null : appState.currentItemStrokeStyle, - )} - onChange={(value) => updateData(value)} - /> +
+ element.strokeStyle, + (element) => element.hasOwnProperty("strokeStyle"), + (hasSelection) => + hasSelection ? null : appState.currentItemStrokeStyle, + )} + onChange={(value) => updateData(value)} + /> +
), }); @@ -661,63 +669,65 @@ export const actionChangeFontSize = register({ PanelComponent: ({ elements, appState, updateData, app }) => (
{t("labels.fontSize")} - { - if (isTextElement(element)) { - return element.fontSize; - } - const boundTextElement = getBoundTextElement( - element, - app.scene.getNonDeletedElementsMap(), - ); - if (boundTextElement) { - return boundTextElement.fontSize; - } - return null; - }, - (element) => - isTextElement(element) || - getBoundTextElement( - element, - app.scene.getNonDeletedElementsMap(), - ) !== null, - (hasSelection) => - hasSelection - ? null - : appState.currentItemFontSize || DEFAULT_FONT_SIZE, - )} - onChange={(value) => updateData(value)} - /> +
+ { + if (isTextElement(element)) { + return element.fontSize; + } + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); + if (boundTextElement) { + return boundTextElement.fontSize; + } + return null; + }, + (element) => + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, + (hasSelection) => + hasSelection + ? null + : appState.currentItemFontSize || DEFAULT_FONT_SIZE, + )} + onChange={(value) => updateData(value)} + /> +
), }); @@ -1189,52 +1199,54 @@ export const actionChangeTextAlign = register({ return (
{t("labels.textAlign")} - - group="text-align" - options={[ - { - value: "left", - text: t("labels.left"), - icon: TextAlignLeftIcon, - testId: "align-left", - }, - { - value: "center", - text: t("labels.center"), - icon: TextAlignCenterIcon, - testId: "align-horizontal-center", - }, - { - value: "right", - text: t("labels.right"), - icon: TextAlignRightIcon, - testId: "align-right", - }, - ]} - value={getFormValue( - elements, - app, - (element) => { - if (isTextElement(element)) { - return element.textAlign; - } - const boundTextElement = getBoundTextElement( - element, - elementsMap, - ); - if (boundTextElement) { - return boundTextElement.textAlign; - } - return null; - }, - (element) => - isTextElement(element) || - getBoundTextElement(element, elementsMap) !== null, - (hasSelection) => - hasSelection ? null : appState.currentItemTextAlign, - )} - onChange={(value) => updateData(value)} - /> +
+ + group="text-align" + options={[ + { + value: "left", + text: t("labels.left"), + icon: TextAlignLeftIcon, + testId: "align-left", + }, + { + value: "center", + text: t("labels.center"), + icon: TextAlignCenterIcon, + testId: "align-horizontal-center", + }, + { + value: "right", + text: t("labels.right"), + icon: TextAlignRightIcon, + testId: "align-right", + }, + ]} + value={getFormValue( + elements, + app, + (element) => { + if (isTextElement(element)) { + return element.textAlign; + } + const boundTextElement = getBoundTextElement( + element, + elementsMap, + ); + if (boundTextElement) { + return boundTextElement.textAlign; + } + return null; + }, + (element) => + isTextElement(element) || + getBoundTextElement(element, elementsMap) !== null, + (hasSelection) => + hasSelection ? null : appState.currentItemTextAlign, + )} + onChange={(value) => updateData(value)} + /> +
); }, @@ -1277,54 +1289,56 @@ export const actionChangeVerticalAlign = register({ PanelComponent: ({ elements, appState, updateData, app }) => { return (
- - group="text-align" - options={[ - { - value: VERTICAL_ALIGN.TOP, - text: t("labels.alignTop"), - icon: , - testId: "align-top", - }, - { - value: VERTICAL_ALIGN.MIDDLE, - text: t("labels.centerVertically"), - icon: , - testId: "align-middle", - }, - { - value: VERTICAL_ALIGN.BOTTOM, - text: t("labels.alignBottom"), - icon: , - testId: "align-bottom", - }, - ]} - value={getFormValue( - elements, - app, - (element) => { - if (isTextElement(element) && element.containerId) { - return element.verticalAlign; - } - const boundTextElement = getBoundTextElement( - element, - app.scene.getNonDeletedElementsMap(), - ); - if (boundTextElement) { - return boundTextElement.verticalAlign; - } - return null; - }, - (element) => - isTextElement(element) || - getBoundTextElement( - element, - app.scene.getNonDeletedElementsMap(), - ) !== null, - (hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE), - )} - onChange={(value) => updateData(value)} - /> +
+ + group="text-align" + options={[ + { + value: VERTICAL_ALIGN.TOP, + text: t("labels.alignTop"), + icon: , + testId: "align-top", + }, + { + value: VERTICAL_ALIGN.MIDDLE, + text: t("labels.centerVertically"), + icon: , + testId: "align-middle", + }, + { + value: VERTICAL_ALIGN.BOTTOM, + text: t("labels.alignBottom"), + icon: , + testId: "align-bottom", + }, + ]} + value={getFormValue( + elements, + app, + (element) => { + if (isTextElement(element) && element.containerId) { + return element.verticalAlign; + } + const boundTextElement = getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ); + if (boundTextElement) { + return boundTextElement.verticalAlign; + } + return null; + }, + (element) => + isTextElement(element) || + getBoundTextElement( + element, + app.scene.getNonDeletedElementsMap(), + ) !== null, + (hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE), + )} + onChange={(value) => updateData(value)} + /> +
); }, @@ -1372,32 +1386,38 @@ export const actionChangeRoundness = register({ return (
{t("labels.edges")} - - hasLegacyRoundness ? null : element.roundness ? "round" : "sharp", - (element) => - !isArrowElement(element) && element.hasOwnProperty("roundness"), - (hasSelection) => - hasSelection ? null : appState.currentItemRoundness, - )} - onChange={(value) => updateData(value)} - /> +
+ + hasLegacyRoundness + ? null + : element.roundness + ? "round" + : "sharp", + (element) => + !isArrowElement(element) && element.hasOwnProperty("roundness"), + (hasSelection) => + hasSelection ? null : appState.currentItemRoundness, + )} + onChange={(value) => updateData(value)} + /> +
); }, @@ -1760,48 +1780,50 @@ export const actionChangeArrowType = register({ return (
{t("labels.arrowtypes")} - { - if (isArrowElement(element)) { - return element.elbowed - ? ARROW_TYPE.elbow - : element.roundness - ? ARROW_TYPE.round - : ARROW_TYPE.sharp; - } +
+ { + if (isArrowElement(element)) { + return element.elbowed + ? ARROW_TYPE.elbow + : element.roundness + ? ARROW_TYPE.round + : ARROW_TYPE.sharp; + } - return null; - }, - (element) => isArrowElement(element), - (hasSelection) => - hasSelection ? null : appState.currentItemArrowType, - )} - onChange={(value) => updateData(value)} - /> + return null; + }, + (element) => isArrowElement(element), + (hasSelection) => + hasSelection ? null : appState.currentItemArrowType, + )} + onChange={(value) => updateData(value)} + /> +
); }, diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index 4f3782048..60dab78f4 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -154,7 +154,7 @@ export const SelectedShapeActions = ({ !isSingleElementBoundContainer && alignActionsPredicate(appState, app); return ( -
+
{canChangeStrokeColor(appState, targetElements) && renderAction("changeStrokeColor")} diff --git a/packages/excalidraw/components/ButtonSelect.tsx b/packages/excalidraw/components/ButtonSelect.tsx deleted file mode 100644 index c47ff65e7..000000000 --- a/packages/excalidraw/components/ButtonSelect.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import clsx from "clsx"; - -export const ButtonSelect = ({ - options, - value, - onChange, - group, -}: { - options: { value: T; text: string }[]; - value: T | null; - onChange: (value: T) => void; - group: string; -}) => ( -
- {options.map((option) => ( - - ))} -
-); diff --git a/packages/excalidraw/components/FontPicker/FontPicker.tsx b/packages/excalidraw/components/FontPicker/FontPicker.tsx index 546e1fa34..118c6fac3 100644 --- a/packages/excalidraw/components/FontPicker/FontPicker.tsx +++ b/packages/excalidraw/components/FontPicker/FontPicker.tsx @@ -6,7 +6,7 @@ import { FONT_FAMILY } from "@excalidraw/common"; import type { FontFamilyValues } from "@excalidraw/element/types"; import { t } from "../../i18n"; -import { ButtonIconSelect } from "../ButtonIconSelect"; +import { RadioSelection } from "../RadioSelection"; import { ButtonSeparator } from "../ButtonSeparator"; import { FontFamilyCodeIcon, @@ -82,12 +82,14 @@ export const FontPicker = React.memo( return (
- - type="button" - options={defaultFonts} - value={selectedFontFamily} - onClick={onSelectCallback} - /> +
+ + type="button" + options={defaultFonts} + value={selectedFontFamily} + onClick={onSelectCallback} + /> +
diff --git a/packages/excalidraw/components/ButtonIconSelect.tsx b/packages/excalidraw/components/RadioSelection.tsx similarity index 88% rename from packages/excalidraw/components/ButtonIconSelect.tsx rename to packages/excalidraw/components/RadioSelection.tsx index 45665e4ca..9cdc47e48 100644 --- a/packages/excalidraw/components/ButtonIconSelect.tsx +++ b/packages/excalidraw/components/RadioSelection.tsx @@ -4,8 +4,7 @@ import { ButtonIcon } from "./ButtonIcon"; import type { JSX } from "react"; -// TODO: It might be "clever" to add option.icon to the existing component -export const ButtonIconSelect = ( +export const RadioSelection = ( props: { options: { value: T; @@ -28,7 +27,7 @@ export const ButtonIconSelect = ( } ), ) => ( -
+ <> {props.options.map((option) => props.type === "button" ? ( ( ), )} -
+ ); diff --git a/packages/excalidraw/css/styles.scss b/packages/excalidraw/css/styles.scss index 6f1d9cd48..f8486083e 100644 --- a/packages/excalidraw/css/styles.scss +++ b/packages/excalidraw/css/styles.scss @@ -140,7 +140,7 @@ body.excalidraw-cursor-resize * { justify-content: space-between; } - .panelColumn { + .selected-shape-actions { display: flex; flex-direction: column; row-gap: 0.75rem; @@ -245,10 +245,6 @@ body.excalidraw-cursor-resize * { left: 0; right: 0; --bar-padding: calc(4 * var(--space-factor)); - padding-top: #{"max(var(--bar-padding), var(--sat,0))"}; - padding-right: var(--sar, 0); - padding-bottom: var(--sab, 0); - padding-left: var(--sal, 0); z-index: 4; display: flex; align-items: flex-end; @@ -263,10 +259,6 @@ body.excalidraw-cursor-resize * { display: flex; flex-direction: column; pointer-events: var(--ui-pointerEvents); - - .panelColumn { - padding: 8px 8px 0 8px; - } } } @@ -302,6 +294,10 @@ body.excalidraw-cursor-resize * { overflow-y: auto; box-sizing: border-box; margin-bottom: var(--bar-padding); + + .selected-shape-actions { + padding: 8px 8px 0 8px; + } } .App-menu {