diff --git a/.gitignore b/.gitignore
index 4a3f6f367d..e637a8c0f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@ src/packages/excalidraw/types
src/packages/excalidraw/example/public/bundle.js
src/packages/excalidraw/example/public/excalidraw-assets-dev
src/packages/excalidraw/example/public/excalidraw.development.js
+coverage
diff --git a/dev-docs/docs/@excalidraw/excalidraw/faq.mdx b/dev-docs/docs/@excalidraw/excalidraw/faq.mdx
index 6f0fd30a70..4684d6c791 100644
--- a/dev-docs/docs/@excalidraw/excalidraw/faq.mdx
+++ b/dev-docs/docs/@excalidraw/excalidraw/faq.mdx
@@ -4,6 +4,34 @@
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
+### Turning off Aggressive Anti-Fingerprinting in Brave browser
+
+When *Aggressive Anti-Fingerprinting* is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
+
+We strongly recommend turning it off. You can follow the steps below on how to do so.
+
+
+1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button
+
+
+
+
+2. Once opened, look for **Aggressively Block Fingerprinting**
+
+
+
+3. Switch to **Block Fingerprinting**
+
+
+
+4. Thats all. All text elements should be fixed now 🎉
+
+
+
+If disabling this setting doesn't fix the display of text elements, please consider opening an [issue](https://github.com/excalidraw/excalidraw/issues/new) on our GitHub, or message us on [Discord](https://discord.gg/UexuTaE).
+
+
+
## Need help?
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
diff --git a/dev-docs/docs/assets/aggressive-block-fingerprint.png b/dev-docs/docs/assets/aggressive-block-fingerprint.png
new file mode 100644
index 0000000000..236a12dbeb
Binary files /dev/null and b/dev-docs/docs/assets/aggressive-block-fingerprint.png differ
diff --git a/dev-docs/docs/assets/block-fingerprint.png b/dev-docs/docs/assets/block-fingerprint.png
new file mode 100644
index 0000000000..bbbf4d26d0
Binary files /dev/null and b/dev-docs/docs/assets/block-fingerprint.png differ
diff --git a/dev-docs/docs/assets/brave-shield.png b/dev-docs/docs/assets/brave-shield.png
new file mode 100644
index 0000000000..bbb1216533
Binary files /dev/null and b/dev-docs/docs/assets/brave-shield.png differ
diff --git a/scripts/locales-coverage-description.js b/scripts/locales-coverage-description.js
index 08db5b841f..0f9bacfaaf 100644
--- a/scripts/locales-coverage-description.js
+++ b/scripts/locales-coverage-description.js
@@ -2,6 +2,9 @@ const fs = require("fs");
const THRESSHOLD = 85;
+// we're using BCP 47 language tags as keys
+// e.g. https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1
+
const crowdinMap = {
"ar-SA": "en-ar",
"bg-BG": "en-bg",
@@ -52,6 +55,7 @@ const crowdinMap = {
"kk-KZ": "en-kk",
"vi-VN": "en-vi",
"mr-IN": "en-mr",
+ "th-TH": "en-th",
};
const flags = {
@@ -104,6 +108,7 @@ const flags = {
"eu-ES": "🇪🇦",
"vi-VN": "🇻🇳",
"mr-IN": "🇮🇳",
+ "th-TH": "🇹🇭",
};
const languages = {
@@ -156,6 +161,7 @@ const languages = {
"zh-TW": "繁體中文",
"vi-VN": "Tiếng Việt",
"mr-IN": "मराठी",
+ "th-TH": "ภาษาไทย",
};
const percentages = fs.readFileSync(
diff --git a/src/actions/shortcuts.ts b/src/actions/shortcuts.ts
index 9f5c1128de..68e0d82cdb 100644
--- a/src/actions/shortcuts.ts
+++ b/src/actions/shortcuts.ts
@@ -1,5 +1,6 @@
import { isDarwin } from "../constants";
import { t } from "../i18n";
+import { SubtypeOf } from "../utility-types";
import { getShortcutKey } from "../utils";
import { ActionName } from "./types";
diff --git a/src/actions/types.ts b/src/actions/types.ts
index cf18b04b66..5811d184a7 100644
--- a/src/actions/types.ts
+++ b/src/actions/types.ts
@@ -6,6 +6,7 @@ import {
ExcalidrawProps,
BinaryFiles,
} from "../types";
+import { MarkOptional } from "../utility-types";
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
diff --git a/src/components/App.test.tsx b/src/components/App.test.tsx
new file mode 100644
index 0000000000..baf25ab9bc
--- /dev/null
+++ b/src/components/App.test.tsx
@@ -0,0 +1,45 @@
+import ReactDOM from "react-dom";
+import * as Renderer from "../renderer/renderScene";
+import { reseed } from "../random";
+import { render, queryByTestId } from "../tests/test-utils";
+
+import ExcalidrawApp from "../excalidraw-app";
+
+const renderScene = jest.spyOn(Renderer, "renderScene");
+
+describe("Test ", () => {
+ beforeEach(async () => {
+ // Unmount ReactDOM from root
+ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
+ localStorage.clear();
+ renderScene.mockClear();
+ reseed(7);
+ });
+
+ it("should show error modal when using brave and measureText API is not working", async () => {
+ (global.navigator as any).brave = {
+ isBrave: {
+ name: "isBrave",
+ },
+ };
+
+ const originalContext = global.HTMLCanvasElement.prototype.getContext("2d");
+ //@ts-ignore
+ global.HTMLCanvasElement.prototype.getContext = (contextId) => {
+ return {
+ ...originalContext,
+ measureText: () => ({
+ width: 0,
+ }),
+ };
+ };
+
+ await render();
+ expect(
+ queryByTestId(
+ document.querySelector(".excalidraw-modal-container")!,
+ "brave-measure-text-error",
+ ),
+ ).toMatchSnapshot();
+ });
+});
diff --git a/src/components/App.tsx b/src/components/App.tsx
index f5b96e8a3e..d0094d6e5a 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -62,6 +62,7 @@ import {
GRID_SIZE,
IMAGE_RENDER_TIMEOUT,
isAndroid,
+ isBrave,
LINE_CONFIRM_THRESHOLD,
MAX_ALLOWED_FILE_BYTES,
MIME_TYPES,
@@ -276,6 +277,7 @@ import {
getContainerDims,
getContainerElement,
getTextBindableContainerAtPosition,
+ isMeasureTextSupported,
isValidTextContainer,
} from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
@@ -294,6 +296,7 @@ import { actionToggleHandTool } from "../actions/actionCanvas";
import { jotaiStore } from "../jotai";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionCreateContainerFromText } from "../actions/actionBoundText";
+import BraveMeasureTextError from "./BraveMeasureTextError";
const deviceContextInitialValue = {
isSmScreen: false,
@@ -439,7 +442,6 @@ class App extends React.Component {
};
this.id = nanoid();
-
this.library = new Library(this);
this.actionManager = new ActionManager(
this.syncActionResult,
@@ -757,6 +759,8 @@ class App extends React.Component {
const theme =
actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
let name = actionResult?.appState?.name ?? this.state.name;
+ const errorMessage =
+ actionResult?.appState?.errorMessage ?? this.state.errorMessage;
if (typeof this.props.viewModeEnabled !== "undefined") {
viewModeEnabled = this.props.viewModeEnabled;
}
@@ -772,7 +776,6 @@ class App extends React.Component {
if (typeof this.props.name !== "undefined") {
name = this.props.name;
}
-
this.setState(
(state) => {
// using Object.assign instead of spread to fool TS 4.2.2+ into
@@ -790,6 +793,7 @@ class App extends React.Component {
gridSize,
theme,
name,
+ errorMessage,
});
},
() => {
@@ -918,7 +922,6 @@ class App extends React.Component {
),
};
}
-
// FontFaceSet loadingdone event we listen on may not always fire
// (looking at you Safari), so on init we manually load fonts for current
// text elements on canvas, and rerender them once done. This also
@@ -1046,6 +1049,13 @@ class App extends React.Component {
} else {
this.updateDOMRect(this.initializeScene);
}
+
+ // note that this check seems to always pass in localhost
+ if (isBrave() && !isMeasureTextSupported()) {
+ this.setState({
+ errorMessage: ,
+ });
+ }
}
public componentWillUnmount() {
diff --git a/src/components/BraveMeasureTextError.tsx b/src/components/BraveMeasureTextError.tsx
new file mode 100644
index 0000000000..8a4a71e4fd
--- /dev/null
+++ b/src/components/BraveMeasureTextError.tsx
@@ -0,0 +1,42 @@
+import { t } from "../i18n";
+const BraveMeasureTextError = () => {
+ return (
+
+
+ {t("errors.brave_measure_text_error.start")}
+
+ {t("errors.brave_measure_text_error.aggressive_block_fingerprint")}
+ {" "}
+ {t("errors.brave_measure_text_error.setting_enabled")}.
+
+
+ {t("errors.brave_measure_text_error.break")}{" "}
+
+ {t("errors.brave_measure_text_error.text_elements")}
+ {" "}
+ {t("errors.brave_measure_text_error.in_your_drawings")}.
+
+
+ {t("errors.brave_measure_text_error.strongly_recommend")}{" "}
+
+ {" "}
+ {t("errors.brave_measure_text_error.steps")}
+ {" "}
+ {t("errors.brave_measure_text_error.how")}.
+
+
+ {t("errors.brave_measure_text_error.disable_setting")}{" "}
+
+ {t("errors.brave_measure_text_error.issue")}
+ {" "}
+ {t("errors.brave_measure_text_error.write")}{" "}
+
+ {t("errors.brave_measure_text_error.discord")}
+
+ .
+
+
+ );
+};
+
+export default BraveMeasureTextError;
diff --git a/src/components/ErrorDialog.tsx b/src/components/ErrorDialog.tsx
index c1c7899812..56c303c157 100644
--- a/src/components/ErrorDialog.tsx
+++ b/src/components/ErrorDialog.tsx
@@ -5,13 +5,13 @@ import { Dialog } from "./Dialog";
import { useExcalidrawContainer } from "./App";
export const ErrorDialog = ({
- message,
+ children,
onClose,
}: {
- message: string;
+ children?: React.ReactNode;
onClose?: () => void;
}) => {
- const [modalIsShown, setModalIsShown] = useState(!!message);
+ const [modalIsShown, setModalIsShown] = useState(!!children);
const { container: excalidrawContainer } = useExcalidrawContainer();
const handleClose = React.useCallback(() => {
@@ -32,7 +32,7 @@ export const ErrorDialog = ({
onCloseRequest={handleClose}
title={t("errorDialog.title")}
>
- {message}
+ {children}
)}
>
diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx
index bea074d625..7103d3adec 100644
--- a/src/components/LayerUI.tsx
+++ b/src/components/LayerUI.tsx
@@ -364,10 +364,9 @@ const LayerUI = ({
{appState.isLoading && }
{appState.errorMessage && (
- setAppState({ errorMessage: null })}
- />
+ setAppState({ errorMessage: null })}>
+ {appState.errorMessage}
+
)}
{appState.openDialog === "help" && (
should show error modal when using brave and measureText API is not working 1`] = `
+
+
+ Looks like you are using Brave browser with the
+
+
+ Aggressively Block Fingerprinting
+
+
+ setting enabled
+ .
+
+
+ This could result in breaking the
+
+
+ Text Elements
+
+
+ in your drawings
+ .
+
+
+ We strongly recommend disabling this setting. You can follow
+
+
+
+ these steps
+
+
+ on how to do so
+ .
+
+
+ If disabling this setting doesn't fix the display of text elements, please open an
+
+
+ issue
+
+
+ on our GitHub, or write us on
+
+
+ Discord
+
+ .
+
+
+`;
diff --git a/src/constants.ts b/src/constants.ts
index aa25667a27..ef563e4a41 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -12,6 +12,9 @@ export const isFirefox =
export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
export const isSafari =
!isChrome && navigator.userAgent.indexOf("Safari") !== -1;
+// keeping function so it can be mocked in test
+export const isBrave = () =>
+ (navigator as any).brave?.isBrave?.name === "isBrave";
export const APP_NAME = "Excalidraw";
diff --git a/src/data/blob.ts b/src/data/blob.ts
index 473042b56f..35c040ef3b 100644
--- a/src/data/blob.ts
+++ b/src/data/blob.ts
@@ -7,6 +7,7 @@ import { CanvasError } from "../errors";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { AppState, DataURL, LibraryItem } from "../types";
+import { ValueOf } from "../utility-types";
import { bytesToHexString } from "../utils";
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
import { isValidExcalidrawData, isValidLibrary } from "./json";
diff --git a/src/data/restore.ts b/src/data/restore.ts
index b9dd5fcfd4..553878669c 100644
--- a/src/data/restore.ts
+++ b/src/data/restore.ts
@@ -35,6 +35,7 @@ import { getUpdatedTimestamp, updateActiveTool } from "../utils";
import { arrayToMap } from "../utils";
import { isValidSubtype } from "../subtypes";
import oc from "open-color";
+import { MarkOptional, Mutable } from "../utility-types";
type RestoredAppState = Omit<
AppState,
diff --git a/src/element/bounds.ts b/src/element/bounds.ts
index 2eab1d93da..3245ca3fc2 100644
--- a/src/element/bounds.ts
+++ b/src/element/bounds.ts
@@ -23,6 +23,7 @@ import {
import { rescalePoints } from "../points";
import { getBoundTextElement, getContainerElement } from "./textElement";
import { LinearElementEditor } from "./linearElementEditor";
+import { Mutable } from "../utility-types";
// x and y position of top left corner, x and y position of bottom right corner
export type Bounds = readonly [number, number, number, number];
diff --git a/src/element/collision.ts b/src/element/collision.ts
index 54540ae5eb..097b76d345 100644
--- a/src/element/collision.ts
+++ b/src/element/collision.ts
@@ -38,6 +38,7 @@ import { isTextElement } from ".";
import { isTransparent } from "../utils";
import { shouldShowBoundingBox } from "./transformHandles";
import { getBoundTextElement } from "./textElement";
+import { Mutable } from "../utility-types";
const isElementDraggableFromInside = (
element: NonDeletedExcalidrawElement,
diff --git a/src/element/linearElementEditor.ts b/src/element/linearElementEditor.ts
index 5c478515df..aedc259744 100644
--- a/src/element/linearElementEditor.ts
+++ b/src/element/linearElementEditor.ts
@@ -41,6 +41,7 @@ import { shouldRotateWithDiscreteAngle } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { getShapeForElement } from "../renderer/renderElement";
import { DRAGGING_THRESHOLD } from "../constants";
+import { Mutable } from "../utility-types";
const editorMidPointsCache: {
version: number | null;
diff --git a/src/element/mutateElement.ts b/src/element/mutateElement.ts
index 45f85592b4..56a6157573 100644
--- a/src/element/mutateElement.ts
+++ b/src/element/mutateElement.ts
@@ -5,6 +5,7 @@ import { getSizeFromPoints } from "../points";
import { randomInteger } from "../random";
import { Point } from "../types";
import { getUpdatedTimestamp } from "../utils";
+import { Mutable } from "../utility-types";
import { maybeGetSubtypeProps } from "./newElement";
import { getSubtypeMethods } from "../subtypes";
diff --git a/src/element/newElement.ts b/src/element/newElement.ts
index cd672bc1e1..e8b66bda91 100644
--- a/src/element/newElement.ts
+++ b/src/element/newElement.ts
@@ -32,6 +32,7 @@ import {
} from "./textElement";
import { VERTICAL_ALIGN } from "../constants";
import { isArrowElement } from "./typeChecks";
+import { MarkOptional, Merge, Mutable } from "../utility-types";
import { getSubtypeMethods, isValidSubtype } from "../subtypes";
export const maybeGetSubtypeProps = (
diff --git a/src/element/textElement.ts b/src/element/textElement.ts
index 9cc055d326..6f0db2b354 100644
--- a/src/element/textElement.ts
+++ b/src/element/textElement.ts
@@ -9,7 +9,13 @@ import {
NonDeletedExcalidrawElement,
} from "./types";
import { mutateElement } from "./mutateElement";
-import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
+import {
+ BOUND_TEXT_PADDING,
+ DEFAULT_FONT_FAMILY,
+ DEFAULT_FONT_SIZE,
+ TEXT_ALIGN,
+ VERTICAL_ALIGN,
+} from "../constants";
import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene";
import { isTextElement } from ".";
@@ -24,6 +30,7 @@ import {
resetOriginalContainerCache,
updateOriginalContainerCache,
} from "./textWysiwyg";
+import { ExtractSetType } from "../utility-types";
export const measureTextElement = function (element, next) {
const map = getSubtypeMethods(element.subtype);
@@ -817,3 +824,14 @@ export const getMaxContainerHeight = (container: ExcalidrawElement) => {
}
return height - BOUND_TEXT_PADDING * 2;
};
+
+export const isMeasureTextSupported = () => {
+ const width = getTextWidth(
+ DUMMY_TEXT,
+ getFontString({
+ fontSize: DEFAULT_FONT_SIZE,
+ fontFamily: DEFAULT_FONT_FAMILY,
+ }),
+ );
+ return width > 0;
+};
diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx
index 0f7d664372..23e755b342 100644
--- a/src/element/textWysiwyg.tsx
+++ b/src/element/textWysiwyg.tsx
@@ -11,7 +11,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./typeChecks";
-import { CLASSES, isFirefox, isSafari, VERTICAL_ALIGN } from "../constants";
+import { CLASSES, VERTICAL_ALIGN } from "../constants";
import {
ExcalidrawElement,
ExcalidrawLinearElement,
@@ -304,8 +304,7 @@ export const textWysiwyg = ({
if (!container) {
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
textElementWidth = Math.min(textElementWidth, maxWidth);
- } else if (isFirefox || isSafari) {
- // As firefox, Safari needs little higher dimensions on DOM
+ } else {
textElementWidth += 0.5;
transformWidth += 0.5;
}
diff --git a/src/element/typeChecks.ts b/src/element/typeChecks.ts
index 0f13648f96..164fafe687 100644
--- a/src/element/typeChecks.ts
+++ b/src/element/typeChecks.ts
@@ -1,5 +1,6 @@
import { ROUNDNESS } from "../constants";
import { AppState } from "../types";
+import { MarkNonNullable } from "../utility-types";
import {
ExcalidrawElement,
ExcalidrawTextElement,
diff --git a/src/element/types.ts b/src/element/types.ts
index 39279c55e6..9d033bc3f5 100644
--- a/src/element/types.ts
+++ b/src/element/types.ts
@@ -7,6 +7,7 @@ import {
THEME,
VERTICAL_ALIGN,
} from "../constants";
+import { MarkNonNullable, ValueOf } from "../utility-types";
export type ChartType = "bar" | "line";
export type FillStyle = "hachure" | "cross-hatch" | "solid";
diff --git a/src/excalidraw-app/collab/Collab.tsx b/src/excalidraw-app/collab/Collab.tsx
index 30c9846c83..e48484ab64 100644
--- a/src/excalidraw-app/collab/Collab.tsx
+++ b/src/excalidraw-app/collab/Collab.tsx
@@ -838,10 +838,9 @@ class Collab extends PureComponent {
/>
)}
{errorMessage && (
- this.setState({ errorMessage: "" })}
- />
+ this.setState({ errorMessage: "" })}>
+ {errorMessage}
+
)}
>
);
diff --git a/src/excalidraw-app/data/firebase.ts b/src/excalidraw-app/data/firebase.ts
index b6b2624979..02e14466f2 100644
--- a/src/excalidraw-app/data/firebase.ts
+++ b/src/excalidraw-app/data/firebase.ts
@@ -14,6 +14,7 @@ import { encryptData, decryptData } from "../../data/encryption";
import { MIME_TYPES } from "../../constants";
import { reconcileElements } from "../collab/reconciliation";
import { getSyncableElements, SyncableExcalidrawElement } from ".";
+import { ResolutionType } from "../../utility-types";
// private
// -----------------------------------------------------------------------------
diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx
index c61de57635..51e760eb80 100644
--- a/src/excalidraw-app/index.tsx
+++ b/src/excalidraw-app/index.tsx
@@ -86,6 +86,7 @@ import { useAtomWithInitialValue } from "../jotai";
import { appJotaiStore } from "./app-jotai";
import "./index.scss";
+import { ResolutionType } from "../utility-types";
polyfill();
@@ -677,10 +678,9 @@ const ExcalidrawWrapper = () => {
{excalidrawAPI && }
{errorMessage && (
- setErrorMessage("")}
- />
+ setErrorMessage("")}>
+ {errorMessage}
+
)}
);
diff --git a/src/global.d.ts b/src/global.d.ts
index 336d089a80..8d7603e4af 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -51,36 +51,6 @@ interface Clipboard extends EventTarget {
write(data: any[]): Promise;
}
-type Mutable = {
- -readonly [P in keyof T]: T[P];
-};
-
-type ValueOf = T[keyof T];
-
-type Merge = Omit & N;
-
-/** utility type to assert that the second type is a subtype of the first type.
- * Returns the subtype. */
-type SubtypeOf = Subtype;
-
-type ResolutionType any> = T extends (
- ...args: any
-) => Promise
- ? R
- : any;
-
-// https://github.com/krzkaczor/ts-essentials
-type MarkOptional = Omit & Partial>;
-
-type MarkRequired = Exclude &
- Required>;
-
-type MarkNonNullable = {
- [P in K]-?: P extends K ? NonNullable : T[P];
-} & { [P in keyof T]: T[P] };
-
-type NonOptional = Exclude;
-
// PNG encoding/decoding
// -----------------------------------------------------------------------------
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
@@ -102,23 +72,6 @@ declare module "png-chunks-extract" {
}
// -----------------------------------------------------------------------------
-// -----------------------------------------------------------------------------
-// type getter for interface's callable type
-// src: https://stackoverflow.com/a/58658851/927631
-// -----------------------------------------------------------------------------
-type SignatureType = T extends (...args: infer R) => any ? R : never;
-type CallableType any> = (
- ...args: SignatureType
-) => ReturnType;
-// --------------------------------------------------------------------------—
-
-// Type for React.forwardRef --- supply only the first generic argument T
-type ForwardRef = Parameters<
- CallableType>
->[1];
-
-// --------------------------------------------------------------------------—
-
interface Blob {
handle?: import("browser-fs-acces").FileSystemHandle;
name?: string;
@@ -166,5 +119,3 @@ declare module "image-blob-reduce" {
const reduce: ImageBlobReduce.ImageBlobReduceStatic;
export = reduce;
}
-
-type ExtractSetType> = T extends Set ? U : never;
diff --git a/src/history.ts b/src/history.ts
index cc620cae10..d102a7ecc9 100644
--- a/src/history.ts
+++ b/src/history.ts
@@ -2,6 +2,7 @@ import { AppState } from "./types";
import { ExcalidrawElement } from "./element/types";
import { isLinearElement } from "./element/typeChecks";
import { deepCopyElement } from "./element/newElement";
+import { Mutable } from "./utility-types";
export interface HistoryEntry {
appState: ReturnType;
diff --git a/src/locales/en.json b/src/locales/en.json
index a7d721d2ec..b656a63cf1 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -121,7 +121,6 @@
"edit": "Edit line",
"exit": "Exit line editor"
},
-
"elementLock": {
"lock": "Lock",
"unlock": "Unlock",
@@ -207,7 +206,22 @@
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again.",
"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."
+ "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.",
+ "brave_measure_text_error": {
+ "start": "Looks like you are using Brave browser with the",
+ "aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
+ "setting_enabled": "setting enabled",
+ "break": "This could result in breaking the",
+ "text_elements": "Text Elements",
+ "in_your_drawings": "in your drawings",
+ "strongly_recommend": "We strongly recommend disabling this setting. You can follow",
+ "steps": "these steps",
+ "how": "on how to do so",
+ "disable_setting": " If disabling this setting doesn't fix the display of text elements, please open an",
+ "issue": "issue",
+ "write": "on our GitHub, or write us on",
+ "discord": "Discord"
+ }
},
"toolBar": {
"selection": "Selection",
diff --git a/src/math.ts b/src/math.ts
index cfa28e230f..602fe976c4 100644
--- a/src/math.ts
+++ b/src/math.ts
@@ -12,6 +12,7 @@ import {
} from "./element/types";
import { getShapeForElement } from "./renderer/renderElement";
import { getCurvePathOps } from "./element/bounds";
+import { Mutable } from "./utility-types";
export const rotate = (
x1: number,
diff --git a/src/tests/__snapshots__/linearElementEditor.test.tsx.snap b/src/tests/__snapshots__/linearElementEditor.test.tsx.snap
index fa28fe75f5..10202e8b8f 100644
--- a/src/tests/__snapshots__/linearElementEditor.test.tsx.snap
+++ b/src/tests/__snapshots__/linearElementEditor.test.tsx.snap
@@ -5,7 +5,7 @@ exports[`Test Linear Elements Test bound text element should match styles for te
class="excalidraw-wysiwyg"
data-type="wysiwyg"
dir="auto"
- style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10px; height: 24px; left: 35px; top: 8px; transform-origin: 5px 12px; transform: translate(0px, 0px) scale(1) rotate(0deg) translate(0px, 0px); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -8px; font: Emoji 20px 20px; line-height: 24px; font-family: Virgil, Segoe UI Emoji;"
+ style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 24px; left: 35px; top: 8px; transform-origin: 5px 12px; transform: translate(0px, 0px) scale(1) rotate(0deg) translate(0px, 0px); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -8px; font: Emoji 20px 20px; line-height: 24px; font-family: Virgil, Segoe UI Emoji;"
tabindex="0"
wrap="off"
/>
diff --git a/src/tests/helpers/api.ts b/src/tests/helpers/api.ts
index 2461677ea5..0fcd53e941 100644
--- a/src/tests/helpers/api.ts
+++ b/src/tests/helpers/api.ts
@@ -30,6 +30,7 @@ import {
import { Point } from "../../types";
import { getSelectedElements } from "../../scene/selection";
import { isLinearElementType } from "../../element/typeChecks";
+import { Mutable } from "../../utility-types";
const readFile = util.promisify(fs.readFile);
diff --git a/src/types.ts b/src/types.ts
index a545a137c8..449e289007 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -38,6 +38,8 @@ import {
import type { FileSystemHandle } from "./data/filesystem";
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import { ContextMenuItems } from "./components/ContextMenu";
+import { Merge, ForwardRef } from "./utility-types";
+import React from "react";
export type Point = Readonly;
@@ -107,7 +109,7 @@ export type AppState = {
} | null;
showWelcomeScreen: boolean;
isLoading: boolean;
- errorMessage: string | null;
+ errorMessage: React.ReactNode;
draggingElement: NonDeletedExcalidrawElement | null;
resizingElement: NonDeletedExcalidrawElement | null;
multiElement: NonDeleted | null;
diff --git a/src/utility-types.ts b/src/utility-types.ts
new file mode 100644
index 0000000000..b84eb19946
--- /dev/null
+++ b/src/utility-types.ts
@@ -0,0 +1,49 @@
+export type Mutable = {
+ -readonly [P in keyof T]: T[P];
+};
+
+export type ValueOf = T[keyof T];
+
+export type Merge = Omit & N;
+
+/** utility type to assert that the second type is a subtype of the first type.
+ * Returns the subtype. */
+export type SubtypeOf = Subtype;
+
+export type ResolutionType any> = T extends (
+ ...args: any
+) => Promise
+ ? R
+ : any;
+
+// https://github.com/krzkaczor/ts-essentials
+export type MarkOptional = Omit &
+ Partial>;
+
+export type MarkRequired = Exclude &
+ Required>;
+
+export type MarkNonNullable = {
+ [P in K]-?: P extends K ? NonNullable : T[P];
+} & { [P in keyof T]: T[P] };
+
+export type NonOptional = Exclude;
+
+// -----------------------------------------------------------------------------
+// type getter for interface's callable type
+// src: https://stackoverflow.com/a/58658851/927631
+// -----------------------------------------------------------------------------
+export type SignatureType = T extends (...args: infer R) => any ? R : never;
+export type CallableType any> = (
+ ...args: SignatureType
+) => ReturnType;
+// --------------------------------------------------------------------------—
+
+// Type for React.forwardRef --- supply only the first generic argument T
+export type ForwardRef = Parameters<
+ CallableType>
+>[1];
+
+export type ExtractSetType> = T extends Set
+ ? U
+ : never;
diff --git a/src/utils.ts b/src/utils.ts
index b2d85d4d37..61925a6064 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -16,6 +16,7 @@ import { AppState, DataURL, LastActiveTool, Zoom } from "./types";
import { unstable_batchedUpdates } from "react-dom";
import { SHAPES } from "./shapes";
import { isEraserActive, isHandToolActive } from "./appState";
+import { ResolutionType } from "./utility-types";
let mockDateTime: string | null = null;