From af6b81df40d4fe24a74837a1ef1b1ea924cba62b Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sat, 11 Nov 2023 10:04:02 +0100 Subject: [PATCH 01/13] fix: Replace hard coded font family with const value in addFrameLabelsAsTextElements (#7269) --- src/scene/export.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/scene/export.ts b/src/scene/export.ts index 694c162da..38bd09e65 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -15,6 +15,7 @@ import { distance, getFontString } from "../utils"; import { AppState, BinaryFiles } from "../types"; import { DEFAULT_EXPORT_PADDING, + FONT_FAMILY, FRAME_STYLE, SVG_NS, THEME_FILTER, @@ -105,7 +106,7 @@ const addFrameLabelsAsTextElements = ( let textElement: Mutable = newTextElement({ x: element.x, y: element.y - FRAME_STYLE.nameOffsetY, - fontFamily: 4, + fontFamily: FONT_FAMILY.Assistant, fontSize: FRAME_STYLE.nameFontSize, lineHeight: FRAME_STYLE.nameLineHeight as ExcalidrawTextElement["lineHeight"], From 7b000893147cab1b0c3acf6cdeb189da76b9e107 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:23:22 +0100 Subject: [PATCH 02/13] chore: bump @excalidraw/random-username (#7272) --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 4311dde60..035409bbf 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ }, "dependencies": { "@braintree/sanitize-url": "6.0.2", - "@excalidraw/mermaid-to-excalidraw": "0.1.2", "@excalidraw/laser-pointer": "1.2.0", - "@excalidraw/random-username": "1.0.0", + "@excalidraw/mermaid-to-excalidraw": "0.1.2", + "@excalidraw/random-username": "1.1.0", "@radix-ui/react-popover": "1.0.3", "@radix-ui/react-tabs": "1.0.2", "@sentry/browser": "6.2.5", diff --git a/yarn.lock b/yarn.lock index eacc147c7..82691f7b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1602,10 +1602,10 @@ resolved "https://registry.yarnpkg.com/@excalidraw/prettier-config/-/prettier-config-1.0.2.tgz#b7c061c99cee2f78b9ca470ea1fbd602683bba65" integrity sha512-rFIq8+A8WvkEzBsF++Rw6gzxE+hU3ZNkdg8foI+Upz2y/rOC/gUpWJaggPbCkoH3nlREVU59axQjZ1+F6ePRGg== -"@excalidraw/random-username@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@excalidraw/random-username/-/random-username-1.0.0.tgz#6d5293148aee6cd08dcdfcadc0c91276572f4499" - integrity sha512-pd4VapWahQ7PIyThGq32+C+JUS73mf3RSdC7BmQiXzhQsCTU4RHc8y9jBi+pb1CFV0iJXvjJRXnVdLCbTj3+HA== +"@excalidraw/random-username@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@excalidraw/random-username/-/random-username-1.1.0.tgz#6f388d6a9708cf655b8c9c6aa3fa569ee71ecf0f" + integrity sha512-nULYsQxkWHnbmHvcs+efMkJ4/9TtvNyFeLyHdeGxW0zHs6P+jYVqcRff9A6Vq9w9JXeDRnRh2VKvTtS19GW2qA== "@firebase/analytics-types@0.4.0": version "0.4.0" From 3d4ff59f40fa802403df3c9df691657148e3814d Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sun, 12 Nov 2023 13:24:13 +0100 Subject: [PATCH 03/13] fix: Can't toggle penMode off due to missing typecheck in togglePenMode (#7273) Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- src/components/App.tsx | 2 +- src/components/LayerUI.tsx | 4 ++-- src/components/MobileMenu.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 56dc795f4..8e4ab6355 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -2716,7 +2716,7 @@ class App extends React.Component { }); }; - togglePenMode = (force?: boolean) => { + togglePenMode = (force: boolean | null) => { this.setState((prevState) => { return { penMode: force ?? !prevState.penMode, diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 8b909da23..5c092fea1 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -66,7 +66,7 @@ interface LayerUIProps { elements: readonly NonDeletedExcalidrawElement[]; onLockToggle: () => void; onHandToolToggle: () => void; - onPenModeToggle: () => void; + onPenModeToggle: AppClassProperties["togglePenMode"]; showExitZenModeBtn: boolean; langCode: Language["code"]; renderTopRightUI?: ExcalidrawProps["renderTopRightUI"]; @@ -258,7 +258,7 @@ const LayerUI = ({ onPenModeToggle(null)} title={t("toolBar.penMode")} penDetected={appState.penDetected} /> diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx index bb26fe713..4299bf844 100644 --- a/src/components/MobileMenu.tsx +++ b/src/components/MobileMenu.tsx @@ -35,7 +35,7 @@ type MobileMenuProps = { elements: readonly NonDeletedExcalidrawElement[]; onLockToggle: () => void; onHandToolToggle: () => void; - onPenModeToggle: () => void; + onPenModeToggle: AppClassProperties["togglePenMode"]; renderTopRightUI?: ( isMobile: boolean, @@ -94,7 +94,7 @@ export const MobileMenu = ({ )} onPenModeToggle(null)} title={t("toolBar.penMode")} isMobile penDetected={appState.penDetected} From ae5b9a4ffdd34eca9f454a15833463e578289332 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sun, 12 Nov 2023 23:32:12 +0100 Subject: [PATCH 04/13] fix: not cloning elements on export polluting Scene mapping (#7276) --- src/scene/export.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/scene/export.ts b/src/scene/export.ts index 38bd09e65..e515a1da0 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -11,7 +11,7 @@ import { getElementAbsoluteCoords, } from "../element/bounds"; import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene"; -import { distance, getFontString } from "../utils"; +import { cloneJSON, distance, getFontString } from "../utils"; import { AppState, BinaryFiles } from "../types"; import { DEFAULT_EXPORT_PADDING, @@ -52,8 +52,9 @@ const __createSceneForElementsHack__ = ( // we can't duplicate elements to regenerate ids because we need the // orig ids when embedding. So we do another hack of not mapping element // ids to Scene instances so that we don't override the editor elements - // mapping - scene.replaceAllElements(elements, false); + // mapping. + // We still need to clone the objects themselves to regen references. + scene.replaceAllElements(cloneJSON(elements), false); return scene; }; From ceb255e8ee158e38cbd57460696fb0b09b89a4cb Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sun, 12 Nov 2023 23:34:05 +0100 Subject: [PATCH 05/13] fix: exportToSvg to honor frameRendering also for name not only for frame itself (#7270) Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- src/scene/export.ts | 97 +++++++++++++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/src/scene/export.ts b/src/scene/export.ts index e515a1da0..26fe7ff43 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -141,6 +141,36 @@ const getFrameRenderingConfig = ( }; }; +const prepareElementsForRender = ({ + elements, + exportingFrame, + frameRendering, + exportWithDarkMode, +}: { + elements: readonly ExcalidrawElement[]; + exportingFrame: ExcalidrawFrameElement | null | undefined; + frameRendering: AppState["frameRendering"]; + exportWithDarkMode: AppState["exportWithDarkMode"]; +}) => { + let nextElements: readonly ExcalidrawElement[]; + + if (exportingFrame) { + nextElements = elementsOverlappingBBox({ + elements, + bounds: exportingFrame, + type: "overlap", + }); + } else if (frameRendering.enabled && frameRendering.name) { + nextElements = addFrameLabelsAsTextElements(elements, { + exportWithDarkMode, + }); + } else { + nextElements = elements; + } + + return nextElements; +}; + export const exportToCanvas = async ( elements: readonly NonDeletedExcalidrawElement[], appState: AppState, @@ -169,21 +199,24 @@ export const exportToCanvas = async ( const tempScene = __createSceneForElementsHack__(elements); elements = tempScene.getNonDeletedElements(); - let nextElements: ExcalidrawElement[]; + const frameRendering = getFrameRenderingConfig( + exportingFrame ?? null, + appState.frameRendering ?? null, + ); + + const elementsForRender = prepareElementsForRender({ + elements, + exportingFrame, + exportWithDarkMode: appState.exportWithDarkMode, + frameRendering, + }); if (exportingFrame) { exportPadding = 0; - nextElements = elementsOverlappingBBox({ - elements, - bounds: exportingFrame, - type: "overlap", - }); - } else { - nextElements = addFrameLabelsAsTextElements(elements, appState); } const [minX, minY, width, height] = getCanvasSize( - exportingFrame ? [exportingFrame] : getRootElements(nextElements), + exportingFrame ? [exportingFrame] : getRootElements(elementsForRender), exportPadding, ); @@ -193,7 +226,7 @@ export const exportToCanvas = async ( const { imageCache } = await updateImageCache({ imageCache: new Map(), - fileIds: getInitializedImageElements(nextElements).map( + fileIds: getInitializedImageElements(elementsForRender).map( (element) => element.fileId, ), files, @@ -202,15 +235,12 @@ export const exportToCanvas = async ( renderStaticScene({ canvas, rc: rough.canvas(canvas), - elements: nextElements, - visibleElements: nextElements, + elements: elementsForRender, + visibleElements: elementsForRender, scale, appState: { ...appState, - frameRendering: getFrameRenderingConfig( - exportingFrame ?? null, - appState.frameRendering ?? null, - ), + frameRendering, viewBackgroundColor: exportBackground ? viewBackgroundColor : null, scrollX: -minX + exportPadding, scrollY: -minY + exportPadding, @@ -250,8 +280,14 @@ export const exportToSvg = async ( const tempScene = __createSceneForElementsHack__(elements); elements = tempScene.getNonDeletedElements(); + const frameRendering = getFrameRenderingConfig( + opts?.exportingFrame ?? null, + appState.frameRendering ?? null, + ); + let { exportPadding = DEFAULT_EXPORT_PADDING, + exportWithDarkMode = false, viewBackgroundColor, exportScale = 1, exportEmbedScene, @@ -259,19 +295,15 @@ export const exportToSvg = async ( const { exportingFrame = null } = opts || {}; - let nextElements: ExcalidrawElement[] = []; + const elementsForRender = prepareElementsForRender({ + elements, + exportingFrame, + exportWithDarkMode, + frameRendering, + }); if (exportingFrame) { exportPadding = 0; - nextElements = elementsOverlappingBBox({ - elements, - bounds: exportingFrame, - type: "overlap", - }); - } else { - nextElements = addFrameLabelsAsTextElements(elements, { - exportWithDarkMode: appState.exportWithDarkMode ?? false, - }); } let metadata = ""; @@ -295,7 +327,7 @@ export const exportToSvg = async ( } const [minX, minY, width, height] = getCanvasSize( - exportingFrame ? [exportingFrame] : getRootElements(nextElements), + exportingFrame ? [exportingFrame] : getRootElements(elementsForRender), exportPadding, ); @@ -306,7 +338,7 @@ export const exportToSvg = async ( svgRoot.setAttribute("viewBox", `0 0 ${width} ${height}`); svgRoot.setAttribute("width", `${width * exportScale}`); svgRoot.setAttribute("height", `${height * exportScale}`); - if (appState.exportWithDarkMode) { + if (exportWithDarkMode) { svgRoot.setAttribute("filter", THEME_FILTER); } @@ -381,15 +413,12 @@ export const exportToSvg = async ( } const rsvg = rough.svg(svgRoot); - renderSceneToSvg(nextElements, rsvg, svgRoot, files || {}, { + renderSceneToSvg(elementsForRender, rsvg, svgRoot, files || {}, { offsetX, offsetY, - exportWithDarkMode: appState.exportWithDarkMode ?? false, + exportWithDarkMode, renderEmbeddables: opts?.renderEmbeddables ?? false, - frameRendering: getFrameRenderingConfig( - exportingFrame ?? null, - appState.frameRendering ?? null, - ), + frameRendering, }); tempScene.destroy(); From adfd95be3394a2ea00088e9a8d609cd01cfefcf6 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Mon, 13 Nov 2023 16:18:36 +0530 Subject: [PATCH 06/13] =?UTF-8?q?build:=20support=20preact=20=F0=9F=A5=B3?= =?UTF-8?q?=20(#7255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build: support preact * add log * Simplify the config and generate prod and dev builds for preact * update changelog * remove logs * use env variable so its available during build time * update cl * fix --- src/packages/excalidraw/CHANGELOG.md | 12 +++++++ src/packages/excalidraw/main.js | 8 ++++- src/packages/excalidraw/package.json | 2 +- .../excalidraw/webpack.preact.config.js | 33 +++++++++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/packages/excalidraw/webpack.preact.config.js diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index a001e2426..f134afbb9 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -37,6 +37,18 @@ Please add the latest change on the top under the correct section. - [`useDevice`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#usedevice) hook's return value was changed to differentiate between `editor` and `viewport` breakpoints. [#7243](https://github.com/excalidraw/excalidraw/pull/7243) +### Build + +- Support Preact [#7255](https://github.com/excalidraw/excalidraw/pull/7255). The host needs to set `process.env.IS_PREACT` to `true` + +When using vite, you will have to make sure the variable process.env.IS_PREACT is available at runtime since Vite removes it by default, so you can update the vite config to ensure its available + +```json +define: { + "process.env.IS_PREACT": process.env.IS_PREACT, +} +``` + ## 0.16.1 (2023-09-21) ## Excalidraw Library diff --git a/src/packages/excalidraw/main.js b/src/packages/excalidraw/main.js index b1668faf7..853bb70f8 100644 --- a/src/packages/excalidraw/main.js +++ b/src/packages/excalidraw/main.js @@ -1,4 +1,10 @@ -if (process.env.NODE_ENV === "production") { +if (process.env.IS_PREACT === "true") { + if (process.env.NODE_ENV === "production") { + module.exports = require("./dist/excalidraw-with-preact.production.min.js"); + } else { + module.exports = require("./dist/excalidraw-with-preact.development.js"); + } +} else if (process.env.NODE_ENV === "production") { module.exports = require("./dist/excalidraw.production.min.js"); } else { module.exports = require("./dist/excalidraw.development.js"); diff --git a/src/packages/excalidraw/package.json b/src/packages/excalidraw/package.json index 2e52e156f..7ca23c2c8 100644 --- a/src/packages/excalidraw/package.json +++ b/src/packages/excalidraw/package.json @@ -78,7 +78,7 @@ "homepage": "https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw", "scripts": { "gen:types": "tsc --project ../../../tsconfig-types.json", - "build:umd": "rm -rf dist && cross-env NODE_ENV=production webpack --config webpack.prod.config.js && cross-env NODE_ENV=development webpack --config webpack.dev.config.js && yarn gen:types", + "build:umd": "rm -rf dist && cross-env NODE_ENV=production webpack --config webpack.prod.config.js && cross-env NODE_ENV=development webpack --config webpack.dev.config.js && NODE_ENV=development webpack --config webpack.preact.config.js && NODE_ENV=production webpack --config webpack.preact.config.js && yarn gen:types", "build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js", "pack": "yarn build:umd && yarn pack", "start": "webpack serve --config webpack.dev-server.config.js", diff --git a/src/packages/excalidraw/webpack.preact.config.js b/src/packages/excalidraw/webpack.preact.config.js new file mode 100644 index 000000000..c0516a768 --- /dev/null +++ b/src/packages/excalidraw/webpack.preact.config.js @@ -0,0 +1,33 @@ +const { merge } = require("webpack-merge"); + +const prodConfig = require("./webpack.prod.config"); +const devConfig = require("./webpack.dev.config"); + +const isProd = process.env.NODE_ENV === "production"; + +const config = isProd ? prodConfig : devConfig; +const outputFile = isProd + ? "excalidraw-with-preact.production.min" + : "excalidraw-with-preact.development"; + +const preactWebpackConfig = { + entry: { + [outputFile]: "./entry.js", + }, + externals: { + ...config.externals, + "react-dom/client": { + root: "ReactDOMClient", + commonjs2: "react-dom/client", + commonjs: "react-dom/client", + amd: "react-dom/client", + }, + "react/jsx-runtime": { + root: "ReactJSXRuntime", + commonjs2: "react/jsx-runtime", + commonjs: "react/jsx-runtime", + amd: "react/jsx-runtime", + }, + }, +}; +module.exports = merge(config, preactWebpackConfig); From 029c3c48ba19b6c40c775bf3b7934de0f3fab89e Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:34:59 +0100 Subject: [PATCH 07/13] fix: image insertion bugs (#7278) --- src/components/App.tsx | 17 +++++++++++++---- src/cursor.ts | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 8e4ab6355..18615e9cf 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -4740,9 +4740,13 @@ class App extends React.Component { }); const { x, y } = viewportCoordsToSceneCoords(event, this.state); + + const frame = this.getTopLayerFrameAtSceneCoords({ x, y }); + mutateElement(pendingImageElement, { x, y, + frameId: frame ? frame.id : null, }); } else if (this.state.activeTool.type === "freedraw") { this.handleFreeDrawElementOnPointerDown( @@ -5609,9 +5613,11 @@ class App extends React.Component { private createImageElement = ({ sceneX, sceneY, + addToFrameUnderCursor = true, }: { sceneX: number; sceneY: number; + addToFrameUnderCursor?: boolean; }) => { const [gridX, gridY] = getGridPoint( sceneX, @@ -5621,10 +5627,12 @@ class App extends React.Component { : this.state.gridSize, ); - const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ - x: gridX, - y: gridY, - }); + const topLayerFrame = addToFrameUnderCursor + ? this.getTopLayerFrameAtSceneCoords({ + x: gridX, + y: gridY, + }) + : null; const element = newImageElement({ type: "image", @@ -7554,6 +7562,7 @@ class App extends React.Component { const imageElement = this.createImageElement({ sceneX: x, sceneY: y, + addToFrameUnderCursor: false, }); if (insertOnCanvasDirectly) { diff --git a/src/cursor.ts b/src/cursor.ts index c39c2efcf..0d5fd2c3c 100644 --- a/src/cursor.ts +++ b/src/cursor.ts @@ -99,7 +99,7 @@ export const setCursorForShape = ( interactiveCanvas.style.cursor = `url(${url}), auto`; } else if (!["image", "custom"].includes(appState.activeTool.type)) { interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR; - } else { + } else if (appState.activeTool.type !== "image") { interactiveCanvas.style.cursor = CURSOR_TYPE.AUTO; } }; From 9d1d45a8ea4c346d1e8b41e1ea7a7cc73fa0a2c0 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Tue, 14 Nov 2023 13:11:05 +0530 Subject: [PATCH 08/13] chore: update changelog (#7279) * chore: update changelog * fix * Update CHANGELOG.md --- src/packages/excalidraw/CHANGELOG.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index f134afbb9..0cb361350 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -17,23 +17,27 @@ Please add the latest change on the top under the correct section. - Support `excalidrawAPI` prop for accessing the Excalidraw API [#7251](https://github.com/excalidraw/excalidraw/pull/7251). -#### BREAKING CHANGE - -- The `Ref` support has been removed in v0.17.0 so if you are using refs, please update the integration to use the [`excalidrawAPI`](http://localhost:3003/docs/@excalidraw/excalidraw/api/props/excalidraw-api) - -- Additionally `ready` and `readyPromise` from the API have been discontinued. These APIs were found to be superfluous, and as part of the effort to streamline the APIs and maintain simplicity, they were removed in version v0.17.0. - - Export [`getCommonBounds`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#getcommonbounds) helper from the package [#7247](https://github.com/excalidraw/excalidraw/pull/7247). -- Support frames via programmatic API [#7205](https://github.com/excalidraw/excalidraw/pull/7205). - - Export `elementsOverlappingBBox`, `isElementInsideBBox`, `elementPartiallyOverlapsWithOrContainsBBox` helpers for filtering/checking if elements within bounds. [#6727](https://github.com/excalidraw/excalidraw/pull/6727) - Regenerate ids by default when using transform api and also update bindings by 0.5px to avoid possible overlapping [#7195](https://github.com/excalidraw/excalidraw/pull/7195) +- Add onChange, onPointerDown, onPointerUp api subscribers [#7154](https://github.com/excalidraw/excalidraw/pull/7154). + +- Support props.locked for setActiveTool [#7153](https://github.com/excalidraw/excalidraw/pull/7153). + - Add `selected` prop for `MainMenu.Item` and `MainMenu.ItemCustom` components to indicate active state. [#7078](https://github.com/excalidraw/excalidraw/pull/7078) -#### BREAKING CHANGES +### Fixes + +- Double image dialog on image insertion [#7152](https://github.com/excalidraw/excalidraw/pull/7152). + +### Breaking Changes + +- The `Ref` support has been removed in v0.17.0 so if you are using refs, please update the integration to use the [`excalidrawAPI`](http://localhost:3003/docs/@excalidraw/excalidraw/api/props/excalidraw-api) [#7251](https://github.com/excalidraw/excalidraw/pull/7251). + +- Additionally `ready` and `readyPromise` from the API have been discontinued. These APIs were found to be superfluous, and as part of the effort to streamline the APIs and maintain simplicity, they were removed in version v0.17.0 [#7251](https://github.com/excalidraw/excalidraw/pull/7251). - [`useDevice`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#usedevice) hook's return value was changed to differentiate between `editor` and `viewport` breakpoints. [#7243](https://github.com/excalidraw/excalidraw/pull/7243) @@ -43,7 +47,7 @@ Please add the latest change on the top under the correct section. When using vite, you will have to make sure the variable process.env.IS_PREACT is available at runtime since Vite removes it by default, so you can update the vite config to ensure its available -```json +```js define: { "process.env.IS_PREACT": process.env.IS_PREACT, } From 9c425224c789d083bf16e0597ce4a429b9ee008e Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:25:41 +0100 Subject: [PATCH 09/13] feat: support disabling image tool (#6320) Co-authored-by: Aakansha Doshi --- src/components/Actions.tsx | 12 +++++- src/components/App.tsx | 49 ++++++++++++++++++++++++- src/components/LayerUI.tsx | 2 + src/components/MobileMenu.tsx | 4 ++ src/constants.ts | 3 ++ src/data/blob.ts | 37 +++++++++++-------- src/errors.ts | 16 ++++++++ src/locales/en.json | 1 + src/packages/excalidraw/CHANGELOG.md | 10 +++++ src/packages/excalidraw/example/App.tsx | 12 ++++++ src/packages/excalidraw/index.tsx | 3 ++ src/types.ts | 7 +++- 12 files changed, 136 insertions(+), 20 deletions(-) diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index bd8cb8b01..6d1d80b1e 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -13,7 +13,7 @@ import { hasStrokeWidth, } from "../scene"; import { SHAPES } from "../shapes"; -import { AppClassProperties, UIAppState, Zoom } from "../types"; +import { AppClassProperties, AppProps, UIAppState, Zoom } from "../types"; import { capitalizeString, isTransparent } from "../utils"; import Stack from "./Stack"; import { ToolButton } from "./ToolButton"; @@ -218,10 +218,12 @@ export const ShapesSwitcher = ({ activeTool, appState, app, + UIOptions, }: { activeTool: UIAppState["activeTool"]; appState: UIAppState; app: AppClassProperties; + UIOptions: AppProps["UIOptions"]; }) => { const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false); @@ -232,6 +234,14 @@ export const ShapesSwitcher = ({ return ( <> {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => { + if ( + UIOptions.tools?.[ + value as Extract + ] === false + ) { + return null; + } + const label = t(`toolBar.${value}`); const letter = key && capitalizeString(typeof key === "string" ? key : key[0]); diff --git a/src/components/App.tsx b/src/components/App.tsx index 18615e9cf..42080e831 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -341,6 +341,7 @@ import { import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas"; import { jotaiStore } from "../jotai"; import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; +import { ImageSceneDataError } from "../errors"; import { getSnapLinesAtPointer, snapDraggedElements, @@ -2272,6 +2273,11 @@ class App extends React.Component { // prefer spreadsheet data over image file (MS Office/Libre Office) if (isSupportedImageFile(file) && !data.spreadsheet) { + if (!this.isToolSupported("image")) { + this.setState({ errorMessage: t("errors.imageToolNotSupported") }); + return; + } + const imageElement = this.createImageElement({ sceneX, sceneY }); this.insertImageElement(imageElement, file); this.initializeImageDimensions(imageElement); @@ -2477,7 +2483,8 @@ class App extends React.Component { ) { if ( !isPlainPaste && - mixedContent.some((node) => node.type === "imageUrl") + mixedContent.some((node) => node.type === "imageUrl") && + this.isToolSupported("image") ) { const imageURLs = mixedContent .filter((node) => node.type === "imageUrl") @@ -3284,6 +3291,16 @@ class App extends React.Component { } }); + // We purposely widen the `tool` type so this helper can be called with + // any tool without having to type check it + private isToolSupported = (tool: T) => { + return ( + this.props.UIOptions.tools?.[ + tool as Extract + ] !== false + ); + }; + setActiveTool = ( tool: ( | ( @@ -3296,6 +3313,13 @@ class App extends React.Component { | { type: "custom"; customType: string } ) & { locked?: boolean }, ) => { + if (!this.isToolSupported(tool.type)) { + console.warn( + `"${tool.type}" tool is disabled via "UIOptions.canvasActions.tools.${tool.type}"`, + ); + return; + } + const nextActiveTool = updateActiveTool(this.state, tool); if (nextActiveTool.type === "hand") { setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB); @@ -7479,6 +7503,13 @@ class App extends React.Component { imageFile: File, showCursorImagePreview?: boolean, ) => { + // we should be handling all cases upstream, but in case we forget to handle + // a future case, let's throw here + if (!this.isToolSupported("image")) { + this.setState({ errorMessage: t("errors.imageToolNotSupported") }); + return; + } + this.scene.addNewElement(imageElement); try { @@ -7863,7 +7894,10 @@ class App extends React.Component { ); try { - if (isSupportedImageFile(file)) { + // if image tool not supported, don't show an error here and let it fall + // through so we still support importing scene data from images. If no + // scene data encoded, we'll show an error then + if (isSupportedImageFile(file) && this.isToolSupported("image")) { // first attempt to decode scene from the image if it's embedded // --------------------------------------------------------------------- @@ -7991,6 +8025,17 @@ class App extends React.Component { }); } } catch (error: any) { + if ( + error instanceof ImageSceneDataError && + error.code === "IMAGE_NOT_CONTAINS_SCENE_DATA" && + !this.isToolSupported("image") + ) { + this.setState({ + isLoading: false, + errorMessage: t("errors.imageToolNotSupported"), + }); + return; + } this.setState({ isLoading: false, errorMessage: error.message }); } }; diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 5c092fea1..b32adcf30 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -280,6 +280,7 @@ const LayerUI = ({ @@ -470,6 +471,7 @@ const LayerUI = ({ renderSidebars={renderSidebars} device={device} renderWelcomeScreen={renderWelcomeScreen} + UIOptions={UIOptions} /> )} {!device.editor.isMobile && ( diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx index 4299bf844..91d0c518c 100644 --- a/src/components/MobileMenu.tsx +++ b/src/components/MobileMenu.tsx @@ -1,6 +1,7 @@ import React from "react"; import { AppClassProperties, + AppProps, AppState, Device, ExcalidrawProps, @@ -45,6 +46,7 @@ type MobileMenuProps = { renderSidebars: () => JSX.Element | null; device: Device; renderWelcomeScreen: boolean; + UIOptions: AppProps["UIOptions"]; app: AppClassProperties; }; @@ -62,6 +64,7 @@ export const MobileMenu = ({ renderSidebars, device, renderWelcomeScreen, + UIOptions, app, }: MobileMenuProps) => { const { @@ -83,6 +86,7 @@ export const MobileMenu = ({ diff --git a/src/constants.ts b/src/constants.ts index fca1c0d29..247349f64 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -222,6 +222,9 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = { toggleTheme: null, saveAsImage: true, }, + tools: { + image: true, + }, }; // breakpoints diff --git a/src/data/blob.ts b/src/data/blob.ts index 81ce340fe..b1b625700 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -3,7 +3,7 @@ import { cleanAppStateForExport } from "../appState"; import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants"; import { clearElementsForExport } from "../element"; import { ExcalidrawElement, FileId } from "../element/types"; -import { CanvasError } from "../errors"; +import { CanvasError, ImageSceneDataError } from "../errors"; import { t } from "../i18n"; import { calculateScrollCenter } from "../scene"; import { AppState, DataURL, LibraryItem } from "../types"; @@ -24,15 +24,12 @@ const parseFileContents = async (blob: Blob | File) => { ).decodePngMetadata(blob); } catch (error: any) { if (error.message === "INVALID") { - throw new DOMException( + throw new ImageSceneDataError( t("alerts.imageDoesNotContainScene"), - "EncodingError", + "IMAGE_NOT_CONTAINS_SCENE_DATA", ); } else { - throw new DOMException( - t("alerts.cannotRestoreFromImage"), - "EncodingError", - ); + throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage")); } } } else { @@ -58,15 +55,12 @@ const parseFileContents = async (blob: Blob | File) => { }); } catch (error: any) { if (error.message === "INVALID") { - throw new DOMException( + throw new ImageSceneDataError( t("alerts.imageDoesNotContainScene"), - "EncodingError", + "IMAGE_NOT_CONTAINS_SCENE_DATA", ); } else { - throw new DOMException( - t("alerts.cannotRestoreFromImage"), - "EncodingError", - ); + throw new ImageSceneDataError(t("alerts.cannotRestoreFromImage")); } } } @@ -131,8 +125,19 @@ export const loadSceneOrLibraryFromBlob = async ( fileHandle?: FileSystemHandle | null, ) => { const contents = await parseFileContents(blob); + let data; try { - const data = JSON.parse(contents); + try { + data = JSON.parse(contents); + } catch (error: any) { + if (isSupportedImageFile(blob)) { + throw new ImageSceneDataError( + t("alerts.imageDoesNotContainScene"), + "IMAGE_NOT_CONTAINS_SCENE_DATA", + ); + } + throw error; + } if (isValidExcalidrawData(data)) { return { type: MIME_TYPES.excalidraw, @@ -162,7 +167,9 @@ export const loadSceneOrLibraryFromBlob = async ( } throw new Error(t("alerts.couldNotLoadInvalidFile")); } catch (error: any) { - console.error(error.message); + if (error instanceof ImageSceneDataError) { + throw error; + } throw new Error(t("alerts.couldNotLoadInvalidFile")); } }; diff --git a/src/errors.ts b/src/errors.ts index e0444d105..4df403496 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -16,3 +16,19 @@ export class AbortError extends DOMException { super(message, "AbortError"); } } + +type ImageSceneDataErrorCode = + | "IMAGE_NOT_CONTAINS_SCENE_DATA" + | "IMAGE_SCENE_DATA_ERROR"; + +export class ImageSceneDataError extends Error { + public code; + constructor( + message = "Image Scene Data Error", + code: ImageSceneDataErrorCode = "IMAGE_SCENE_DATA_ERROR", + ) { + super(message); + this.name = "EncodingError"; + this.code = code; + } +} diff --git a/src/locales/en.json b/src/locales/en.json index 846d2dbcf..3b4eba6de 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -209,6 +209,7 @@ "importLibraryError": "Couldn't load library", "collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.", "collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.", + "imageToolNotSupported": "Images are disabled.", "brave_measure_text_error": { "line1": "Looks like you are using Brave browser with the Aggressively Block Fingerprinting setting enabled.", "line2": "This could result in breaking the Text Elements in your drawings.", diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 0cb361350..00c10775a 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -15,6 +15,16 @@ Please add the latest change on the top under the correct section. ### Features +- Added support for disabling `image` tool (also disabling image insertion in general, though keeps support for importing from `.excalidraw` files) [#6320](https://github.com/excalidraw/excalidraw/pull/6320). + +For disabling `image` you need to set 👇 + +``` +UIOptions.tools = { + image: false +} +``` + - Support `excalidrawAPI` prop for accessing the Excalidraw API [#7251](https://github.com/excalidraw/excalidraw/pull/7251). - Export [`getCommonBounds`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils#getcommonbounds) helper from the package [#7247](https://github.com/excalidraw/excalidraw/pull/7247). diff --git a/src/packages/excalidraw/example/App.tsx b/src/packages/excalidraw/example/App.tsx index 974bbb7ef..0f9056783 100644 --- a/src/packages/excalidraw/example/App.tsx +++ b/src/packages/excalidraw/example/App.tsx @@ -98,6 +98,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { const [exportWithDarkMode, setExportWithDarkMode] = useState(false); const [exportEmbedScene, setExportEmbedScene] = useState(false); const [theme, setTheme] = useState("light"); + const [disableImageTool, setDisableImageTool] = useState(false); const [isCollaborating, setIsCollaborating] = useState(false); const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>( {}, @@ -606,6 +607,16 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { /> Switch to Dark Theme +