mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: introduce font picker (#8012)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
4c5408263c
commit
62228e0bbb
120 changed files with 3390 additions and 1106 deletions
|
@ -59,7 +59,7 @@ pre a {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
background: #70b1ec;
|
background: #70b1ec;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -872,7 +872,7 @@ export default function App({
|
||||||
files: excalidrawAPI.getFiles(),
|
files: excalidrawAPI.getFiles(),
|
||||||
});
|
});
|
||||||
const ctx = canvas.getContext("2d")!;
|
const ctx = canvas.getContext("2d")!;
|
||||||
ctx.font = "30px Virgil";
|
ctx.font = "30px Excalifont";
|
||||||
ctx.strokeText("My custom text", 50, 60);
|
ctx.strokeText("My custom text", 50, 60);
|
||||||
setCanvasUrl(canvas.toDataURL());
|
setCanvasUrl(canvas.toDataURL());
|
||||||
}}
|
}}
|
||||||
|
@ -893,7 +893,7 @@ export default function App({
|
||||||
files: excalidrawAPI.getFiles(),
|
files: excalidrawAPI.getFiles(),
|
||||||
});
|
});
|
||||||
const ctx = canvas.getContext("2d")!;
|
const ctx = canvas.getContext("2d")!;
|
||||||
ctx.font = "30px Virgil";
|
ctx.font = "30px Excalifont";
|
||||||
ctx.strokeText("My custom text", 50, 60);
|
ctx.strokeText("My custom text", 50, 60);
|
||||||
setCanvasUrl(canvas.toDataURL());
|
setCanvasUrl(canvas.toDataURL());
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -46,7 +46,7 @@ const elements: ExcalidrawElementSkeleton[] = [
|
||||||
];
|
];
|
||||||
export default {
|
export default {
|
||||||
elements,
|
elements,
|
||||||
appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 1 },
|
appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 5 },
|
||||||
scrollToContent: true,
|
scrollToContent: true,
|
||||||
libraryItems: [
|
libraryItems: [
|
||||||
[
|
[
|
||||||
|
|
3
examples/excalidraw/with-nextjs/.gitignore
vendored
3
examples/excalidraw/with-nextjs/.gitignore
vendored
|
@ -34,3 +34,6 @@ yarn-error.log*
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# copied assets
|
||||||
|
public/*.woff2
|
|
@ -3,7 +3,8 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm",
|
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm && yarn copy:assets",
|
||||||
|
"copy:assets": "cp ../../../packages/excalidraw/dist/browser/prod/excalidraw-assets/*.woff2 ./public",
|
||||||
"dev": "yarn build:workspace && next dev -p 3005",
|
"dev": "yarn build:workspace && next dev -p 3005",
|
||||||
"build": "yarn build:workspace && next build",
|
"build": "yarn build:workspace && next build",
|
||||||
"start": "next start -p 3006",
|
"start": "next start -p 3006",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import Script from "next/script";
|
||||||
import "../common.scss";
|
import "../common.scss";
|
||||||
|
|
||||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||||
|
@ -15,7 +16,9 @@ export default function Page() {
|
||||||
<>
|
<>
|
||||||
<a href="/excalidraw-in-pages">Switch to Pages router</a>
|
<a href="/excalidraw-in-pages">Switch to Pages router</a>
|
||||||
<h1 className="page-title">App Router</h1>
|
<h1 className="page-title">App Router</h1>
|
||||||
|
<Script id="load-env-variables" strategy="beforeInteractive">
|
||||||
|
{`window["EXCALIDRAW_ASSET_PATH"] = window.origin;`}
|
||||||
|
</Script>
|
||||||
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
|
{/* @ts-expect-error - https://github.com/vercel/next.js/issues/42292 */}
|
||||||
<ExcalidrawWithClientOnly />
|
<ExcalidrawWithClientOnly />
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -7,7 +7,7 @@ a {
|
||||||
color: #1c7ed6;
|
color: #1c7ed6;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 550;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.page-title {
|
||||||
|
|
2
examples/excalidraw/with-script-in-browser/.gitignore
vendored
Normal file
2
examples/excalidraw/with-script-in-browser/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# copied assets
|
||||||
|
public/*.woff2
|
|
@ -11,6 +11,7 @@
|
||||||
<title>React App</title>
|
<title>React App</title>
|
||||||
<script>
|
<script>
|
||||||
window.name = "codesandbox";
|
window.name = "codesandbox";
|
||||||
|
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/dist/browser/dev/index.css" />
|
<link rel="stylesheet" href="/dist/browser/dev/index.css" />
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -12,8 +12,10 @@
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "yarn workspace @excalidraw/excalidraw run build:esm && vite",
|
"build:workspace": "yarn workspace @excalidraw/excalidraw run build:esm && yarn copy:assets",
|
||||||
"build": "yarn workspace @excalidraw/excalidraw run build:esm && vite build",
|
"copy:assets": "cp ../../../packages/excalidraw/dist/browser/prod/excalidraw-assets/*.woff2 ./public",
|
||||||
|
"start": "yarn build:workspace && vite",
|
||||||
|
"build": "yarn build:workspace && vite build",
|
||||||
"build:preview": "yarn build && vite preview --port 5002"
|
"build:preview": "yarn build && vite preview --port 5002"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,6 +114,14 @@
|
||||||
) {
|
) {
|
||||||
window.location.href = "https://app.excalidraw.com";
|
window.location.href = "https://app.excalidraw.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// point into our CDN in prod
|
||||||
|
window.EXCALIDRAW_ASSET_PATH =
|
||||||
|
"https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/";
|
||||||
|
</script>
|
||||||
|
<% } else { %>
|
||||||
|
<script>
|
||||||
|
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
@ -124,22 +132,74 @@
|
||||||
<!-- Excalidraw version -->
|
<!-- Excalidraw version -->
|
||||||
<meta name="version" content="{version}" />
|
<meta name="version" content="{version}" />
|
||||||
|
|
||||||
|
<!-- Warmup the connection for Google fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|
||||||
|
<!-- Preload all default fonts and Virgil for backwards compatibility to avoid swap on init -->
|
||||||
|
<% if (typeof PROD != 'undefined' && PROD == true) { %>
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="/Virgil.woff2"
|
href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/Excalifont-Regular-C9eKQy_N.woff2"
|
||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="/Cascadia.woff2"
|
href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/Virgil-Regular-hO16qHwV.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/ComicShanns-Regular-D0c8wzsC.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<% } else { %>
|
||||||
|
<!-- in DEV we need to preload from the local server and without the hash -->
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="../packages/excalidraw/fonts/assets/Excalifont-Regular.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="../packages/excalidraw/fonts/assets/Virgil-Regular.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="../packages/excalidraw/fonts/assets/ComicShanns-Regular.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<!-- For Nunito only preload the latin range, which should be enough for now -->
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
|
||||||
as="font"
|
as="font"
|
||||||
type="font/woff2"
|
type="font/woff2"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/fonts/fonts.css" type="text/css" />
|
<!-- Register Assistant as the UI font, before the scene inits -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="../packages/excalidraw/fonts/assets/fonts.css"
|
||||||
|
type="text/css"
|
||||||
|
/>
|
||||||
|
|
||||||
<% if (typeof VITE_APP_DEV_DISABLE_LIVE_RELOAD != 'undefined' &&
|
<% if (typeof VITE_APP_DEV_DISABLE_LIVE_RELOAD != 'undefined' &&
|
||||||
VITE_APP_DEV_DISABLE_LIVE_RELOAD == true) { %>
|
VITE_APP_DEV_DISABLE_LIVE_RELOAD == true) { %>
|
||||||
<script>
|
<script>
|
||||||
|
@ -158,7 +218,6 @@
|
||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
<script>
|
<script>
|
||||||
window.EXCALIDRAW_ASSET_PATH = "/";
|
|
||||||
// setting this so that libraries installation reuses this window tab.
|
// setting this so that libraries installation reuses this window tab.
|
||||||
window.name = "_excalidraw";
|
window.name = "_excalidraw";
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -36,7 +36,8 @@
|
||||||
"build:version": "node ../scripts/build-version.js",
|
"build:version": "node ../scripts/build-version.js",
|
||||||
"build": "yarn build:app && yarn build:version",
|
"build": "yarn build:app && yarn build:version",
|
||||||
"start": "yarn && vite",
|
"start": "yarn && vite",
|
||||||
"start:production": "npm run build && npx http-server build -a localhost -p 5001 -o",
|
"start:production": "yarn build && yarn serve",
|
||||||
|
"serve": "npx http-server build -a localhost -p 5001 -o",
|
||||||
"build:preview": "yarn build && vite preview --port 5000"
|
"build:preview": "yarn build && vite preview --port 5000"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
||||||
class="welcome-screen-center"
|
class="welcome-screen-center"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="welcome-screen-center__logo virgil welcome-screen-decor"
|
class="welcome-screen-center__logo excalifont welcome-screen-decor"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ExcalidrawLogo is-small"
|
class="ExcalidrawLogo is-small"
|
||||||
|
@ -48,7 +48,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="welcome-screen-center__heading welcome-screen-decor virgil"
|
class="welcome-screen-center__heading welcome-screen-decor excalifont"
|
||||||
>
|
>
|
||||||
All your data is saved locally in your browser.
|
All your data is saved locally in your browser.
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
import checker from "vite-plugin-checker";
|
import checker from "vite-plugin-checker";
|
||||||
import { createHtmlPlugin } from "vite-plugin-html";
|
import { createHtmlPlugin } from "vite-plugin-html";
|
||||||
|
import { woff2BrowserPlugin } from "../scripts/woff2/woff2-vite-plugins";
|
||||||
|
|
||||||
// To load .env.local variables
|
// To load .env.local variables
|
||||||
const envVars = loadEnv("", `../`);
|
const envVars = loadEnv("", `../`);
|
||||||
|
@ -22,6 +23,14 @@ export default defineConfig({
|
||||||
outDir: "build",
|
outDir: "build",
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
|
assetFileNames(chunkInfo) {
|
||||||
|
if (chunkInfo?.name?.endsWith(".woff2")) {
|
||||||
|
// put on root so we are flexible about the CDN path
|
||||||
|
return '[name]-[hash][extname]';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'assets/[name]-[hash][extname]';
|
||||||
|
},
|
||||||
// Creating separate chunk for locales except for en and percentages.json so they
|
// Creating separate chunk for locales except for en and percentages.json so they
|
||||||
// can be cached at runtime and not merged with
|
// can be cached at runtime and not merged with
|
||||||
// app precache. en.json and percentages.json are needed for first load
|
// app precache. en.json and percentages.json are needed for first load
|
||||||
|
@ -35,12 +44,13 @@ export default defineConfig({
|
||||||
// Taking the substring after "locales/"
|
// Taking the substring after "locales/"
|
||||||
return `locales/${id.substring(index + 8)}`;
|
return `locales/${id.substring(index + 8)}`;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
woff2BrowserPlugin(),
|
||||||
react(),
|
react(),
|
||||||
checker({
|
checker({
|
||||||
typescript: true,
|
typescript: true,
|
||||||
|
|
|
@ -19,6 +19,8 @@ Please add the latest change on the top under the correct section.
|
||||||
|
|
||||||
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
|
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
|
||||||
|
|
||||||
|
- Added font picker component to have the ability to choose from a range of different fonts. Also, changed the default fonts to `Excalifont`, `Nunito` and `Comic Shanns` and deprecated `Virgil`, `Helvetica` and `Cascadia`.
|
||||||
|
|
||||||
- `MainMenu.DefaultItems.ToggleTheme` now supports `onSelect(theme: string)` callback, and optionally `allowSystemTheme: boolean` alongside `theme: string` to indicate you want to allow users to set to system theme (you need to handle this yourself). [#7853](https://github.com/excalidraw/excalidraw/pull/7853)
|
- `MainMenu.DefaultItems.ToggleTheme` now supports `onSelect(theme: string)` callback, and optionally `allowSystemTheme: boolean` alongside `theme: string` to indicate you want to allow users to set to system theme (you need to handle this yourself). [#7853](https://github.com/excalidraw/excalidraw/pull/7853)
|
||||||
|
|
||||||
- Add `useHandleLibrary`'s `opts.adapter` as the new recommended pattern to handle library initialization and persistence on library updates. [#7655](https://github.com/excalidraw/excalidraw/pull/7655)
|
- Add `useHandleLibrary`'s `opts.adapter` as the new recommended pattern to handle library initialization and persistence on library updates. [#7655](https://github.com/excalidraw/excalidraw/pull/7655)
|
||||||
|
|
|
@ -155,13 +155,15 @@ describe("element locking", () => {
|
||||||
});
|
});
|
||||||
const text = API.createElement({
|
const text = API.createElement({
|
||||||
type: "text",
|
type: "text",
|
||||||
fontFamily: FONT_FAMILY.Cascadia,
|
fontFamily: FONT_FAMILY["Comic Shanns"],
|
||||||
});
|
});
|
||||||
h.elements = [rect, text];
|
h.elements = [rect, text];
|
||||||
API.setSelectedElements([rect, text]);
|
API.setSelectedElements([rect, text]);
|
||||||
|
|
||||||
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
|
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
|
||||||
expect(queryByTestId(document.body, `font-family-code`)).toBeChecked();
|
expect(queryByTestId(document.body, `font-family-code`)).toHaveClass(
|
||||||
|
"active",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { AppClassProperties, AppState, Primitive } from "../types";
|
import type { AppClassProperties, AppState, Primitive } from "../types";
|
||||||
|
import type { StoreActionType } from "../store";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
|
@ -9,6 +11,7 @@ import { trackEvent } from "../analytics";
|
||||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||||
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
||||||
import { IconPicker } from "../components/IconPicker";
|
import { IconPicker } from "../components/IconPicker";
|
||||||
|
import { FontPicker } from "../components/FontPicker/FontPicker";
|
||||||
// TODO barnabasmolnar/editor-redesign
|
// TODO barnabasmolnar/editor-redesign
|
||||||
// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
|
// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
|
||||||
// ArrowHead icons
|
// ArrowHead icons
|
||||||
|
@ -38,9 +41,6 @@ import {
|
||||||
FontSizeExtraLargeIcon,
|
FontSizeExtraLargeIcon,
|
||||||
EdgeSharpIcon,
|
EdgeSharpIcon,
|
||||||
EdgeRoundIcon,
|
EdgeRoundIcon,
|
||||||
FreedrawIcon,
|
|
||||||
FontFamilyNormalIcon,
|
|
||||||
FontFamilyCodeIcon,
|
|
||||||
TextAlignLeftIcon,
|
TextAlignLeftIcon,
|
||||||
TextAlignCenterIcon,
|
TextAlignCenterIcon,
|
||||||
TextAlignRightIcon,
|
TextAlignRightIcon,
|
||||||
|
@ -65,10 +65,7 @@ import {
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
import {
|
import { getBoundTextElement } from "../element/textElement";
|
||||||
getBoundTextElement,
|
|
||||||
getDefaultLineHeight,
|
|
||||||
} from "../element/textElement";
|
|
||||||
import {
|
import {
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
|
@ -94,9 +91,10 @@ import {
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getFontFamilyString, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
import { Fonts, getLineHeight } from "../fonts";
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||||
|
|
||||||
|
@ -729,104 +727,391 @@ export const actionIncreaseFontSize = register({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type ChangeFontFamilyData = Partial<
|
||||||
|
Pick<
|
||||||
|
AppState,
|
||||||
|
"openPopup" | "currentItemFontFamily" | "currentHoveredFontFamily"
|
||||||
|
>
|
||||||
|
> & {
|
||||||
|
/** cache of selected & editing elements populated on opened popup */
|
||||||
|
cachedElements?: Map<string, ExcalidrawElement>;
|
||||||
|
/** flag to reset all elements to their cached versions */
|
||||||
|
resetAll?: true;
|
||||||
|
/** flag to reset all containers to their cached versions */
|
||||||
|
resetContainers?: true;
|
||||||
|
};
|
||||||
|
|
||||||
export const actionChangeFontFamily = register({
|
export const actionChangeFontFamily = register({
|
||||||
name: "changeFontFamily",
|
name: "changeFontFamily",
|
||||||
label: "labels.fontFamily",
|
label: "labels.fontFamily",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value, app) => {
|
perform: (elements, appState, value, app) => {
|
||||||
return {
|
const { cachedElements, resetAll, resetContainers, ...nextAppState } =
|
||||||
elements: changeProperty(
|
value as ChangeFontFamilyData;
|
||||||
|
|
||||||
|
if (resetAll) {
|
||||||
|
const nextElements = changeProperty(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(oldElement) => {
|
(element) => {
|
||||||
if (isTextElement(oldElement)) {
|
const cachedElement = cachedElements?.get(element.id);
|
||||||
const newElement: ExcalidrawTextElement = newElementWith(
|
if (cachedElement) {
|
||||||
oldElement,
|
const newElement = newElementWith(element, {
|
||||||
{
|
...cachedElement,
|
||||||
fontFamily: value,
|
});
|
||||||
lineHeight: getDefaultLineHeight(value),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
redrawTextBoundingBox(
|
|
||||||
newElement,
|
|
||||||
app.scene.getContainerElement(oldElement),
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
return oldElement;
|
return element;
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
),
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements: nextElements,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
...nextAppState,
|
||||||
|
},
|
||||||
|
storeAction: StoreAction.UPDATE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentItemFontFamily, currentHoveredFontFamily } = value;
|
||||||
|
|
||||||
|
let nexStoreAction: StoreActionType = StoreAction.NONE;
|
||||||
|
let nextFontFamily: FontFamilyValues | undefined;
|
||||||
|
let skipOnHoverRender = false;
|
||||||
|
|
||||||
|
if (currentItemFontFamily) {
|
||||||
|
nextFontFamily = currentItemFontFamily;
|
||||||
|
nexStoreAction = StoreAction.CAPTURE;
|
||||||
|
} else if (currentHoveredFontFamily) {
|
||||||
|
nextFontFamily = currentHoveredFontFamily;
|
||||||
|
nexStoreAction = StoreAction.NONE;
|
||||||
|
|
||||||
|
const selectedTextElements = getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
}).filter((element) => isTextElement(element));
|
||||||
|
|
||||||
|
// skip on hover re-render for more than 200 text elements or for text element with more than 5000 chars combined
|
||||||
|
if (selectedTextElements.length > 200) {
|
||||||
|
skipOnHoverRender = true;
|
||||||
|
} else {
|
||||||
|
let i = 0;
|
||||||
|
let textLengthAccumulator = 0;
|
||||||
|
|
||||||
|
while (
|
||||||
|
i < selectedTextElements.length &&
|
||||||
|
textLengthAccumulator < 5000
|
||||||
|
) {
|
||||||
|
const textElement = selectedTextElements[i] as ExcalidrawTextElement;
|
||||||
|
textLengthAccumulator += textElement?.originalText.length || 0;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textLengthAccumulator > 5000) {
|
||||||
|
skipOnHoverRender = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
currentItemFontFamily: value,
|
...nextAppState,
|
||||||
},
|
},
|
||||||
storeAction: StoreAction.CAPTURE,
|
storeAction: nexStoreAction,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (nextFontFamily && !skipOnHoverRender) {
|
||||||
|
const elementContainerMapping = new Map<
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
ExcalidrawElement | null
|
||||||
|
>();
|
||||||
|
let uniqueGlyphs = new Set<string>();
|
||||||
|
let skipFontFaceCheck = false;
|
||||||
|
|
||||||
|
const fontsCache = Array.from(Fonts.loadedFontsCache.values());
|
||||||
|
const fontFamily = Object.entries(FONT_FAMILY).find(
|
||||||
|
([_, value]) => value === nextFontFamily,
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
// skip `document.font.check` check on hover, if at least one font family has loaded as it's super slow (could result in slightly different bbox, which is fine)
|
||||||
|
if (
|
||||||
|
currentHoveredFontFamily &&
|
||||||
|
fontFamily &&
|
||||||
|
fontsCache.some((sig) => sig.startsWith(fontFamily))
|
||||||
|
) {
|
||||||
|
skipFontFaceCheck = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// following causes re-render so make sure we changed the family
|
||||||
|
// otherwise it could cause unexpected issues, such as preventing opening the popover when in wysiwyg
|
||||||
|
Object.assign(result, {
|
||||||
|
elements: changeProperty(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
(oldElement) => {
|
||||||
|
if (
|
||||||
|
isTextElement(oldElement) &&
|
||||||
|
(oldElement.fontFamily !== nextFontFamily ||
|
||||||
|
currentItemFontFamily) // force update on selection
|
||||||
|
) {
|
||||||
|
const newElement: ExcalidrawTextElement = newElementWith(
|
||||||
|
oldElement,
|
||||||
|
{
|
||||||
|
fontFamily: nextFontFamily,
|
||||||
|
lineHeight: getLineHeight(nextFontFamily!),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const cachedContainer =
|
||||||
|
cachedElements?.get(oldElement.containerId || "") || {};
|
||||||
|
|
||||||
|
const container = app.scene.getContainerElement(oldElement);
|
||||||
|
|
||||||
|
if (resetContainers && container && cachedContainer) {
|
||||||
|
// reset the container back to it's cached version
|
||||||
|
mutateElement(container, { ...cachedContainer }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipFontFaceCheck) {
|
||||||
|
uniqueGlyphs = new Set([
|
||||||
|
...uniqueGlyphs,
|
||||||
|
...Array.from(newElement.originalText),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
elementContainerMapping.set(newElement, container);
|
||||||
|
|
||||||
|
return newElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldElement;
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// size is irrelevant, but necessary
|
||||||
|
const fontString = `10px ${getFontFamilyString({
|
||||||
|
fontFamily: nextFontFamily,
|
||||||
|
})}`;
|
||||||
|
const glyphs = Array.from(uniqueGlyphs.values()).join();
|
||||||
|
|
||||||
|
if (
|
||||||
|
skipFontFaceCheck ||
|
||||||
|
window.document.fonts.check(fontString, glyphs)
|
||||||
|
) {
|
||||||
|
// we either skip the check (have at least one font face loaded) or do the check and find out all the font faces have loaded
|
||||||
|
for (const [element, container] of elementContainerMapping) {
|
||||||
|
// trigger synchronous redraw
|
||||||
|
redrawTextBoundingBox(
|
||||||
|
element,
|
||||||
|
container,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// otherwise try to load all font faces for the given glyphs and redraw elements once our font faces loaded
|
||||||
|
window.document.fonts.load(fontString, glyphs).then((fontFaces) => {
|
||||||
|
for (const [element, container] of elementContainerMapping) {
|
||||||
|
// use latest element state to ensure we don't have closure over an old instance in order to avoid possible race conditions (i.e. font faces load out-of-order while rapidly switching fonts)
|
||||||
|
const latestElement = app.scene.getElement(element.id);
|
||||||
|
const latestContainer = container
|
||||||
|
? app.scene.getElement(container.id)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (latestElement) {
|
||||||
|
// trigger async redraw
|
||||||
|
redrawTextBoundingBox(
|
||||||
|
latestElement as ExcalidrawTextElement,
|
||||||
|
latestContainer,
|
||||||
|
app.scene.getNonDeletedElementsMap(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger update once we've mutated all the elements, which also updates our cache
|
||||||
|
app.fonts.onLoaded(fontFaces);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => {
|
PanelComponent: ({ elements, appState, app, updateData }) => {
|
||||||
const options: {
|
const cachedElementsRef = useRef<Map<string, ExcalidrawElement>>(new Map());
|
||||||
value: FontFamilyValues;
|
const prevSelectedFontFamilyRef = useRef<number | null>(null);
|
||||||
text: string;
|
// relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
|
||||||
icon: JSX.Element;
|
const [batchedData, setBatchedData] = useState<ChangeFontFamilyData>({});
|
||||||
testId: string;
|
const isUnmounted = useRef(true);
|
||||||
}[] = [
|
|
||||||
{
|
const selectedFontFamily = useMemo(() => {
|
||||||
value: FONT_FAMILY.Virgil,
|
const getFontFamily = (
|
||||||
text: t("labels.handDrawn"),
|
elementsArray: readonly ExcalidrawElement[],
|
||||||
icon: FreedrawIcon,
|
elementsMap: Map<string, ExcalidrawElement>,
|
||||||
testId: "font-family-virgil",
|
) =>
|
||||||
},
|
getFormValue(
|
||||||
{
|
elementsArray,
|
||||||
value: FONT_FAMILY.Helvetica,
|
appState,
|
||||||
text: t("labels.normal"),
|
(element) => {
|
||||||
icon: FontFamilyNormalIcon,
|
if (isTextElement(element)) {
|
||||||
testId: "font-family-normal",
|
return element.fontFamily;
|
||||||
},
|
}
|
||||||
{
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
value: FONT_FAMILY.Cascadia,
|
if (boundTextElement) {
|
||||||
text: t("labels.code"),
|
return boundTextElement.fontFamily;
|
||||||
icon: FontFamilyCodeIcon,
|
}
|
||||||
testId: "font-family-code",
|
return null;
|
||||||
},
|
},
|
||||||
];
|
(element) =>
|
||||||
|
isTextElement(element) ||
|
||||||
|
getBoundTextElement(element, elementsMap) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection
|
||||||
|
? null
|
||||||
|
: appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
||||||
|
);
|
||||||
|
|
||||||
|
// popup opened, use cached elements
|
||||||
|
if (
|
||||||
|
batchedData.openPopup === "fontFamily" &&
|
||||||
|
appState.openPopup === "fontFamily"
|
||||||
|
) {
|
||||||
|
return getFontFamily(
|
||||||
|
Array.from(cachedElementsRef.current?.values() ?? []),
|
||||||
|
cachedElementsRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// popup closed, use all elements
|
||||||
|
if (!batchedData.openPopup && appState.openPopup !== "fontFamily") {
|
||||||
|
return getFontFamily(elements, app.scene.getNonDeletedElementsMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// popup props are not in sync, hence we are in the middle of an update, so keeping the previous value we've had
|
||||||
|
return prevSelectedFontFamilyRef.current;
|
||||||
|
}, [batchedData.openPopup, appState, elements, app.scene]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
prevSelectedFontFamilyRef.current = selectedFontFamily;
|
||||||
|
}, [selectedFontFamily]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(batchedData).length) {
|
||||||
|
updateData(batchedData);
|
||||||
|
// reset the data after we've used the data
|
||||||
|
setBatchedData({});
|
||||||
|
}
|
||||||
|
// call update only on internal state changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [batchedData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isUnmounted.current = false;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isUnmounted.current = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.fontFamily")}</legend>
|
<legend>{t("labels.fontFamily")}</legend>
|
||||||
<ButtonIconSelect<FontFamilyValues | false>
|
<FontPicker
|
||||||
group="font-family"
|
isOpened={appState.openPopup === "fontFamily"}
|
||||||
options={options}
|
selectedFontFamily={selectedFontFamily}
|
||||||
value={getFormValue(
|
hoveredFontFamily={appState.currentHoveredFontFamily}
|
||||||
elements,
|
onSelect={(fontFamily) => {
|
||||||
appState,
|
setBatchedData({
|
||||||
(element) => {
|
openPopup: null,
|
||||||
if (isTextElement(element)) {
|
currentHoveredFontFamily: null,
|
||||||
return element.fontFamily;
|
currentItemFontFamily: fontFamily,
|
||||||
|
});
|
||||||
|
|
||||||
|
// defensive clear so immediate close won't abuse the cached elements
|
||||||
|
cachedElementsRef.current.clear();
|
||||||
|
}}
|
||||||
|
onHover={(fontFamily) => {
|
||||||
|
setBatchedData({
|
||||||
|
currentHoveredFontFamily: fontFamily,
|
||||||
|
cachedElements: new Map(cachedElementsRef.current),
|
||||||
|
resetContainers: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onLeave={() => {
|
||||||
|
setBatchedData({
|
||||||
|
currentHoveredFontFamily: null,
|
||||||
|
cachedElements: new Map(cachedElementsRef.current),
|
||||||
|
resetAll: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onPopupChange={(open) => {
|
||||||
|
if (open) {
|
||||||
|
// open, populate the cache from scratch
|
||||||
|
cachedElementsRef.current.clear();
|
||||||
|
|
||||||
|
const { editingElement } = appState;
|
||||||
|
|
||||||
|
if (editingElement?.type === "text") {
|
||||||
|
// retrieve the latest version from the scene, as `editingElement` isn't mutated
|
||||||
|
const latestEditingElement = app.scene.getElement(
|
||||||
|
editingElement.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// inside the wysiwyg editor
|
||||||
|
cachedElementsRef.current.set(
|
||||||
|
editingElement.id,
|
||||||
|
newElementWith(
|
||||||
|
latestEditingElement || editingElement,
|
||||||
|
{},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const element of selectedElements) {
|
||||||
|
cachedElementsRef.current.set(
|
||||||
|
element.id,
|
||||||
|
newElementWith(element, {}, true),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const boundTextElement = getBoundTextElement(
|
|
||||||
element,
|
setBatchedData({
|
||||||
app.scene.getNonDeletedElementsMap(),
|
openPopup: "fontFamily",
|
||||||
);
|
});
|
||||||
if (boundTextElement) {
|
} else {
|
||||||
return boundTextElement.fontFamily;
|
// close, use the cache and clear it afterwards
|
||||||
|
const data = {
|
||||||
|
openPopup: null,
|
||||||
|
currentHoveredFontFamily: null,
|
||||||
|
cachedElements: new Map(cachedElementsRef.current),
|
||||||
|
resetAll: true,
|
||||||
|
} as ChangeFontFamilyData;
|
||||||
|
|
||||||
|
if (isUnmounted.current) {
|
||||||
|
// in case the component was unmounted by the parent, trigger the update directly
|
||||||
|
updateData({ ...batchedData, ...data });
|
||||||
|
} else {
|
||||||
|
setBatchedData(data);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
},
|
cachedElementsRef.current.clear();
|
||||||
(element) =>
|
}
|
||||||
isTextElement(element) ||
|
}}
|
||||||
getBoundTextElement(
|
|
||||||
element,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
) !== null,
|
|
||||||
(hasSelection) =>
|
|
||||||
hasSelection
|
|
||||||
? null
|
|
||||||
: appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
|
||||||
)}
|
|
||||||
onChange={(value) => updateData(value)}
|
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,10 +12,7 @@ import {
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import {
|
import { getBoundTextElement } from "../element/textElement";
|
||||||
getBoundTextElement,
|
|
||||||
getDefaultLineHeight,
|
|
||||||
} from "../element/textElement";
|
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
canApplyRoundnessTypeToElement,
|
canApplyRoundnessTypeToElement,
|
||||||
|
@ -27,6 +24,7 @@ import { getSelectedElements } from "../scene";
|
||||||
import type { ExcalidrawTextElement } from "../element/types";
|
import type { ExcalidrawTextElement } from "../element/types";
|
||||||
import { paintIcon } from "../components/icons";
|
import { paintIcon } from "../components/icons";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
import { getLineHeight } from "../fonts";
|
||||||
|
|
||||||
// `copiedStyles` is exported only for tests.
|
// `copiedStyles` is exported only for tests.
|
||||||
export let copiedStyles: string = "{}";
|
export let copiedStyles: string = "{}";
|
||||||
|
@ -122,7 +120,7 @@ export const actionPasteStyles = register({
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
lineHeight:
|
lineHeight:
|
||||||
(elementStylesToCopyFrom as ExcalidrawTextElement).lineHeight ||
|
(elementStylesToCopyFrom as ExcalidrawTextElement).lineHeight ||
|
||||||
getDefaultLineHeight(fontFamily),
|
getLineHeight(fontFamily),
|
||||||
});
|
});
|
||||||
let container = null;
|
let container = null;
|
||||||
if (newElement.containerId) {
|
if (newElement.containerId) {
|
||||||
|
|
|
@ -36,6 +36,7 @@ export const getDefaultAppState = (): Omit<
|
||||||
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
|
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
|
||||||
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
||||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||||
|
currentHoveredFontFamily: null,
|
||||||
cursorButton: "up",
|
cursorButton: "up",
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
|
@ -149,6 +150,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||||
currentItemStrokeStyle: { browser: true, export: false, server: false },
|
currentItemStrokeStyle: { browser: true, export: false, server: false },
|
||||||
currentItemStrokeWidth: { browser: true, export: false, server: false },
|
currentItemStrokeWidth: { browser: true, export: false, server: false },
|
||||||
currentItemTextAlign: { browser: true, export: false, server: false },
|
currentItemTextAlign: { browser: true, export: false, server: false },
|
||||||
|
currentHoveredFontFamily: { browser: false, export: false, server: false },
|
||||||
cursorButton: { browser: true, export: false, server: false },
|
cursorButton: { browser: true, export: false, server: false },
|
||||||
activeEmbeddable: { browser: false, export: false, server: false },
|
activeEmbeddable: { browser: false, export: false, server: false },
|
||||||
draggingElement: { browser: false, export: false, server: false },
|
draggingElement: { browser: false, export: false, server: false },
|
||||||
|
|
|
@ -158,10 +158,8 @@ export const SelectedShapeActions = ({
|
||||||
{(appState.activeTool.type === "text" ||
|
{(appState.activeTool.type === "text" ||
|
||||||
targetElements.some(isTextElement)) && (
|
targetElements.some(isTextElement)) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeFontSize")}
|
|
||||||
|
|
||||||
{renderAction("changeFontFamily")}
|
{renderAction("changeFontFamily")}
|
||||||
|
{renderAction("changeFontSize")}
|
||||||
{(appState.activeTool.type === "text" ||
|
{(appState.activeTool.type === "text" ||
|
||||||
suppportsHorizontalAlign(targetElements, elementsMap)) &&
|
suppportsHorizontalAlign(targetElements, elementsMap)) &&
|
||||||
renderAction("changeTextAlign")}
|
renderAction("changeTextAlign")}
|
||||||
|
|
|
@ -321,7 +321,6 @@ import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCenter,
|
getContainerCenter,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getDefaultLineHeight,
|
|
||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
getMinTextElementWidth,
|
getMinTextElementWidth,
|
||||||
isMeasureTextSupported,
|
isMeasureTextSupported,
|
||||||
|
@ -337,7 +336,7 @@ import {
|
||||||
import { isLocalLink, normalizeLink, toValidURL } from "../data/url";
|
import { isLocalLink, normalizeLink, toValidURL } from "../data/url";
|
||||||
import { shouldShowBoundingBox } from "../element/transformHandles";
|
import { shouldShowBoundingBox } from "../element/transformHandles";
|
||||||
import { actionUnlockAllElements } from "../actions/actionElementLock";
|
import { actionUnlockAllElements } from "../actions/actionElementLock";
|
||||||
import { Fonts } from "../scene/Fonts";
|
import { Fonts, getLineHeight } from "../fonts";
|
||||||
import {
|
import {
|
||||||
getFrameChildren,
|
getFrameChildren,
|
||||||
isCursorInFrame,
|
isCursorInFrame,
|
||||||
|
@ -532,8 +531,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
public scene: Scene;
|
public scene: Scene;
|
||||||
|
public fonts: Fonts;
|
||||||
public renderer: Renderer;
|
public renderer: Renderer;
|
||||||
private fonts: Fonts;
|
|
||||||
private resizeObserver: ResizeObserver | undefined;
|
private resizeObserver: ResizeObserver | undefined;
|
||||||
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
private nearestScrollableContainer: HTMLElement | Document | undefined;
|
||||||
public library: AppClassProperties["library"];
|
public library: AppClassProperties["library"];
|
||||||
|
@ -2335,11 +2334,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 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
|
|
||||||
// seems faster even in browsers that do fire the loadingdone event.
|
|
||||||
this.fonts.loadFontsForElements(scene.elements);
|
|
||||||
|
|
||||||
this.resetStore();
|
this.resetStore();
|
||||||
this.resetHistory();
|
this.resetHistory();
|
||||||
|
@ -2347,6 +2341,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
...scene,
|
...scene,
|
||||||
storeAction: StoreAction.UPDATE,
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FontFaceSet loadingdone event we listen on may not always
|
||||||
|
// fire (looking at you Safari), so on init we manually load all
|
||||||
|
// fonts and rerender scene text elements once done. This also
|
||||||
|
// seems faster even in browsers that do fire the loadingdone event.
|
||||||
|
this.fonts.load();
|
||||||
};
|
};
|
||||||
|
|
||||||
private isMobileBreakpoint = (width: number, height: number) => {
|
private isMobileBreakpoint = (width: number, height: number) => {
|
||||||
|
@ -2439,6 +2439,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: this.store,
|
value: this.store,
|
||||||
},
|
},
|
||||||
|
fonts: {
|
||||||
|
configurable: true,
|
||||||
|
value: this.fonts,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2576,7 +2580,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
// rerender text elements on font load to fix #637 && #1553
|
// rerender text elements on font load to fix #637 && #1553
|
||||||
addEventListener(document.fonts, "loadingdone", (event) => {
|
addEventListener(document.fonts, "loadingdone", (event) => {
|
||||||
const loadedFontFaces = (event as FontFaceSetLoadEvent).fontfaces;
|
const loadedFontFaces = (event as FontFaceSetLoadEvent).fontfaces;
|
||||||
this.fonts.onFontsLoaded(loadedFontFaces);
|
this.fonts.onLoaded(loadedFontFaces);
|
||||||
}),
|
}),
|
||||||
// Safari-only desktop pinch zoom
|
// Safari-only desktop pinch zoom
|
||||||
addEventListener(
|
addEventListener(
|
||||||
|
@ -3379,7 +3383,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
fontSize: textElementProps.fontSize,
|
fontSize: textElementProps.fontSize,
|
||||||
fontFamily: textElementProps.fontFamily,
|
fontFamily: textElementProps.fontFamily,
|
||||||
});
|
});
|
||||||
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
|
const lineHeight = getLineHeight(textElementProps.fontFamily);
|
||||||
const [x1, , x2] = getVisibleSceneBounds(this.state);
|
const [x1, , x2] = getVisibleSceneBounds(this.state);
|
||||||
// long texts should not go beyond 800 pixels in width nor should it go below 200 px
|
// long texts should not go beyond 800 pixels in width nor should it go below 200 px
|
||||||
const maxTextWidth = Math.max(Math.min((x2 - x1) * 0.5, 800), 200);
|
const maxTextWidth = Math.max(Math.min((x2 - x1) * 0.5, 800), 200);
|
||||||
|
@ -3397,13 +3401,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
let metrics = measureText(originalText, fontString, lineHeight);
|
let metrics = measureText(originalText, fontString, lineHeight);
|
||||||
const isTextWrapped = metrics.width > maxTextWidth;
|
const isTextUnwrapped = metrics.width > maxTextWidth;
|
||||||
|
|
||||||
const text = isTextWrapped
|
const text = isTextUnwrapped
|
||||||
? wrapText(originalText, fontString, maxTextWidth)
|
? wrapText(originalText, fontString, maxTextWidth)
|
||||||
: originalText;
|
: originalText;
|
||||||
|
|
||||||
metrics = isTextWrapped
|
metrics = isTextUnwrapped
|
||||||
? measureText(text, fontString, lineHeight)
|
? measureText(text, fontString, lineHeight)
|
||||||
: metrics;
|
: metrics;
|
||||||
|
|
||||||
|
@ -3417,7 +3421,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
text,
|
text,
|
||||||
originalText,
|
originalText,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
autoResize: !isTextWrapped,
|
autoResize: !isTextUnwrapped,
|
||||||
frameId: topLayerFrame ? topLayerFrame.id : null,
|
frameId: topLayerFrame ? topLayerFrame.id : null,
|
||||||
});
|
});
|
||||||
acc.push(element);
|
acc.push(element);
|
||||||
|
@ -4107,6 +4111,36 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
event.shiftKey &&
|
||||||
|
event.key.toLowerCase() === KEYS.F
|
||||||
|
) {
|
||||||
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.state.activeTool.type === "selection" &&
|
||||||
|
!selectedElements.length
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.state.activeTool.type === "text" ||
|
||||||
|
selectedElements.find(
|
||||||
|
(element) =>
|
||||||
|
isTextElement(element) ||
|
||||||
|
getBoundTextElement(
|
||||||
|
element,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState({ openPopup: "fontFamily" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
|
if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
|
||||||
if (this.state.activeTool.type === "laser") {
|
if (this.state.activeTool.type === "laser") {
|
||||||
this.setActiveTool({ type: "selection" });
|
this.setActiveTool({ type: "selection" });
|
||||||
|
@ -4761,7 +4795,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
existingTextElement?.fontFamily || this.state.currentItemFontFamily;
|
existingTextElement?.fontFamily || this.state.currentItemFontFamily;
|
||||||
|
|
||||||
const lineHeight =
|
const lineHeight =
|
||||||
existingTextElement?.lineHeight || getDefaultLineHeight(fontFamily);
|
existingTextElement?.lineHeight || getLineHeight(fontFamily);
|
||||||
const fontSize = this.state.currentItemFontSize;
|
const fontSize = this.state.currentItemFontSize;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
12
packages/excalidraw/components/ButtonIcon.scss
Normal file
12
packages/excalidraw/components/ButtonIcon.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
@import "../css/theme";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
button.standalone {
|
||||||
|
@include outlineButtonIconStyles;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
// dissalow pointer events on children, so we always have event.target on the button itself
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
packages/excalidraw/components/ButtonIcon.tsx
Normal file
36
packages/excalidraw/components/ButtonIcon.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import "./ButtonIcon.scss";
|
||||||
|
|
||||||
|
interface ButtonIconProps {
|
||||||
|
icon: JSX.Element;
|
||||||
|
title: string;
|
||||||
|
className?: string;
|
||||||
|
testId?: string;
|
||||||
|
/** if not supplied, defaults to value identity check */
|
||||||
|
active?: boolean;
|
||||||
|
/** include standalone style (could interfere with parent styles) */
|
||||||
|
standalone?: boolean;
|
||||||
|
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonIcon = forwardRef<HTMLButtonElement, ButtonIconProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const { title, className, testId, active, standalone, icon, onClick } =
|
||||||
|
props;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
ref={ref}
|
||||||
|
key={title}
|
||||||
|
title={title}
|
||||||
|
data-testid={testId}
|
||||||
|
className={clsx(className, { standalone, active })}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
|
@ -1,4 +1,5 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { ButtonIcon } from "./ButtonIcon";
|
||||||
|
|
||||||
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
||||||
export const ButtonIconSelect = <T extends Object>(
|
export const ButtonIconSelect = <T extends Object>(
|
||||||
|
@ -24,21 +25,17 @@ export const ButtonIconSelect = <T extends Object>(
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
) => (
|
) => (
|
||||||
<div className="buttonList buttonListIcon">
|
<div className="buttonList">
|
||||||
{props.options.map((option) =>
|
{props.options.map((option) =>
|
||||||
props.type === "button" ? (
|
props.type === "button" ? (
|
||||||
<button
|
<ButtonIcon
|
||||||
type="button"
|
|
||||||
key={option.text}
|
key={option.text}
|
||||||
onClick={(event) => props.onClick(option.value, event)}
|
icon={option.icon}
|
||||||
className={clsx({
|
|
||||||
active: option.active ?? props.value === option.value,
|
|
||||||
})}
|
|
||||||
data-testid={option.testId}
|
|
||||||
title={option.text}
|
title={option.text}
|
||||||
>
|
testId={option.testId}
|
||||||
{option.icon}
|
active={option.active ?? props.value === option.value}
|
||||||
</button>
|
onClick={(event) => props.onClick(option.value, event)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<label
|
<label
|
||||||
key={option.text}
|
key={option.text}
|
||||||
|
|
10
packages/excalidraw/components/ButtonSeparator.tsx
Normal file
10
packages/excalidraw/components/ButtonSeparator.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export const ButtonSeparator = () => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 1,
|
||||||
|
height: "1rem",
|
||||||
|
backgroundColor: "var(--default-border-color)",
|
||||||
|
margin: "0 auto",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
|
@ -20,7 +20,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@include isMobile {
|
@include isMobile {
|
||||||
max-width: 175px;
|
max-width: 11rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,24 @@
|
||||||
import { isInteractive, isTransparent, isWritableElement } from "../../utils";
|
import { isTransparent } from "../../utils";
|
||||||
import type { ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
import { TopPicks } from "./TopPicks";
|
import { TopPicks } from "./TopPicks";
|
||||||
|
import { ButtonSeparator } from "../ButtonSeparator";
|
||||||
import { Picker } from "./Picker";
|
import { Picker } from "./Picker";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import type { ColorPickerType } from "./colorPickerUtils";
|
import type { ColorPickerType } from "./colorPickerUtils";
|
||||||
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
||||||
import { useDevice, useExcalidrawContainer } from "../App";
|
import { useExcalidrawContainer } from "../App";
|
||||||
import type { ColorTuple, ColorPaletteCustom } from "../../colors";
|
import type { ColorTuple, ColorPaletteCustom } from "../../colors";
|
||||||
import { COLOR_PALETTE } from "../../colors";
|
import { COLOR_PALETTE } from "../../colors";
|
||||||
import PickerHeading from "./PickerHeading";
|
import PickerHeading from "./PickerHeading";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { useRef } from "react";
|
||||||
import { jotaiScope } from "../../jotai";
|
import { jotaiScope } from "../../jotai";
|
||||||
import { ColorInput } from "./ColorInput";
|
import { ColorInput } from "./ColorInput";
|
||||||
import { useRef } from "react";
|
|
||||||
import { activeEyeDropperAtom } from "../EyeDropper";
|
import { activeEyeDropperAtom } from "../EyeDropper";
|
||||||
|
import { PropertiesPopover } from "../PropertiesPopover";
|
||||||
|
|
||||||
import "./ColorPicker.scss";
|
import "./ColorPicker.scss";
|
||||||
|
|
||||||
|
@ -71,6 +73,7 @@ const ColorPickerPopupContent = ({
|
||||||
| "palette"
|
| "palette"
|
||||||
| "updateData"
|
| "updateData"
|
||||||
>) => {
|
>) => {
|
||||||
|
const { container } = useExcalidrawContainer();
|
||||||
const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom);
|
const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom);
|
||||||
|
|
||||||
const [eyeDropperState, setEyeDropperState] = useAtom(
|
const [eyeDropperState, setEyeDropperState] = useAtom(
|
||||||
|
@ -78,9 +81,6 @@ const ColorPickerPopupContent = ({
|
||||||
jotaiScope,
|
jotaiScope,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { container } = useExcalidrawContainer();
|
|
||||||
const device = useDevice();
|
|
||||||
|
|
||||||
const colorInputJSX = (
|
const colorInputJSX = (
|
||||||
<div>
|
<div>
|
||||||
<PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
|
<PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
|
||||||
|
@ -94,6 +94,7 @@ const ColorPickerPopupContent = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const focusPickerContent = () => {
|
const focusPickerContent = () => {
|
||||||
|
@ -103,120 +104,73 @@ const ColorPickerPopupContent = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Portal container={container}>
|
<PropertiesPopover
|
||||||
<Popover.Content
|
container={container}
|
||||||
ref={popoverRef}
|
style={{ maxWidth: "208px" }}
|
||||||
className="focus-visible-none"
|
onFocusOutside={(event) => {
|
||||||
data-prevent-outside-click
|
// refocus due to eye dropper
|
||||||
onFocusOutside={(event) => {
|
focusPickerContent();
|
||||||
focusPickerContent();
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
onPointerDownOutside={(event) => {
|
||||||
|
if (eyeDropperState) {
|
||||||
|
// prevent from closing if we click outside the popover
|
||||||
|
// while eyedropping (e.g. click when clicking the sidebar;
|
||||||
|
// the eye-dropper-backdrop is prevented downstream)
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}}
|
|
||||||
onPointerDownOutside={(event) => {
|
|
||||||
if (eyeDropperState) {
|
|
||||||
// prevent from closing if we click outside the popover
|
|
||||||
// while eyedropping (e.g. click when clicking the sidebar;
|
|
||||||
// the eye-dropper-backdrop is prevented downstream)
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onCloseAutoFocus={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
// prevents focusing the trigger
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// return focus to excalidraw container unless
|
|
||||||
// user focuses an interactive element, such as a button, or
|
|
||||||
// enters the text editor by clicking on canvas with the text tool
|
|
||||||
if (container && !isInteractive(document.activeElement)) {
|
|
||||||
container.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateData({ openPopup: null });
|
|
||||||
setActiveColorPickerSection(null);
|
|
||||||
}}
|
|
||||||
side={
|
|
||||||
device.editor.isMobile && !device.viewport.isLandscape
|
|
||||||
? "bottom"
|
|
||||||
: "right"
|
|
||||||
}
|
}
|
||||||
align={
|
}}
|
||||||
device.editor.isMobile && !device.viewport.isLandscape
|
onClose={() => {
|
||||||
? "center"
|
updateData({ openPopup: null });
|
||||||
: "start"
|
setActiveColorPickerSection(null);
|
||||||
}
|
}}
|
||||||
alignOffset={-16}
|
>
|
||||||
sideOffset={20}
|
{palette ? (
|
||||||
style={{
|
<Picker
|
||||||
zIndex: "var(--zIndex-layerUI)",
|
palette={palette}
|
||||||
backgroundColor: "var(--popup-bg-color)",
|
color={color}
|
||||||
maxWidth: "208px",
|
onChange={(changedColor) => {
|
||||||
maxHeight: window.innerHeight,
|
onChange(changedColor);
|
||||||
padding: "12px",
|
}}
|
||||||
borderRadius: "8px",
|
onEyeDropperToggle={(force) => {
|
||||||
boxSizing: "border-box",
|
setEyeDropperState((state) => {
|
||||||
overflowY: "auto",
|
if (force) {
|
||||||
boxShadow:
|
state = state || {
|
||||||
"0px 7px 14px rgba(0, 0, 0, 0.05), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 0.931014px rgba(0, 0, 0, 0.1702)",
|
keepOpenOnAlt: true,
|
||||||
}}
|
onSelect: onChange,
|
||||||
>
|
colorPickerType: type,
|
||||||
{palette ? (
|
};
|
||||||
<Picker
|
state.keepOpenOnAlt = true;
|
||||||
palette={palette}
|
return state;
|
||||||
color={color}
|
}
|
||||||
onChange={(changedColor) => {
|
|
||||||
onChange(changedColor);
|
return force === false || state
|
||||||
}}
|
? null
|
||||||
onEyeDropperToggle={(force) => {
|
: {
|
||||||
setEyeDropperState((state) => {
|
keepOpenOnAlt: false,
|
||||||
if (force) {
|
|
||||||
state = state || {
|
|
||||||
keepOpenOnAlt: true,
|
|
||||||
onSelect: onChange,
|
onSelect: onChange,
|
||||||
colorPickerType: type,
|
colorPickerType: type,
|
||||||
};
|
};
|
||||||
state.keepOpenOnAlt = true;
|
});
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
return force === false || state
|
|
||||||
? null
|
|
||||||
: {
|
|
||||||
keepOpenOnAlt: false,
|
|
||||||
onSelect: onChange,
|
|
||||||
colorPickerType: type,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onEscape={(event) => {
|
|
||||||
if (eyeDropperState) {
|
|
||||||
setEyeDropperState(null);
|
|
||||||
} else if (isWritableElement(event.target)) {
|
|
||||||
focusPickerContent();
|
|
||||||
} else {
|
|
||||||
updateData({ openPopup: null });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
label={label}
|
|
||||||
type={type}
|
|
||||||
elements={elements}
|
|
||||||
updateData={updateData}
|
|
||||||
>
|
|
||||||
{colorInputJSX}
|
|
||||||
</Picker>
|
|
||||||
) : (
|
|
||||||
colorInputJSX
|
|
||||||
)}
|
|
||||||
<Popover.Arrow
|
|
||||||
width={20}
|
|
||||||
height={10}
|
|
||||||
style={{
|
|
||||||
fill: "var(--popup-bg-color)",
|
|
||||||
filter: "drop-shadow(rgba(0, 0, 0, 0.05) 0px 3px 2px)",
|
|
||||||
}}
|
}}
|
||||||
/>
|
onEscape={(event) => {
|
||||||
</Popover.Content>
|
if (eyeDropperState) {
|
||||||
</Popover.Portal>
|
setEyeDropperState(null);
|
||||||
|
} else {
|
||||||
|
updateData({ openPopup: null });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
label={label}
|
||||||
|
type={type}
|
||||||
|
elements={elements}
|
||||||
|
updateData={updateData}
|
||||||
|
>
|
||||||
|
{colorInputJSX}
|
||||||
|
</Picker>
|
||||||
|
) : (
|
||||||
|
colorInputJSX
|
||||||
|
)}
|
||||||
|
</PropertiesPopover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -232,7 +186,7 @@ const ColorPickerTrigger = ({
|
||||||
return (
|
return (
|
||||||
<Popover.Trigger
|
<Popover.Trigger
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx("color-picker__button active-color", {
|
className={clsx("color-picker__button active-color properties-trigger", {
|
||||||
"is-transparent": color === "transparent" || !color,
|
"is-transparent": color === "transparent" || !color,
|
||||||
})}
|
})}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
|
@ -268,14 +222,7 @@ export const ColorPicker = ({
|
||||||
type={type}
|
type={type}
|
||||||
topPicks={topPicks}
|
topPicks={topPicks}
|
||||||
/>
|
/>
|
||||||
<div
|
<ButtonSeparator />
|
||||||
style={{
|
|
||||||
width: 1,
|
|
||||||
height: "100%",
|
|
||||||
backgroundColor: "var(--default-border-color)",
|
|
||||||
margin: "0 auto",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={appState.openPopup === type}
|
open={appState.openPopup === type}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
|
|
@ -138,7 +138,7 @@ export const Picker = ({
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="color-picker-content"
|
className="color-picker-content properties-content"
|
||||||
// to allow focusing by clicking but not by tabbing
|
// to allow focusing by clicking but not by tabbing
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
|
|
15
packages/excalidraw/components/FontPicker/FontPicker.scss
Normal file
15
packages/excalidraw/components/FontPicker/FontPicker.scss
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
@import "../../css/variables.module.scss";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.FontPicker__container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: calc(1rem + 3 * var(--default-button-size)) 1rem 1fr; // calc ~ 2 gaps + 4 buttons
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
max-width: calc(
|
||||||
|
2rem + 4 * var(--default-button-size)
|
||||||
|
); // 4 gaps + 4 buttons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
110
packages/excalidraw/components/FontPicker/FontPicker.tsx
Normal file
110
packages/excalidraw/components/FontPicker/FontPicker.tsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import React, { useCallback, useMemo } from "react";
|
||||||
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
|
import { FontPickerList } from "./FontPickerList";
|
||||||
|
import { FontPickerTrigger } from "./FontPickerTrigger";
|
||||||
|
import { ButtonIconSelect } from "../ButtonIconSelect";
|
||||||
|
import {
|
||||||
|
FontFamilyCodeIcon,
|
||||||
|
FontFamilyNormalIcon,
|
||||||
|
FreedrawIcon,
|
||||||
|
} from "../icons";
|
||||||
|
import { ButtonSeparator } from "../ButtonSeparator";
|
||||||
|
import type { FontFamilyValues } from "../../element/types";
|
||||||
|
import { FONT_FAMILY } from "../../constants";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
|
||||||
|
import "./FontPicker.scss";
|
||||||
|
|
||||||
|
export const DEFAULT_FONTS = [
|
||||||
|
{
|
||||||
|
value: FONT_FAMILY.Excalifont,
|
||||||
|
icon: FreedrawIcon,
|
||||||
|
text: t("labels.handDrawn"),
|
||||||
|
testId: "font-family-handrawn",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: FONT_FAMILY.Nunito,
|
||||||
|
icon: FontFamilyNormalIcon,
|
||||||
|
text: t("labels.normal"),
|
||||||
|
testId: "font-family-normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: FONT_FAMILY["Comic Shanns"],
|
||||||
|
icon: FontFamilyCodeIcon,
|
||||||
|
text: t("labels.code"),
|
||||||
|
testId: "font-family-code",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultFontFamilies = new Set(DEFAULT_FONTS.map((x) => x.value));
|
||||||
|
|
||||||
|
export const isDefaultFont = (fontFamily: number | null) => {
|
||||||
|
if (!fontFamily) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultFontFamilies.has(fontFamily);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FontPickerProps {
|
||||||
|
isOpened: boolean;
|
||||||
|
selectedFontFamily: FontFamilyValues | null;
|
||||||
|
hoveredFontFamily: FontFamilyValues | null;
|
||||||
|
onSelect: (fontFamily: FontFamilyValues) => void;
|
||||||
|
onHover: (fontFamily: FontFamilyValues) => void;
|
||||||
|
onLeave: () => void;
|
||||||
|
onPopupChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FontPicker = React.memo(
|
||||||
|
({
|
||||||
|
isOpened,
|
||||||
|
selectedFontFamily,
|
||||||
|
hoveredFontFamily,
|
||||||
|
onSelect,
|
||||||
|
onHover,
|
||||||
|
onLeave,
|
||||||
|
onPopupChange,
|
||||||
|
}: FontPickerProps) => {
|
||||||
|
const defaultFonts = useMemo(() => DEFAULT_FONTS, []);
|
||||||
|
const onSelectCallback = useCallback(
|
||||||
|
(value: number | false) => {
|
||||||
|
if (value) {
|
||||||
|
onSelect(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSelect],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div role="dialog" aria-modal="true" className="FontPicker__container">
|
||||||
|
<ButtonIconSelect<FontFamilyValues | false>
|
||||||
|
type="button"
|
||||||
|
options={defaultFonts}
|
||||||
|
value={selectedFontFamily}
|
||||||
|
onClick={onSelectCallback}
|
||||||
|
/>
|
||||||
|
<ButtonSeparator />
|
||||||
|
<Popover.Root open={isOpened} onOpenChange={onPopupChange}>
|
||||||
|
<FontPickerTrigger selectedFontFamily={selectedFontFamily} />
|
||||||
|
{isOpened && (
|
||||||
|
<FontPickerList
|
||||||
|
selectedFontFamily={selectedFontFamily}
|
||||||
|
hoveredFontFamily={hoveredFontFamily}
|
||||||
|
onSelect={onSelectCallback}
|
||||||
|
onHover={onHover}
|
||||||
|
onLeave={onLeave}
|
||||||
|
onOpen={() => onPopupChange(true)}
|
||||||
|
onClose={() => onPopupChange(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popover.Root>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(prev, next) =>
|
||||||
|
prev.isOpened === next.isOpened &&
|
||||||
|
prev.selectedFontFamily === next.selectedFontFamily &&
|
||||||
|
prev.hoveredFontFamily === next.hoveredFontFamily,
|
||||||
|
);
|
268
packages/excalidraw/components/FontPicker/FontPickerList.tsx
Normal file
268
packages/excalidraw/components/FontPicker/FontPickerList.tsx
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
import React, {
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
type KeyboardEventHandler,
|
||||||
|
} from "react";
|
||||||
|
import { useApp, useAppProps, useExcalidrawContainer } from "../App";
|
||||||
|
import { PropertiesPopover } from "../PropertiesPopover";
|
||||||
|
import { QuickSearch } from "../QuickSearch";
|
||||||
|
import { ScrollableList } from "../ScrollableList";
|
||||||
|
import DropdownMenuGroup from "../dropdownMenu/DropdownMenuGroup";
|
||||||
|
import DropdownMenuItem, {
|
||||||
|
DropDownMenuItemBadgeType,
|
||||||
|
DropDownMenuItemBadge,
|
||||||
|
} from "../dropdownMenu/DropdownMenuItem";
|
||||||
|
import { type FontFamilyValues } from "../../element/types";
|
||||||
|
import { arrayToList, debounce, getFontFamilyString } from "../../utils";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
import { fontPickerKeyHandler } from "./keyboardNavHandlers";
|
||||||
|
import { Fonts } from "../../fonts";
|
||||||
|
import type { ValueOf } from "../../utility-types";
|
||||||
|
|
||||||
|
export interface FontDescriptor {
|
||||||
|
value: number;
|
||||||
|
icon: JSX.Element;
|
||||||
|
text: string;
|
||||||
|
deprecated?: true;
|
||||||
|
badge?: {
|
||||||
|
type: ValueOf<typeof DropDownMenuItemBadgeType>;
|
||||||
|
placeholder: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FontPickerListProps {
|
||||||
|
selectedFontFamily: FontFamilyValues | null;
|
||||||
|
hoveredFontFamily: FontFamilyValues | null;
|
||||||
|
onSelect: (value: number) => void;
|
||||||
|
onHover: (value: number) => void;
|
||||||
|
onLeave: () => void;
|
||||||
|
onOpen: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FontPickerList = React.memo(
|
||||||
|
({
|
||||||
|
selectedFontFamily,
|
||||||
|
hoveredFontFamily,
|
||||||
|
onSelect,
|
||||||
|
onHover,
|
||||||
|
onLeave,
|
||||||
|
onOpen,
|
||||||
|
onClose,
|
||||||
|
}: FontPickerListProps) => {
|
||||||
|
const { container } = useExcalidrawContainer();
|
||||||
|
const { fonts } = useApp();
|
||||||
|
const { showDeprecatedFonts } = useAppProps();
|
||||||
|
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const allFonts = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.from(Fonts.registered.entries())
|
||||||
|
.filter(([_, { metadata }]) => !metadata.serverSide)
|
||||||
|
.map(([familyId, { metadata, fontFaces }]) => {
|
||||||
|
const font = {
|
||||||
|
value: familyId,
|
||||||
|
icon: metadata.icon,
|
||||||
|
text: fontFaces[0].fontFace.family,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (metadata.deprecated) {
|
||||||
|
Object.assign(font, {
|
||||||
|
deprecated: metadata.deprecated,
|
||||||
|
badge: {
|
||||||
|
type: DropDownMenuItemBadgeType.RED,
|
||||||
|
placeholder: t("fontList.badge.old"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return font as FontDescriptor;
|
||||||
|
})
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.text.toLowerCase() > b.text.toLowerCase() ? 1 : -1,
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const sceneFamilies = useMemo(
|
||||||
|
() => new Set(fonts.sceneFamilies),
|
||||||
|
// cache per selected font family, so hover re-render won't mess it up
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[selectedFontFamily],
|
||||||
|
);
|
||||||
|
|
||||||
|
const sceneFonts = useMemo(
|
||||||
|
() => allFonts.filter((font) => sceneFamilies.has(font.value)), // always show all the fonts in the scene, even those that were deprecated
|
||||||
|
[allFonts, sceneFamilies],
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableFonts = useMemo(
|
||||||
|
() =>
|
||||||
|
allFonts.filter(
|
||||||
|
(font) =>
|
||||||
|
!sceneFamilies.has(font.value) &&
|
||||||
|
(showDeprecatedFonts || !font.deprecated), // skip deprecated fonts
|
||||||
|
),
|
||||||
|
[allFonts, sceneFamilies, showDeprecatedFonts],
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredFonts = useMemo(
|
||||||
|
() =>
|
||||||
|
arrayToList(
|
||||||
|
[...sceneFonts, ...availableFonts].filter((font) =>
|
||||||
|
font.text?.toLowerCase().includes(searchTerm),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[sceneFonts, availableFonts, searchTerm],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hoveredFont = useMemo(() => {
|
||||||
|
let font;
|
||||||
|
|
||||||
|
if (hoveredFontFamily) {
|
||||||
|
font = filteredFonts.find((font) => font.value === hoveredFontFamily);
|
||||||
|
} else if (selectedFontFamily) {
|
||||||
|
font = filteredFonts.find((font) => font.value === selectedFontFamily);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!font && searchTerm) {
|
||||||
|
if (filteredFonts[0]?.value) {
|
||||||
|
// hover first element on search
|
||||||
|
onHover(filteredFonts[0].value);
|
||||||
|
} else {
|
||||||
|
// re-render cache on no results
|
||||||
|
onLeave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return font;
|
||||||
|
}, [
|
||||||
|
hoveredFontFamily,
|
||||||
|
selectedFontFamily,
|
||||||
|
searchTerm,
|
||||||
|
filteredFonts,
|
||||||
|
onHover,
|
||||||
|
onLeave,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
||||||
|
(event) => {
|
||||||
|
const handled = fontPickerKeyHandler({
|
||||||
|
event,
|
||||||
|
inputRef,
|
||||||
|
hoveredFont,
|
||||||
|
filteredFonts,
|
||||||
|
onSelect,
|
||||||
|
onHover,
|
||||||
|
onClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[hoveredFont, filteredFonts, onSelect, onHover, onClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onOpen();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sceneFilteredFonts = useMemo(
|
||||||
|
() => filteredFonts.filter((font) => sceneFamilies.has(font.value)),
|
||||||
|
[filteredFonts, sceneFamilies],
|
||||||
|
);
|
||||||
|
|
||||||
|
const availableFilteredFonts = useMemo(
|
||||||
|
() => filteredFonts.filter((font) => !sceneFamilies.has(font.value)),
|
||||||
|
[filteredFonts, sceneFamilies],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderFont = (font: FontDescriptor, index: number) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={font.value}
|
||||||
|
icon={font.icon}
|
||||||
|
value={font.value}
|
||||||
|
order={index}
|
||||||
|
textStyle={{
|
||||||
|
fontFamily: getFontFamilyString({ fontFamily: font.value }),
|
||||||
|
}}
|
||||||
|
hovered={font.value === hoveredFont?.value}
|
||||||
|
selected={font.value === selectedFontFamily}
|
||||||
|
// allow to tab between search and selected font
|
||||||
|
tabIndex={font.value === selectedFontFamily ? 0 : -1}
|
||||||
|
onClick={(e) => {
|
||||||
|
onSelect(Number(e.currentTarget.value));
|
||||||
|
}}
|
||||||
|
onMouseMove={() => {
|
||||||
|
if (hoveredFont?.value !== font.value) {
|
||||||
|
onHover(font.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{font.text}
|
||||||
|
{font.badge && (
|
||||||
|
<DropDownMenuItemBadge type={font.badge.type}>
|
||||||
|
{font.badge.placeholder}
|
||||||
|
</DropDownMenuItemBadge>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
const groups = [];
|
||||||
|
|
||||||
|
if (sceneFilteredFonts.length) {
|
||||||
|
groups.push(
|
||||||
|
<DropdownMenuGroup title={t("fontList.sceneFonts")} key="group_1">
|
||||||
|
{sceneFilteredFonts.map(renderFont)}
|
||||||
|
</DropdownMenuGroup>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (availableFilteredFonts.length) {
|
||||||
|
groups.push(
|
||||||
|
<DropdownMenuGroup title={t("fontList.availableFonts")} key="group_2">
|
||||||
|
{availableFilteredFonts.map((font, index) =>
|
||||||
|
renderFont(font, index + sceneFilteredFonts.length),
|
||||||
|
)}
|
||||||
|
</DropdownMenuGroup>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PropertiesPopover
|
||||||
|
className="properties-content"
|
||||||
|
container={container}
|
||||||
|
style={{ width: "15rem" }}
|
||||||
|
onClose={onClose}
|
||||||
|
onPointerLeave={onLeave}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
>
|
||||||
|
<QuickSearch
|
||||||
|
ref={inputRef}
|
||||||
|
placeholder={t("quickSearch.placeholder")}
|
||||||
|
onChange={debounce(setSearchTerm, 20)}
|
||||||
|
/>
|
||||||
|
<ScrollableList
|
||||||
|
className="dropdown-menu fonts manual-hover"
|
||||||
|
placeholder={t("fontList.empty")}
|
||||||
|
>
|
||||||
|
{groups.length ? groups : null}
|
||||||
|
</ScrollableList>
|
||||||
|
</PropertiesPopover>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(prev, next) =>
|
||||||
|
prev.selectedFontFamily === next.selectedFontFamily &&
|
||||||
|
prev.hoveredFontFamily === next.hoveredFontFamily,
|
||||||
|
);
|
|
@ -0,0 +1,38 @@
|
||||||
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { ButtonIcon } from "../ButtonIcon";
|
||||||
|
import { TextIcon } from "../icons";
|
||||||
|
import type { FontFamilyValues } from "../../element/types";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
import { isDefaultFont } from "./FontPicker";
|
||||||
|
|
||||||
|
interface FontPickerTriggerProps {
|
||||||
|
selectedFontFamily: FontFamilyValues | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FontPickerTrigger = ({
|
||||||
|
selectedFontFamily,
|
||||||
|
}: FontPickerTriggerProps) => {
|
||||||
|
const isTriggerActive = useMemo(
|
||||||
|
() => Boolean(selectedFontFamily && !isDefaultFont(selectedFontFamily)),
|
||||||
|
[selectedFontFamily],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
{/* Empty div as trigger so it's stretched 100% due to different button sizes */}
|
||||||
|
<div>
|
||||||
|
<ButtonIcon
|
||||||
|
standalone
|
||||||
|
icon={TextIcon}
|
||||||
|
title={t("labels.showFonts")}
|
||||||
|
className="properties-trigger"
|
||||||
|
testId={"font-family-show-fonts"}
|
||||||
|
active={isTriggerActive}
|
||||||
|
// no-op
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Popover.Trigger>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,66 @@
|
||||||
|
import type { Node } from "../../utils";
|
||||||
|
import { KEYS } from "../../keys";
|
||||||
|
import { type FontDescriptor } from "./FontPickerList";
|
||||||
|
|
||||||
|
interface FontPickerKeyNavHandlerProps {
|
||||||
|
event: React.KeyboardEvent<HTMLDivElement>;
|
||||||
|
inputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
hoveredFont: Node<FontDescriptor> | undefined;
|
||||||
|
filteredFonts: Node<FontDescriptor>[];
|
||||||
|
onClose: () => void;
|
||||||
|
onSelect: (value: number) => void;
|
||||||
|
onHover: (value: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fontPickerKeyHandler = ({
|
||||||
|
event,
|
||||||
|
inputRef,
|
||||||
|
hoveredFont,
|
||||||
|
filteredFonts,
|
||||||
|
onClose,
|
||||||
|
onSelect,
|
||||||
|
onHover,
|
||||||
|
}: FontPickerKeyNavHandlerProps) => {
|
||||||
|
if (
|
||||||
|
!event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
event.shiftKey &&
|
||||||
|
event.key.toLowerCase() === KEYS.F
|
||||||
|
) {
|
||||||
|
// refocus input on the popup trigger shortcut
|
||||||
|
inputRef.current?.focus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === KEYS.ESCAPE) {
|
||||||
|
onClose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === KEYS.ENTER) {
|
||||||
|
if (hoveredFont?.value) {
|
||||||
|
onSelect(hoveredFont.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === KEYS.ARROW_DOWN) {
|
||||||
|
if (hoveredFont?.next) {
|
||||||
|
onHover(hoveredFont.next.value);
|
||||||
|
} else if (filteredFonts[0]?.value) {
|
||||||
|
onHover(filteredFonts[0].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === KEYS.ARROW_UP) {
|
||||||
|
if (hoveredFont?.prev) {
|
||||||
|
onHover(hoveredFont.prev.value);
|
||||||
|
} else if (filteredFonts[filteredFonts.length - 1]?.value) {
|
||||||
|
onHover(filteredFonts[filteredFonts.length - 1].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem 0;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@
|
||||||
&__island {
|
&__island {
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-bottom: 0.625rem;
|
margin-bottom: 0.625rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -458,6 +458,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||||
label={t("labels.showBackground")}
|
label={t("labels.showBackground")}
|
||||||
shortcuts={[getShortcutKey("G")]}
|
shortcuts={[getShortcutKey("G")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.showFonts")}
|
||||||
|
shortcuts={[getShortcutKey("Shift+F")]}
|
||||||
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.decreaseFontSize")}
|
label={t("labels.decreaseFontSize")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
.library-actions-counter {
|
.library-actions-counter {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
color: var(--color-primary-light);
|
color: var(--color-primary-light);
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
&__label {
|
&__label {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
&__header {
|
&__header {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-right: 4rem; // due to dropdown button
|
padding-right: 4rem; // due to dropdown button
|
||||||
|
|
96
packages/excalidraw/components/PropertiesPopover.tsx
Normal file
96
packages/excalidraw/components/PropertiesPopover.tsx
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import React, { type ReactNode } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
|
import { useDevice } from "./App";
|
||||||
|
import { Island } from "./Island";
|
||||||
|
import { isInteractive } from "../utils";
|
||||||
|
|
||||||
|
interface PropertiesPopoverProps {
|
||||||
|
className?: string;
|
||||||
|
container: HTMLDivElement | null;
|
||||||
|
children: ReactNode;
|
||||||
|
style?: object;
|
||||||
|
onClose: () => void;
|
||||||
|
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
|
||||||
|
onPointerLeave?: React.PointerEventHandler<HTMLDivElement>;
|
||||||
|
onFocusOutside?: Popover.DismissableLayerProps["onFocusOutside"];
|
||||||
|
onPointerDownOutside?: Popover.DismissableLayerProps["onPointerDownOutside"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PropertiesPopover = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
PropertiesPopoverProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
className,
|
||||||
|
container,
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
onClose,
|
||||||
|
onKeyDown,
|
||||||
|
onFocusOutside,
|
||||||
|
onPointerLeave,
|
||||||
|
onPointerDownOutside,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const device = useDevice();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Portal container={container}>
|
||||||
|
<Popover.Content
|
||||||
|
ref={ref}
|
||||||
|
className={clsx("focus-visible-none", className)}
|
||||||
|
data-prevent-outside-click
|
||||||
|
side={
|
||||||
|
device.editor.isMobile && !device.viewport.isLandscape
|
||||||
|
? "bottom"
|
||||||
|
: "right"
|
||||||
|
}
|
||||||
|
align={
|
||||||
|
device.editor.isMobile && !device.viewport.isLandscape
|
||||||
|
? "center"
|
||||||
|
: "start"
|
||||||
|
}
|
||||||
|
alignOffset={-16}
|
||||||
|
sideOffset={20}
|
||||||
|
style={{
|
||||||
|
zIndex: "var(--zIndex-popup)",
|
||||||
|
}}
|
||||||
|
onPointerLeave={onPointerLeave}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onFocusOutside={onFocusOutside}
|
||||||
|
onPointerDownOutside={onPointerDownOutside}
|
||||||
|
onCloseAutoFocus={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// prevents focusing the trigger
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// return focus to excalidraw container unless
|
||||||
|
// user focuses an interactive element, such as a button, or
|
||||||
|
// enters the text editor by clicking on canvas with the text tool
|
||||||
|
if (container && !isInteractive(document.activeElement)) {
|
||||||
|
container.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Island padding={3} style={style}>
|
||||||
|
{children}
|
||||||
|
</Island>
|
||||||
|
<Popover.Arrow
|
||||||
|
width={20}
|
||||||
|
height={10}
|
||||||
|
style={{
|
||||||
|
fill: "var(--popup-bg-color)",
|
||||||
|
filter: "drop-shadow(rgba(0, 0, 0, 0.05) 0px 3px 2px)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Portal>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
|
@ -133,7 +133,7 @@
|
||||||
.required,
|
.required,
|
||||||
.error {
|
.error {
|
||||||
color: $oc-red-8;
|
color: $oc-red-8;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin: 0.2rem;
|
margin: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
48
packages/excalidraw/components/QuickSearch.scss
Normal file
48
packages/excalidraw/components/QuickSearch.scss
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
.excalidraw {
|
||||||
|
--list-border-color: var(--color-gray-20);
|
||||||
|
|
||||||
|
.QuickSearch__wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 2.6rem; // added +0.1 due to Safari
|
||||||
|
border-bottom: 1px solid var(--list-border-color);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
top: 47.5%; // 50% is not exactly in the center of the input
|
||||||
|
transform: translateY(-50%);
|
||||||
|
left: 0.75rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
color: var(--color-gray-40);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.theme--dark {
|
||||||
|
--list-border-color: var(--color-gray-80);
|
||||||
|
|
||||||
|
.QuickSearch__wrapper {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.QuickSearch__input {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 0 !important;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding-left: 2.5rem !important;
|
||||||
|
padding-right: 0.75rem !important;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-gray-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
packages/excalidraw/components/QuickSearch.tsx
Normal file
28
packages/excalidraw/components/QuickSearch.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
import { searchIcon } from "./icons";
|
||||||
|
|
||||||
|
import "./QuickSearch.scss";
|
||||||
|
|
||||||
|
interface QuickSearchProps {
|
||||||
|
className?: string;
|
||||||
|
placeholder: string;
|
||||||
|
onChange: (term: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuickSearch = React.forwardRef<HTMLInputElement, QuickSearchProps>(
|
||||||
|
({ className, placeholder, onChange }, ref) => {
|
||||||
|
return (
|
||||||
|
<div className={clsx("QuickSearch__wrapper", className)}>
|
||||||
|
{searchIcon}
|
||||||
|
<input
|
||||||
|
ref={ref}
|
||||||
|
className="QuickSearch__input"
|
||||||
|
type="text"
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(e) => onChange(e.target.value.trim().toLowerCase())}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
21
packages/excalidraw/components/ScrollableList.scss
Normal file
21
packages/excalidraw/components/ScrollableList.scss
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
.excalidraw {
|
||||||
|
.ScrollableList__wrapper {
|
||||||
|
position: static !important;
|
||||||
|
border: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
& > .empty,
|
||||||
|
& > .hint {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-gray-60);
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 150%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
packages/excalidraw/components/ScrollableList.tsx
Normal file
24
packages/excalidraw/components/ScrollableList.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { Children } from "react";
|
||||||
|
|
||||||
|
import "./ScrollableList.scss";
|
||||||
|
|
||||||
|
interface ScrollableListProps {
|
||||||
|
className?: string;
|
||||||
|
placeholder: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScrollableList = ({
|
||||||
|
className,
|
||||||
|
placeholder,
|
||||||
|
children,
|
||||||
|
}: ScrollableListProps) => {
|
||||||
|
const isEmpty = !Children.count(children);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx("ScrollableList__wrapper", className)} role="menu">
|
||||||
|
{isEmpty ? <div className="empty">{placeholder}</div> : children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -139,7 +139,7 @@ $verticalBreakpoint: 861px;
|
||||||
|
|
||||||
.ttd-dialog-output-error {
|
.ttd-dialog-output-error {
|
||||||
color: red;
|
color: red;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
--avatarList-gap: 0.625rem;
|
--avatarList-gap: 0.625rem;
|
||||||
--userList-padding: var(--space-factor);
|
--userList-padding: var(--space-factor);
|
||||||
|
|
||||||
.UserList-wrapper {
|
.UserList__wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,10 +22,6 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--avatarList-gap);
|
gap: var(--avatarList-gap);
|
||||||
|
|
||||||
&:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
--max-size: calc(
|
--max-size: calc(
|
||||||
|
@ -157,66 +154,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.UserList__collaborators {
|
.UserList__collaborators {
|
||||||
position: static;
|
|
||||||
top: auto;
|
top: auto;
|
||||||
margin-top: 0;
|
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-top: 1px solid var(--userlist-collaborators-border-color);
|
|
||||||
border-bottom: 1px solid var(--userlist-collaborators-border-color);
|
|
||||||
|
|
||||||
&__empty {
|
|
||||||
color: var(--color-gray-60);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 150%;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.UserList__hint {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--userlist-hint-text-color);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 150%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.UserList__search-wrapper {
|
|
||||||
position: relative;
|
|
||||||
height: 2.5rem;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
left: 0.75rem;
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
color: var(--color-gray-40);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.UserList__search {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 0 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
padding-left: 2.5rem !important;
|
|
||||||
padding-right: 0.75rem !important;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--color-gray-40);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,12 @@ import type { ActionManager } from "../actions/manager";
|
||||||
|
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { searchIcon } from "./icons";
|
import { QuickSearch } from "./QuickSearch";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isShallowEqual } from "../utils";
|
import { isShallowEqual } from "../utils";
|
||||||
import { supportsResizeObserver } from "../constants";
|
import { supportsResizeObserver } from "../constants";
|
||||||
import type { MarkRequired } from "../utility-types";
|
import type { MarkRequired } from "../utility-types";
|
||||||
|
import { ScrollableList } from "./ScrollableList";
|
||||||
|
|
||||||
export type GoToCollaboratorComponentProps = {
|
export type GoToCollaboratorComponentProps = {
|
||||||
socketId: SocketId;
|
socketId: SocketId;
|
||||||
|
@ -40,7 +41,7 @@ const ConditionalTooltipWrapper = ({
|
||||||
shouldWrap ? (
|
shouldWrap ? (
|
||||||
<Tooltip label={username || "Unknown user"}>{children}</Tooltip>
|
<Tooltip label={username || "Unknown user"}>{children}</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment>{children}</React.Fragment>
|
<>{children}</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderCollaborator = ({
|
const renderCollaborator = ({
|
||||||
|
@ -128,6 +129,10 @@ export const UserList = React.memo(
|
||||||
).filter((collaborator) => collaborator.username?.trim());
|
).filter((collaborator) => collaborator.username?.trim());
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = React.useState("");
|
const [searchTerm, setSearchTerm] = React.useState("");
|
||||||
|
const filteredCollaborators = uniqueCollaboratorsArray.filter(
|
||||||
|
(collaborator) =>
|
||||||
|
collaborator.username?.toLowerCase().includes(searchTerm),
|
||||||
|
);
|
||||||
|
|
||||||
const userListWrapper = React.useRef<HTMLDivElement | null>(null);
|
const userListWrapper = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
@ -161,14 +166,6 @@ export const UserList = React.memo(
|
||||||
|
|
||||||
const [maxAvatars, setMaxAvatars] = React.useState(DEFAULT_MAX_AVATARS);
|
const [maxAvatars, setMaxAvatars] = React.useState(DEFAULT_MAX_AVATARS);
|
||||||
|
|
||||||
const searchTermNormalized = searchTerm.trim().toLowerCase();
|
|
||||||
|
|
||||||
const filteredCollaborators = searchTermNormalized
|
|
||||||
? uniqueCollaboratorsArray.filter((collaborator) =>
|
|
||||||
collaborator.username?.toLowerCase().includes(searchTerm),
|
|
||||||
)
|
|
||||||
: uniqueCollaboratorsArray;
|
|
||||||
|
|
||||||
const firstNCollaborators = uniqueCollaboratorsArray.slice(
|
const firstNCollaborators = uniqueCollaboratorsArray.slice(
|
||||||
0,
|
0,
|
||||||
maxAvatars - 1,
|
maxAvatars - 1,
|
||||||
|
@ -197,7 +194,7 @@ export const UserList = React.memo(
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="UserList-wrapper" ref={userListWrapper}>
|
<div className="UserList__wrapper" ref={userListWrapper}>
|
||||||
<div
|
<div
|
||||||
className={clsx("UserList", className)}
|
className={clsx("UserList", className)}
|
||||||
style={{ [`--max-avatars` as any]: maxAvatars }}
|
style={{ [`--max-avatars` as any]: maxAvatars }}
|
||||||
|
@ -205,13 +202,7 @@ export const UserList = React.memo(
|
||||||
{firstNAvatarsJSX}
|
{firstNAvatarsJSX}
|
||||||
|
|
||||||
{uniqueCollaboratorsArray.length > maxAvatars - 1 && (
|
{uniqueCollaboratorsArray.length > maxAvatars - 1 && (
|
||||||
<Popover.Root
|
<Popover.Root>
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
if (!isOpen) {
|
|
||||||
setSearchTerm("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Popover.Trigger className="UserList__more">
|
<Popover.Trigger className="UserList__more">
|
||||||
+{uniqueCollaboratorsArray.length - maxAvatars + 1}
|
+{uniqueCollaboratorsArray.length - maxAvatars + 1}
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
|
@ -224,41 +215,43 @@ export const UserList = React.memo(
|
||||||
align="end"
|
align="end"
|
||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
>
|
>
|
||||||
<Island style={{ overflow: "hidden" }}>
|
<Island padding={2}>
|
||||||
{uniqueCollaboratorsArray.length >=
|
{uniqueCollaboratorsArray.length >=
|
||||||
SHOW_COLLABORATORS_FILTER_AT && (
|
SHOW_COLLABORATORS_FILTER_AT && (
|
||||||
<div className="UserList__search-wrapper">
|
<QuickSearch
|
||||||
{searchIcon}
|
placeholder={t("quickSearch.placeholder")}
|
||||||
<input
|
onChange={setSearchTerm}
|
||||||
className="UserList__search"
|
/>
|
||||||
type="text"
|
|
||||||
placeholder={t("userList.search.placeholder")}
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSearchTerm(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className="dropdown-menu UserList__collaborators">
|
<ScrollableList
|
||||||
{filteredCollaborators.length === 0 && (
|
className={"dropdown-menu UserList__collaborators"}
|
||||||
<div className="UserList__collaborators__empty">
|
placeholder={t("userList.empty")}
|
||||||
{t("userList.search.empty")}
|
>
|
||||||
</div>
|
{/* The list checks for `Children.count()`, hence defensively returning empty list */}
|
||||||
)}
|
{filteredCollaborators.length > 0
|
||||||
<div className="UserList__hint">
|
? [
|
||||||
{t("userList.hint.text")}
|
<div className="hint">{t("userList.hint.text")}</div>,
|
||||||
</div>
|
filteredCollaborators.map((collaborator) =>
|
||||||
{filteredCollaborators.map((collaborator) =>
|
renderCollaborator({
|
||||||
renderCollaborator({
|
actionManager,
|
||||||
actionManager,
|
collaborator,
|
||||||
collaborator,
|
socketId: collaborator.socketId,
|
||||||
socketId: collaborator.socketId,
|
withName: true,
|
||||||
withName: true,
|
isBeingFollowed:
|
||||||
isBeingFollowed: collaborator.socketId === userToFollow,
|
collaborator.socketId === userToFollow,
|
||||||
}),
|
}),
|
||||||
)}
|
),
|
||||||
</div>
|
]
|
||||||
|
: []}
|
||||||
|
</ScrollableList>
|
||||||
|
<Popover.Arrow
|
||||||
|
width={20}
|
||||||
|
height={10}
|
||||||
|
style={{
|
||||||
|
fill: "var(--popup-bg-color)",
|
||||||
|
filter: "drop-shadow(rgba(0, 0, 0, 0.05) 0px 3px 2px)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Island>
|
</Island>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
|
|
|
@ -105,6 +105,7 @@ const getRelevantAppStateProps = (
|
||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
frameToHighlight: appState.frameToHighlight,
|
frameToHighlight: appState.frameToHighlight,
|
||||||
editingGroupId: appState.editingGroupId,
|
editingGroupId: appState.editingGroupId,
|
||||||
|
currentHoveredFontFamily: appState.currentHoveredFontFamily,
|
||||||
});
|
});
|
||||||
|
|
||||||
const areEqual = (
|
const areEqual = (
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.5rem;
|
||||||
|
|
||||||
&--mobile {
|
&--mobile {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -35,21 +35,69 @@
|
||||||
|
|
||||||
.dropdown-menu-item-base {
|
.dropdown-menu-item-base {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 0.625rem;
|
|
||||||
column-gap: 0.625rem;
|
column-gap: 0.625rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--color-on-surface);
|
color: var(--color-on-surface);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-weight: normal;
|
font-weight: 400;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.manual-hover {
|
||||||
|
// disable built-in hover due to keyboard navigation
|
||||||
|
.dropdown-menu-item {
|
||||||
|
&:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--hovered {
|
||||||
|
background-color: var(--button-hover-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
background-color: var(--color-primary-light) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fonts {
|
||||||
|
margin-top: 1rem;
|
||||||
|
// display max 7 items per list, where each has 2rem (2.25) height and 1px margin top & bottom
|
||||||
|
// count in 2 groups, where each allocates 1.3*0.75rem font-size and 0.5rem margin bottom, plus one extra 1rem margin top
|
||||||
|
max-height: calc(7 * (2rem + 2px) + 2 * (0.5rem + 1.3 * 0.75rem) + 1rem);
|
||||||
|
|
||||||
|
@media screen and (min-width: 1921px) {
|
||||||
|
max-height: calc(
|
||||||
|
7 * (2.25rem + 2px) + 2 * (0.5rem + 1.3 * 0.75rem) + 1rem
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-item-base {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-group:not(:first-child) {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-group-title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-menu-item {
|
.dropdown-menu-item {
|
||||||
|
height: 2rem;
|
||||||
|
margin: 1px;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
width: calc(100% - 2px);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 2rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: var(--border-radius-md);
|
||||||
|
|
||||||
|
@ -57,11 +105,6 @@
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--selected {
|
|
||||||
background: var(--color-primary-light);
|
|
||||||
--icon-fill-color: var(--color-primary-darker);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
&__text {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -83,6 +126,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
background: var(--color-primary-light);
|
||||||
|
--icon-fill-color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--button-hover-bg);
|
background-color: var(--button-hover-bg);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -1,37 +1,62 @@
|
||||||
import React from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
getDropdownMenuItemClassName,
|
getDropdownMenuItemClassName,
|
||||||
useHandleDropdownMenuItemClick,
|
useHandleDropdownMenuItemClick,
|
||||||
} from "./common";
|
} from "./common";
|
||||||
import MenuItemContent from "./DropdownMenuItemContent";
|
import MenuItemContent from "./DropdownMenuItemContent";
|
||||||
|
import { useExcalidrawAppState } from "../App";
|
||||||
|
import { THEME } from "../../constants";
|
||||||
|
import type { ValueOf } from "../../utility-types";
|
||||||
|
|
||||||
const DropdownMenuItem = ({
|
const DropdownMenuItem = ({
|
||||||
icon,
|
icon,
|
||||||
onSelect,
|
value,
|
||||||
|
order,
|
||||||
children,
|
children,
|
||||||
shortcut,
|
shortcut,
|
||||||
className,
|
className,
|
||||||
|
hovered,
|
||||||
selected,
|
selected,
|
||||||
|
textStyle,
|
||||||
|
onSelect,
|
||||||
|
onClick,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
onSelect: (event: Event) => void;
|
value?: string | number | undefined;
|
||||||
|
order?: number;
|
||||||
|
onSelect?: (event: Event) => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
|
hovered?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
|
textStyle?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
const handleClick = useHandleDropdownMenuItemClick(onClick, onSelect);
|
||||||
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hovered) {
|
||||||
|
if (order === 0) {
|
||||||
|
// scroll into the first item differently, so it's visible what is above (i.e. group title)
|
||||||
|
ref.current?.scrollIntoView({ block: "end" });
|
||||||
|
} else {
|
||||||
|
ref.current?.scrollIntoView({ block: "nearest" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [hovered, order]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
{...rest}
|
{...rest}
|
||||||
|
ref={ref}
|
||||||
|
value={value}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
type="button"
|
className={getDropdownMenuItemClassName(className, selected, hovered)}
|
||||||
className={getDropdownMenuItemClassName(className, selected)}
|
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
>
|
>
|
||||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
<MenuItemContent textStyle={textStyle} icon={icon} shortcut={shortcut}>
|
||||||
{children}
|
{children}
|
||||||
</MenuItemContent>
|
</MenuItemContent>
|
||||||
</button>
|
</button>
|
||||||
|
@ -39,24 +64,53 @@ const DropdownMenuItem = ({
|
||||||
};
|
};
|
||||||
DropdownMenuItem.displayName = "DropdownMenuItem";
|
DropdownMenuItem.displayName = "DropdownMenuItem";
|
||||||
|
|
||||||
|
export const DropDownMenuItemBadgeType = {
|
||||||
|
GREEN: "green",
|
||||||
|
RED: "red",
|
||||||
|
BLUE: "blue",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const DropDownMenuItemBadge = ({
|
export const DropDownMenuItemBadge = ({
|
||||||
|
type = DropDownMenuItemBadgeType.BLUE,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
|
type?: ValueOf<typeof DropDownMenuItemBadgeType>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
const { theme } = useExcalidrawAppState();
|
||||||
<div
|
const style = {
|
||||||
style={{
|
display: "inline-flex",
|
||||||
display: "inline-flex",
|
marginLeft: "auto",
|
||||||
marginLeft: "auto",
|
padding: "2px 4px",
|
||||||
padding: "2px 4px",
|
borderRadius: 6,
|
||||||
|
fontSize: 9,
|
||||||
|
fontFamily: "Cascadia, monospace",
|
||||||
|
border: theme === THEME.LIGHT ? "1.5px solid white" : "none",
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case DropDownMenuItemBadgeType.GREEN:
|
||||||
|
Object.assign(style, {
|
||||||
|
backgroundColor: "var(--background-color-badge)",
|
||||||
|
color: "var(--color-badge)",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case DropDownMenuItemBadgeType.RED:
|
||||||
|
Object.assign(style, {
|
||||||
|
backgroundColor: "pink",
|
||||||
|
color: "darkred",
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case DropDownMenuItemBadgeType.BLUE:
|
||||||
|
default:
|
||||||
|
Object.assign(style, {
|
||||||
background: "var(--color-promo)",
|
background: "var(--color-promo)",
|
||||||
color: "var(--color-surface-lowest)",
|
color: "var(--color-surface-lowest)",
|
||||||
borderRadius: 6,
|
});
|
||||||
fontSize: 9,
|
}
|
||||||
fontFamily: "Cascadia, monospace",
|
|
||||||
}}
|
return (
|
||||||
>
|
<div className="DropDownMenuItemBadge" style={style}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,19 +1,23 @@
|
||||||
import { useDevice } from "../App";
|
import { useDevice } from "../App";
|
||||||
|
|
||||||
const MenuItemContent = ({
|
const MenuItemContent = ({
|
||||||
|
textStyle,
|
||||||
icon,
|
icon,
|
||||||
shortcut,
|
shortcut,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
|
textStyle?: React.CSSProperties;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dropdown-menu-item__icon">{icon}</div>
|
{icon && <div className="dropdown-menu-item__icon">{icon}</div>}
|
||||||
<div className="dropdown-menu-item__text">{children}</div>
|
<div style={textStyle} className="dropdown-menu-item__text">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
{shortcut && !device.editor.isMobile && (
|
{shortcut && !device.editor.isMobile && (
|
||||||
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -9,9 +9,11 @@ export const DropdownMenuContentPropsContext = React.createContext<{
|
||||||
export const getDropdownMenuItemClassName = (
|
export const getDropdownMenuItemClassName = (
|
||||||
className = "",
|
className = "",
|
||||||
selected = false,
|
selected = false,
|
||||||
|
hovered = false,
|
||||||
) => {
|
) => {
|
||||||
return `dropdown-menu-item dropdown-menu-item-base ${className} ${
|
return `dropdown-menu-item dropdown-menu-item-base ${className}
|
||||||
selected ? "dropdown-menu-item--selected" : ""
|
${selected ? "dropdown-menu-item--selected" : ""} ${
|
||||||
|
hovered ? "dropdown-menu-item--hovered" : ""
|
||||||
}`.trim();
|
}`.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1438,6 +1438,27 @@ export const fontSizeIcon = createIcon(
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const FontFamilyHeadingIcon = createIcon(
|
||||||
|
<>
|
||||||
|
<g
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.25"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M7 12h10" />
|
||||||
|
<path d="M7 5v14" />
|
||||||
|
<path d="M17 5v14" />
|
||||||
|
<path d="M15 19h4" />
|
||||||
|
<path d="M15 5h4" />
|
||||||
|
<path d="M5 19h4" />
|
||||||
|
<path d="M5 5h4" />
|
||||||
|
</g>
|
||||||
|
</>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
export const FontFamilyNormalIcon = createIcon(
|
export const FontFamilyNormalIcon = createIcon(
|
||||||
<>
|
<>
|
||||||
<g
|
<g
|
||||||
|
|
|
@ -109,7 +109,7 @@ Center.displayName = "Center";
|
||||||
|
|
||||||
const Logo = ({ children }: { children?: React.ReactNode }) => {
|
const Logo = ({ children }: { children?: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<div className="welcome-screen-center__logo virgil welcome-screen-decor">
|
<div className="welcome-screen-center__logo excalifont welcome-screen-decor">
|
||||||
{children || <ExcalidrawLogo withText />}
|
{children || <ExcalidrawLogo withText />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -118,7 +118,7 @@ Logo.displayName = "Logo";
|
||||||
|
|
||||||
const Heading = ({ children }: { children: React.ReactNode }) => {
|
const Heading = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<div className="welcome-screen-center__heading welcome-screen-decor virgil">
|
<div className="welcome-screen-center__heading welcome-screen-decor excalifont">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,7 +10,7 @@ const MenuHint = ({ children }: { children?: React.ReactNode }) => {
|
||||||
const { WelcomeScreenMenuHintTunnel } = useTunnels();
|
const { WelcomeScreenMenuHintTunnel } = useTunnels();
|
||||||
return (
|
return (
|
||||||
<WelcomeScreenMenuHintTunnel.In>
|
<WelcomeScreenMenuHintTunnel.In>
|
||||||
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu">
|
<div className="excalifont welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu">
|
||||||
{WelcomeScreenMenuArrow}
|
{WelcomeScreenMenuArrow}
|
||||||
<div className="welcome-screen-decor-hint__label">
|
<div className="welcome-screen-decor-hint__label">
|
||||||
{children || t("welcomeScreen.defaults.menuHint")}
|
{children || t("welcomeScreen.defaults.menuHint")}
|
||||||
|
@ -25,7 +25,7 @@ const ToolbarHint = ({ children }: { children?: React.ReactNode }) => {
|
||||||
const { WelcomeScreenToolbarHintTunnel } = useTunnels();
|
const { WelcomeScreenToolbarHintTunnel } = useTunnels();
|
||||||
return (
|
return (
|
||||||
<WelcomeScreenToolbarHintTunnel.In>
|
<WelcomeScreenToolbarHintTunnel.In>
|
||||||
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar">
|
<div className="excalifont welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar">
|
||||||
<div className="welcome-screen-decor-hint__label">
|
<div className="welcome-screen-decor-hint__label">
|
||||||
{children || t("welcomeScreen.defaults.toolbarHint")}
|
{children || t("welcomeScreen.defaults.toolbarHint")}
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,7 +40,7 @@ const HelpHint = ({ children }: { children?: React.ReactNode }) => {
|
||||||
const { WelcomeScreenHelpHintTunnel } = useTunnels();
|
const { WelcomeScreenHelpHintTunnel } = useTunnels();
|
||||||
return (
|
return (
|
||||||
<WelcomeScreenHelpHintTunnel.In>
|
<WelcomeScreenHelpHintTunnel.In>
|
||||||
<div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">
|
<div className="excalifont welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">
|
||||||
<div>{children || t("welcomeScreen.defaults.helpHint")}</div>
|
<div>{children || t("welcomeScreen.defaults.helpHint")}</div>
|
||||||
{WelcomeScreenHelpArrow}
|
{WelcomeScreenHelpArrow}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.virgil {
|
.excalifont {
|
||||||
font-family: "Virgil";
|
font-family: "Excalifont";
|
||||||
}
|
}
|
||||||
|
|
||||||
// WelcomeSreen common
|
// WelcomeSreen common
|
||||||
|
|
|
@ -114,12 +114,24 @@ export const CLASSES = {
|
||||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1-based in case we ever do `if(element.fontFamily)`
|
/**
|
||||||
|
* // TODO: shouldn't be really `const`, likely neither have integers as values, due to value for the custom fonts, which should likely be some hash.
|
||||||
|
*
|
||||||
|
* Let's think this through and consider:
|
||||||
|
* - https://developer.mozilla.org/en-US/docs/Web/CSS/generic-family
|
||||||
|
* - https://drafts.csswg.org/css-fonts-4/#font-family-prop
|
||||||
|
* - https://learn.microsoft.com/en-us/typography/opentype/spec/ibmfc
|
||||||
|
*/
|
||||||
export const FONT_FAMILY = {
|
export const FONT_FAMILY = {
|
||||||
Virgil: 1,
|
Virgil: 1,
|
||||||
Helvetica: 2,
|
Helvetica: 2,
|
||||||
Cascadia: 3,
|
Cascadia: 3,
|
||||||
Assistant: 4,
|
// leave 4 unused as it was historically used for Assistant (which we don't use anymore) or custom font (Obsidian)
|
||||||
|
Excalifont: 5,
|
||||||
|
Nunito: 6,
|
||||||
|
"Lilita One": 7,
|
||||||
|
"Comic Shanns": 8,
|
||||||
|
"Liberation Sans": 9,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const THEME = {
|
export const THEME = {
|
||||||
|
@ -147,7 +159,7 @@ export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
||||||
|
|
||||||
export const MIN_FONT_SIZE = 1;
|
export const MIN_FONT_SIZE = 1;
|
||||||
export const DEFAULT_FONT_SIZE = 20;
|
export const DEFAULT_FONT_SIZE = 20;
|
||||||
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Excalifont;
|
||||||
export const DEFAULT_TEXT_ALIGN = "left";
|
export const DEFAULT_TEXT_ALIGN = "left";
|
||||||
export const DEFAULT_VERTICAL_ALIGN = "top";
|
export const DEFAULT_VERTICAL_ALIGN = "top";
|
||||||
export const DEFAULT_VERSION = "{version}";
|
export const DEFAULT_VERSION = "{version}";
|
||||||
|
|
|
@ -152,7 +152,7 @@ body.excalidraw-cursor-resize * {
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
font-weight: normal;
|
font-weight: 400;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,14 +227,7 @@ body.excalidraw-cursor-resize * {
|
||||||
label,
|
label,
|
||||||
button,
|
button,
|
||||||
.zIndexButton {
|
.zIndexButton {
|
||||||
@include outlineButtonStyles;
|
@include outlineButtonIconStyles;
|
||||||
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: var(--default-icon-size);
|
|
||||||
height: var(--default-icon-size);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,7 +387,7 @@ body.excalidraw-cursor-resize * {
|
||||||
.App-menu__left {
|
.App-menu__left {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
width: 202px;
|
width: 200px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
@ -585,7 +578,7 @@ body.excalidraw-cursor-resize * {
|
||||||
// use custom, minimalistic scrollbar
|
// use custom, minimalistic scrollbar
|
||||||
// (doesn't work in Firefox)
|
// (doesn't work in Firefox)
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 3px;
|
width: 4px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -664,6 +657,10 @@ body.excalidraw-cursor-resize * {
|
||||||
--button-hover-bg: #363541;
|
--button-hover-bg: #363541;
|
||||||
--button-bg: var(--color-surface-high);
|
--button-bg: var(--color-surface-high);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonList {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw__paragraph {
|
.excalidraw__paragraph {
|
||||||
|
@ -757,7 +754,7 @@ body.excalidraw-cursor-resize * {
|
||||||
padding: 1rem 1.6rem;
|
padding: 1rem 1.6rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
letter-spacing: 0.6px;
|
letter-spacing: 0.6px;
|
||||||
font-family: "Assistant";
|
font-family: "Assistant";
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,6 +151,9 @@
|
||||||
--color-border-outline-variant: #c5c5d0;
|
--color-border-outline-variant: #c5c5d0;
|
||||||
--color-surface-primary-container: #e0dfff;
|
--color-surface-primary-container: #e0dfff;
|
||||||
|
|
||||||
|
--color-badge: #0b6513;
|
||||||
|
--background-color-badge: #d3ffd2;
|
||||||
|
|
||||||
&.theme--dark {
|
&.theme--dark {
|
||||||
&.theme--dark-background-none {
|
&.theme--dark-background-none {
|
||||||
background: none;
|
background: none;
|
||||||
|
|
|
@ -124,6 +124,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin outlineButtonIconStyles {
|
||||||
|
@include outlineButtonStyles;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: var(--default-icon-size);
|
||||||
|
height: var(--default-icon-size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@mixin avatarStyles {
|
@mixin avatarStyles {
|
||||||
width: var(--avatar-size, 1.5rem);
|
width: var(--avatar-size, 1.5rem);
|
||||||
height: var(--avatar-size, 1.5rem);
|
height: var(--avatar-size, 1.5rem);
|
||||||
|
@ -135,7 +145,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: var(--color-gray-90);
|
color: var(--color-gray-90);
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
|
@ -239,7 +239,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -285,7 +285,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -386,7 +386,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||||
"containerId": "id48",
|
"containerId": "id48",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -487,7 +487,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||||
"containerId": "id37",
|
"containerId": "id37",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -662,7 +662,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||||
"containerId": "id41",
|
"containerId": "id41",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -708,7 +708,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -754,7 +754,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -1207,7 +1207,7 @@ exports[`Test Transform > should transform text element 1`] = `
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -1248,7 +1248,7 @@ exports[`Test Transform > should transform text element 2`] = `
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -1581,7 +1581,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||||
"containerId": "B",
|
"containerId": "B",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [
|
"groupIds": [
|
||||||
|
@ -1624,7 +1624,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||||
"containerId": "A",
|
"containerId": "A",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [
|
"groupIds": [
|
||||||
|
@ -1667,7 +1667,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||||
"containerId": "Alice",
|
"containerId": "Alice",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [
|
"groupIds": [
|
||||||
|
@ -1710,7 +1710,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||||
"containerId": "Bob",
|
"containerId": "Bob",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [
|
"groupIds": [
|
||||||
|
@ -1753,7 +1753,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||||
"containerId": "Bob_Alice",
|
"containerId": "Bob_Alice",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -1794,7 +1794,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||||
"containerId": "Bob_B",
|
"containerId": "Bob_B",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2043,7 +2043,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"containerId": "id25",
|
"containerId": "id25",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2084,7 +2084,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"containerId": "id26",
|
"containerId": "id26",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2125,7 +2125,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"containerId": "id27",
|
"containerId": "id27",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2167,7 +2167,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||||
"containerId": "id28",
|
"containerId": "id28",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2431,7 +2431,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||||
"containerId": "id13",
|
"containerId": "id13",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2472,7 +2472,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||||
"containerId": "id14",
|
"containerId": "id14",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2514,7 +2514,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||||
"containerId": "id15",
|
"containerId": "id15",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2558,7 +2558,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||||
"containerId": "id16",
|
"containerId": "id16",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2600,7 +2600,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||||
"containerId": "id17",
|
"containerId": "id17",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2643,7 +2643,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||||
"containerId": "id18",
|
"containerId": "id18",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
|
|
@ -44,14 +44,11 @@ import { bumpVersion } from "../element/mutateElement";
|
||||||
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import type { MarkOptional, Mutable } from "../utility-types";
|
import type { MarkOptional, Mutable } from "../utility-types";
|
||||||
import {
|
import { detectLineHeight, getContainerElement } from "../element/textElement";
|
||||||
detectLineHeight,
|
|
||||||
getContainerElement,
|
|
||||||
getDefaultLineHeight,
|
|
||||||
} from "../element/textElement";
|
|
||||||
import { normalizeLink } from "./url";
|
import { normalizeLink } from "./url";
|
||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
|
import { getLineHeight } from "../fonts";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -206,7 +203,7 @@ const restoreElement = (
|
||||||
detectLineHeight(element)
|
detectLineHeight(element)
|
||||||
: // no element height likely means programmatic use, so default
|
: // no element height likely means programmatic use, so default
|
||||||
// to a fixed line height
|
// to a fixed line height
|
||||||
getDefaultLineHeight(element.fontFamily));
|
getLineHeight(element.fontFamily));
|
||||||
element = restoreElementWithProperties(element, {
|
element = restoreElementWithProperties(element, {
|
||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
|
|
|
@ -18,11 +18,7 @@ import {
|
||||||
newMagicFrameElement,
|
newMagicFrameElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
} from "../element/newElement";
|
} from "../element/newElement";
|
||||||
import {
|
import { measureText, normalizeText } from "../element/textElement";
|
||||||
getDefaultLineHeight,
|
|
||||||
measureText,
|
|
||||||
normalizeText,
|
|
||||||
} from "../element/textElement";
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
|
@ -54,6 +50,7 @@ import {
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
|
import { getLineHeight } from "../fonts";
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
|
@ -568,8 +565,7 @@ export const convertToExcalidrawElements = (
|
||||||
case "text": {
|
case "text": {
|
||||||
const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY;
|
const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY;
|
||||||
const fontSize = element?.fontSize || DEFAULT_FONT_SIZE;
|
const fontSize = element?.fontSize || DEFAULT_FONT_SIZE;
|
||||||
const lineHeight =
|
const lineHeight = element?.lineHeight || getLineHeight(fontFamily);
|
||||||
element?.lineHeight || getDefaultLineHeight(fontFamily);
|
|
||||||
const text = element.text ?? "";
|
const text = element.text ?? "";
|
||||||
const normalizedText = normalizeText(text);
|
const normalizedText = normalizeText(text);
|
||||||
const metrics = measureText(
|
const metrics = measureText(
|
||||||
|
|
|
@ -107,6 +107,8 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
export const newElementWith = <TElement extends ExcalidrawElement>(
|
export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||||
element: TElement,
|
element: TElement,
|
||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
|
/** pass `true` to always regenerate */
|
||||||
|
force = false,
|
||||||
): TElement => {
|
): TElement => {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
for (const key in updates) {
|
for (const key in updates) {
|
||||||
|
@ -123,7 +125,7 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!didChange) {
|
if (!didChange && !force) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,6 @@ import {
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
wrapText,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
getDefaultLineHeight,
|
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_PROPS,
|
DEFAULT_ELEMENT_PROPS,
|
||||||
|
@ -47,6 +46,7 @@ import {
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import type { MarkOptional, Merge, Mutable } from "../utility-types";
|
import type { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||||
|
import { getLineHeight } from "../fonts";
|
||||||
|
|
||||||
export type ElementConstructorOpts = MarkOptional<
|
export type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
|
@ -228,7 +228,7 @@ export const newTextElement = (
|
||||||
): NonDeleted<ExcalidrawTextElement> => {
|
): NonDeleted<ExcalidrawTextElement> => {
|
||||||
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
|
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
|
||||||
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
|
||||||
const lineHeight = opts.lineHeight || getDefaultLineHeight(fontFamily);
|
const lineHeight = opts.lineHeight || getLineHeight(fontFamily);
|
||||||
const text = normalizeText(opts.text);
|
const text = normalizeText(opts.text);
|
||||||
const metrics = measureText(
|
const metrics = measureText(
|
||||||
text,
|
text,
|
||||||
|
@ -514,7 +514,7 @@ export const regenerateId = (
|
||||||
if (
|
if (
|
||||||
window.h?.app
|
window.h?.app
|
||||||
?.getSceneElementsIncludingDeleted()
|
?.getSceneElementsIncludingDeleted()
|
||||||
.find((el) => el.id === nextId)
|
.find((el: ExcalidrawElement) => el.id === nextId)
|
||||||
) {
|
) {
|
||||||
nextId += "_copy";
|
nextId += "_copy";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
||||||
|
import { getLineHeight } from "../fonts";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import {
|
import {
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
|
@ -8,7 +9,6 @@ import {
|
||||||
wrapText,
|
wrapText,
|
||||||
detectLineHeight,
|
detectLineHeight,
|
||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
getDefaultLineHeight,
|
|
||||||
parseTokens,
|
parseTokens,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import type { ExcalidrawTextElementWithContainer, FontString } from "./types";
|
import type { ExcalidrawTextElementWithContainer, FontString } from "./types";
|
||||||
|
@ -418,15 +418,15 @@ describe("Test getLineHeightInPx", () => {
|
||||||
describe("Test getDefaultLineHeight", () => {
|
describe("Test getDefaultLineHeight", () => {
|
||||||
it("should return line height using default font family when not passed", () => {
|
it("should return line height using default font family when not passed", () => {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
expect(getDefaultLineHeight()).toBe(1.25);
|
expect(getLineHeight()).toBe(1.25);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return line height using default font family for unknown font", () => {
|
it("should return line height using default font family for unknown font", () => {
|
||||||
const UNKNOWN_FONT = 5;
|
const UNKNOWN_FONT = 5;
|
||||||
expect(getDefaultLineHeight(UNKNOWN_FONT)).toBe(1.25);
|
expect(getLineHeight(UNKNOWN_FONT)).toBe(1.25);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return correct line height", () => {
|
it("should return correct line height", () => {
|
||||||
expect(getDefaultLineHeight(FONT_FAMILY.Cascadia)).toBe(1.2);
|
expect(getLineHeight(FONT_FAMILY.Cascadia)).toBe(1.2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,6 @@ import type {
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
FontFamilyValues,
|
|
||||||
FontString,
|
FontString,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
@ -17,7 +16,6 @@ import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
FONT_FAMILY,
|
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
|
@ -30,7 +28,7 @@ import {
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
import type { ExtractSetType, MakeBrand } from "../utility-types";
|
import type { ExtractSetType } from "../utility-types";
|
||||||
|
|
||||||
export const normalizeText = (text: string) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
|
@ -321,24 +319,6 @@ export const getLineHeightInPx = (
|
||||||
return fontSize * lineHeight;
|
return fontSize * lineHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates vertical offset for a text with alphabetic baseline.
|
|
||||||
*/
|
|
||||||
export const getVerticalOffset = (
|
|
||||||
fontFamily: ExcalidrawTextElement["fontFamily"],
|
|
||||||
fontSize: ExcalidrawTextElement["fontSize"],
|
|
||||||
lineHeightPx: number,
|
|
||||||
) => {
|
|
||||||
const { unitsPerEm, ascender, descender } =
|
|
||||||
FONT_METRICS[fontFamily] || FONT_METRICS[FONT_FAMILY.Helvetica];
|
|
||||||
|
|
||||||
const fontSizeEm = fontSize / unitsPerEm;
|
|
||||||
const lineGap = lineHeightPx - fontSizeEm * ascender + fontSizeEm * descender;
|
|
||||||
|
|
||||||
const verticalOffset = fontSizeEm * ascender + lineGap;
|
|
||||||
return verticalOffset;
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME rename to getApproxMinContainerHeight
|
// FIXME rename to getApproxMinContainerHeight
|
||||||
export const getApproxMinLineHeight = (
|
export const getApproxMinLineHeight = (
|
||||||
fontSize: ExcalidrawTextElement["fontSize"],
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
@ -349,29 +329,72 @@ export const getApproxMinLineHeight = (
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement | undefined;
|
let canvas: HTMLCanvasElement | undefined;
|
||||||
|
|
||||||
const getLineWidth = (text: string, font: FontString) => {
|
/**
|
||||||
|
* @param forceAdvanceWidth use to force retrieve the "advance width" ~ `metrics.width`, instead of the actual boundind box width.
|
||||||
|
*
|
||||||
|
* > The advance width is the distance between the glyph's initial pen position and the next glyph's initial pen position.
|
||||||
|
*
|
||||||
|
* We need to use the advance width as that's the closest thing to the browser wrapping algo, hence using it for:
|
||||||
|
* - text wrapping
|
||||||
|
* - wysiwyg editor (+padding)
|
||||||
|
*
|
||||||
|
* Everything else should be based on the actual bounding box width.
|
||||||
|
*
|
||||||
|
* `Math.ceil` of the final width adds additional buffer which stabilizes slight wrapping incosistencies.
|
||||||
|
*/
|
||||||
|
const getLineWidth = (
|
||||||
|
text: string,
|
||||||
|
font: FontString,
|
||||||
|
forceAdvanceWidth?: true,
|
||||||
|
) => {
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
canvas = document.createElement("canvas");
|
canvas = document.createElement("canvas");
|
||||||
}
|
}
|
||||||
const canvas2dContext = canvas.getContext("2d")!;
|
const canvas2dContext = canvas.getContext("2d")!;
|
||||||
canvas2dContext.font = font;
|
canvas2dContext.font = font;
|
||||||
const width = canvas2dContext.measureText(text).width;
|
const metrics = canvas2dContext.measureText(text);
|
||||||
|
|
||||||
|
const advanceWidth = metrics.width;
|
||||||
|
|
||||||
|
// retrieve the actual bounding box width if these metrics are available (as of now > 95% coverage)
|
||||||
|
if (
|
||||||
|
!forceAdvanceWidth &&
|
||||||
|
window.TextMetrics &&
|
||||||
|
"actualBoundingBoxLeft" in window.TextMetrics.prototype &&
|
||||||
|
"actualBoundingBoxRight" in window.TextMetrics.prototype
|
||||||
|
) {
|
||||||
|
// could be negative, therefore getting the absolute value
|
||||||
|
const actualWidth =
|
||||||
|
Math.abs(metrics.actualBoundingBoxLeft) +
|
||||||
|
Math.abs(metrics.actualBoundingBoxRight);
|
||||||
|
|
||||||
|
// fallback to advance width if the actual width is zero, i.e. on text editing start
|
||||||
|
// or when actual width does not respect whitespace chars, i.e. spaces
|
||||||
|
// otherwise actual width should always be bigger
|
||||||
|
return Math.max(actualWidth, advanceWidth);
|
||||||
|
}
|
||||||
|
|
||||||
// since in test env the canvas measureText algo
|
// since in test env the canvas measureText algo
|
||||||
// doesn't measure text and instead just returns number of
|
// doesn't measure text and instead just returns number of
|
||||||
// characters hence we assume that each letteris 10px
|
// characters hence we assume that each letteris 10px
|
||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
return width * 10;
|
return advanceWidth * 10;
|
||||||
}
|
}
|
||||||
return width;
|
|
||||||
|
return advanceWidth;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTextWidth = (text: string, font: FontString) => {
|
export const getTextWidth = (
|
||||||
|
text: string,
|
||||||
|
font: FontString,
|
||||||
|
forceAdvanceWidth?: true,
|
||||||
|
) => {
|
||||||
const lines = splitIntoLines(text);
|
const lines = splitIntoLines(text);
|
||||||
let width = 0;
|
let width = 0;
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
width = Math.max(width, getLineWidth(line, font));
|
width = Math.max(width, getLineWidth(line, font, forceAdvanceWidth));
|
||||||
});
|
});
|
||||||
|
|
||||||
return width;
|
return width;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -402,7 +425,11 @@ export const parseTokens = (text: string) => {
|
||||||
return words.join(" ").split(" ");
|
return words.join(" ").split(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
export const wrapText = (
|
||||||
|
text: string,
|
||||||
|
font: FontString,
|
||||||
|
maxWidth: number,
|
||||||
|
): string => {
|
||||||
// if maxWidth is not finite or NaN which can happen in case of bugs in
|
// if maxWidth is not finite or NaN which can happen in case of bugs in
|
||||||
// computation, we need to make sure we don't continue as we'll end up
|
// computation, we need to make sure we don't continue as we'll end up
|
||||||
// in an infinite loop
|
// in an infinite loop
|
||||||
|
@ -412,7 +439,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
|
|
||||||
const lines: Array<string> = [];
|
const lines: Array<string> = [];
|
||||||
const originalLines = text.split("\n");
|
const originalLines = text.split("\n");
|
||||||
const spaceWidth = getLineWidth(" ", font);
|
const spaceAdvanceWidth = getLineWidth(" ", font, true);
|
||||||
|
|
||||||
let currentLine = "";
|
let currentLine = "";
|
||||||
let currentLineWidthTillNow = 0;
|
let currentLineWidthTillNow = 0;
|
||||||
|
@ -427,13 +454,14 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
currentLine = "";
|
currentLine = "";
|
||||||
currentLineWidthTillNow = 0;
|
currentLineWidthTillNow = 0;
|
||||||
};
|
};
|
||||||
originalLines.forEach((originalLine) => {
|
|
||||||
const currentLineWidth = getTextWidth(originalLine, font);
|
for (const originalLine of originalLines) {
|
||||||
|
const currentLineWidth = getLineWidth(originalLine, font, true);
|
||||||
|
|
||||||
// Push the line if its <= maxWidth
|
// Push the line if its <= maxWidth
|
||||||
if (currentLineWidth <= maxWidth) {
|
if (currentLineWidth <= maxWidth) {
|
||||||
lines.push(originalLine);
|
lines.push(originalLine);
|
||||||
return; // continue
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const words = parseTokens(originalLine);
|
const words = parseTokens(originalLine);
|
||||||
|
@ -442,7 +470,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
while (index < words.length) {
|
while (index < words.length) {
|
||||||
const currentWordWidth = getLineWidth(words[index], font);
|
const currentWordWidth = getLineWidth(words[index], font, true);
|
||||||
|
|
||||||
// This will only happen when single word takes entire width
|
// This will only happen when single word takes entire width
|
||||||
if (currentWordWidth === maxWidth) {
|
if (currentWordWidth === maxWidth) {
|
||||||
|
@ -454,7 +482,6 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
else if (currentWordWidth > maxWidth) {
|
else if (currentWordWidth > maxWidth) {
|
||||||
// push current line since the current word exceeds the max width
|
// push current line since the current word exceeds the max width
|
||||||
// so will be appended in next line
|
// so will be appended in next line
|
||||||
|
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
|
|
||||||
resetParams();
|
resetParams();
|
||||||
|
@ -463,20 +490,26 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
const currentChar = String.fromCodePoint(
|
const currentChar = String.fromCodePoint(
|
||||||
words[index].codePointAt(0)!,
|
words[index].codePointAt(0)!,
|
||||||
);
|
);
|
||||||
const width = charWidth.calculate(currentChar, font);
|
|
||||||
currentLineWidthTillNow += width;
|
const line = currentLine + currentChar;
|
||||||
|
// use advance width instead of the actual width as it's closest to the browser wapping algo
|
||||||
|
// use width of the whole line instead of calculating individual chars to accomodate for kerning
|
||||||
|
const lineAdvanceWidth = getLineWidth(line, font, true);
|
||||||
|
const charAdvanceWidth = charWidth.calculate(currentChar, font);
|
||||||
|
|
||||||
|
currentLineWidthTillNow = lineAdvanceWidth;
|
||||||
words[index] = words[index].slice(currentChar.length);
|
words[index] = words[index].slice(currentChar.length);
|
||||||
|
|
||||||
if (currentLineWidthTillNow >= maxWidth) {
|
if (currentLineWidthTillNow >= maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
currentLine = currentChar;
|
currentLine = currentChar;
|
||||||
currentLineWidthTillNow = width;
|
currentLineWidthTillNow = charAdvanceWidth;
|
||||||
} else {
|
} else {
|
||||||
currentLine += currentChar;
|
currentLine = line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// push current line if appending space exceeds max width
|
// push current line if appending space exceeds max width
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
if (currentLineWidthTillNow + spaceAdvanceWidth >= maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
resetParams();
|
resetParams();
|
||||||
// space needs to be appended before next word
|
// space needs to be appended before next word
|
||||||
|
@ -485,14 +518,18 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
// with css word-wrap
|
// with css word-wrap
|
||||||
} else if (!currentLine.endsWith("-")) {
|
} else if (!currentLine.endsWith("-")) {
|
||||||
currentLine += " ";
|
currentLine += " ";
|
||||||
currentLineWidthTillNow += spaceWidth;
|
currentLineWidthTillNow += spaceAdvanceWidth;
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
} else {
|
} else {
|
||||||
// Start appending words in a line till max width reached
|
// Start appending words in a line till max width reached
|
||||||
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
while (currentLineWidthTillNow < maxWidth && index < words.length) {
|
||||||
const word = words[index];
|
const word = words[index];
|
||||||
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
currentLineWidthTillNow = getLineWidth(
|
||||||
|
currentLine + word,
|
||||||
|
font,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
if (currentLineWidthTillNow > maxWidth) {
|
if (currentLineWidthTillNow > maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
|
@ -512,7 +549,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push the word if appending space exceeds max width
|
// Push the word if appending space exceeds max width
|
||||||
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
|
if (currentLineWidthTillNow + spaceAdvanceWidth >= maxWidth) {
|
||||||
if (shouldAppendSpace) {
|
if (shouldAppendSpace) {
|
||||||
lines.push(currentLine.slice(0, -1));
|
lines.push(currentLine.slice(0, -1));
|
||||||
} else {
|
} else {
|
||||||
|
@ -524,12 +561,14 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentLine.slice(-1) === " ") {
|
if (currentLine.slice(-1) === " ") {
|
||||||
// only remove last trailing space which we have added when joining words
|
// only remove last trailing space which we have added when joining words
|
||||||
currentLine = currentLine.slice(0, -1);
|
currentLine = currentLine.slice(0, -1);
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -542,7 +581,7 @@ export const charWidth = (() => {
|
||||||
cachedCharWidth[font] = [];
|
cachedCharWidth[font] = [];
|
||||||
}
|
}
|
||||||
if (!cachedCharWidth[font][ascii]) {
|
if (!cachedCharWidth[font][ascii]) {
|
||||||
const width = getLineWidth(char, font);
|
const width = getLineWidth(char, font, true);
|
||||||
cachedCharWidth[font][ascii] = width;
|
cachedCharWidth[font][ascii] = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -594,30 +633,6 @@ export const getMaxCharWidth = (font: FontString) => {
|
||||||
return Math.max(...cacheWithOutEmpty);
|
return Math.max(...cacheWithOutEmpty);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
|
||||||
// Generally lower case is used so converting to lower case
|
|
||||||
const dummyText = DUMMY_TEXT.toLocaleLowerCase();
|
|
||||||
const batchLength = 6;
|
|
||||||
let index = 0;
|
|
||||||
let widthTillNow = 0;
|
|
||||||
let str = "";
|
|
||||||
while (widthTillNow <= width) {
|
|
||||||
const batch = dummyText.substr(index, index + batchLength);
|
|
||||||
str += batch;
|
|
||||||
widthTillNow += getLineWidth(str, font);
|
|
||||||
if (index === dummyText.length - 1) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
index = index + batchLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (widthTillNow > width) {
|
|
||||||
str = str.substr(0, str.length - 1);
|
|
||||||
widthTillNow = getLineWidth(str, font);
|
|
||||||
}
|
|
||||||
return str.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
||||||
return container?.boundElements?.length
|
return container?.boundElements?.length
|
||||||
? container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id ||
|
? container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id ||
|
||||||
|
@ -866,79 +881,6 @@ export const isMeasureTextSupported = () => {
|
||||||
return width > 0;
|
return width > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Unitless line height
|
|
||||||
*
|
|
||||||
* In previous versions we used `normal` line height, which browsers interpret
|
|
||||||
* differently, and based on font-family and font-size.
|
|
||||||
*
|
|
||||||
* To make line heights consistent across browsers we hardcode the values for
|
|
||||||
* each of our fonts based on most common average line-heights.
|
|
||||||
* See https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971
|
|
||||||
* where the values come from.
|
|
||||||
*/
|
|
||||||
const DEFAULT_LINE_HEIGHT = {
|
|
||||||
// ~1.25 is the average for Virgil in WebKit and Blink.
|
|
||||||
// Gecko (FF) uses ~1.28.
|
|
||||||
[FONT_FAMILY.Virgil]: 1.25 as ExcalidrawTextElement["lineHeight"],
|
|
||||||
// ~1.15 is the average for Helvetica in WebKit and Blink.
|
|
||||||
[FONT_FAMILY.Helvetica]: 1.15 as ExcalidrawTextElement["lineHeight"],
|
|
||||||
// ~1.2 is the average for Cascadia in WebKit and Blink, and kinda Gecko too
|
|
||||||
[FONT_FAMILY.Cascadia]: 1.2 as ExcalidrawTextElement["lineHeight"],
|
|
||||||
};
|
|
||||||
|
|
||||||
/** OS/2 sTypoAscender, https://learn.microsoft.com/en-us/typography/opentype/spec/os2#stypoascender */
|
|
||||||
type sTypoAscender = number & MakeBrand<"sTypoAscender">;
|
|
||||||
|
|
||||||
/** OS/2 sTypoDescender, https://learn.microsoft.com/en-us/typography/opentype/spec/os2#stypodescender */
|
|
||||||
type sTypoDescender = number & MakeBrand<"sTypoDescender">;
|
|
||||||
|
|
||||||
/** head.unitsPerEm, usually either 1000 or 2048 */
|
|
||||||
type unitsPerEm = number & MakeBrand<"unitsPerEm">;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hardcoded metrics for default fonts, read by https://opentype.js.org/font-inspector.html.
|
|
||||||
* For custom fonts, read these metrics from OS/2 table and extend this object.
|
|
||||||
*
|
|
||||||
* WARN: opentype does NOT open WOFF2 correctly, make sure to convert WOFF2 to TTF first.
|
|
||||||
*/
|
|
||||||
export const FONT_METRICS: Record<
|
|
||||||
number,
|
|
||||||
{
|
|
||||||
unitsPerEm: number;
|
|
||||||
ascender: sTypoAscender;
|
|
||||||
descender: sTypoDescender;
|
|
||||||
}
|
|
||||||
> = {
|
|
||||||
[FONT_FAMILY.Virgil]: {
|
|
||||||
unitsPerEm: 1000 as unitsPerEm,
|
|
||||||
ascender: 886 as sTypoAscender,
|
|
||||||
descender: -374 as sTypoDescender,
|
|
||||||
},
|
|
||||||
[FONT_FAMILY.Helvetica]: {
|
|
||||||
unitsPerEm: 2048 as unitsPerEm,
|
|
||||||
ascender: 1577 as sTypoAscender,
|
|
||||||
descender: -471 as sTypoDescender,
|
|
||||||
},
|
|
||||||
[FONT_FAMILY.Cascadia]: {
|
|
||||||
unitsPerEm: 2048 as unitsPerEm,
|
|
||||||
ascender: 1977 as sTypoAscender,
|
|
||||||
descender: -480 as sTypoDescender,
|
|
||||||
},
|
|
||||||
[FONT_FAMILY.Assistant]: {
|
|
||||||
unitsPerEm: 1000 as unitsPerEm,
|
|
||||||
ascender: 1021 as sTypoAscender,
|
|
||||||
descender: -287 as sTypoDescender,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
|
|
||||||
if (fontFamily in DEFAULT_LINE_HEIGHT) {
|
|
||||||
return DEFAULT_LINE_HEIGHT[fontFamily];
|
|
||||||
}
|
|
||||||
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMinTextElementWidth = (
|
export const getMinTextElementWidth = (
|
||||||
font: FontString,
|
font: FontString,
|
||||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||||
|
|
|
@ -916,13 +916,13 @@ describe("textWysiwyg", () => {
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
|
expect(text.fontFamily).toEqual(FONT_FAMILY.Excalifont);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle(/code/i));
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
||||||
).toEqual(FONT_FAMILY.Cascadia);
|
).toEqual(FONT_FAMILY["Comic Shanns"]);
|
||||||
|
|
||||||
//undo
|
//undo
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
@ -930,7 +930,7 @@ describe("textWysiwyg", () => {
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
||||||
).toEqual(FONT_FAMILY.Virgil);
|
).toEqual(FONT_FAMILY.Excalifont);
|
||||||
|
|
||||||
//redo
|
//redo
|
||||||
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
||||||
|
@ -938,7 +938,7 @@ describe("textWysiwyg", () => {
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
||||||
).toEqual(FONT_FAMILY.Cascadia);
|
).toEqual(FONT_FAMILY["Comic Shanns"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should wrap text and vertcially center align once text submitted", async () => {
|
it("should wrap text and vertcially center align once text submitted", async () => {
|
||||||
|
@ -1330,14 +1330,14 @@ describe("textWysiwyg", () => {
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
||||||
).toEqual(FONT_FAMILY.Cascadia);
|
).toEqual(FONT_FAMILY["Comic Shanns"]);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle(/Very large/i));
|
fireEvent.click(screen.getByTitle(/Very large/i));
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontSize,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontSize,
|
||||||
).toEqual(36);
|
).toEqual(36);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(97);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update line height when font family updated", async () => {
|
it("should update line height when font family updated", async () => {
|
||||||
|
@ -1357,18 +1357,18 @@ describe("textWysiwyg", () => {
|
||||||
fireEvent.click(screen.getByTitle(/code/i));
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
||||||
).toEqual(FONT_FAMILY.Cascadia);
|
).toEqual(FONT_FAMILY["Comic Shanns"]);
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
).toEqual(1.2);
|
).toEqual(1.25);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle(/normal/i));
|
fireEvent.click(screen.getByTitle(/normal/i));
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
|
||||||
).toEqual(FONT_FAMILY.Helvetica);
|
).toEqual(FONT_FAMILY.Nunito);
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
).toEqual(1.15);
|
).toEqual(1.35);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should align correctly", () => {
|
describe("should align correctly", () => {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { CLASSES } from "../constants";
|
import { CLASSES, isSafari } from "../constants";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
@ -132,10 +132,15 @@ export const textWysiwyg = ({
|
||||||
updatedTextElement,
|
updatedTextElement,
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let width = updatedTextElement.width;
|
||||||
|
|
||||||
|
// set to element height by default since that's
|
||||||
|
// what is going to be used for unbounded text
|
||||||
|
let height = updatedTextElement.height;
|
||||||
|
|
||||||
let maxWidth = updatedTextElement.width;
|
let maxWidth = updatedTextElement.width;
|
||||||
let maxHeight = updatedTextElement.height;
|
let maxHeight = updatedTextElement.height;
|
||||||
let textElementWidth = updatedTextElement.width;
|
|
||||||
const textElementHeight = updatedTextElement.height;
|
|
||||||
|
|
||||||
if (container && updatedTextElement.containerId) {
|
if (container && updatedTextElement.containerId) {
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
|
@ -177,9 +182,9 @@ export const textWysiwyg = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
// autogrow container height if text exceeds
|
// autogrow container height if text exceeds
|
||||||
if (!isArrowElement(container) && textElementHeight > maxHeight) {
|
if (!isArrowElement(container) && height > maxHeight) {
|
||||||
const targetContainerHeight = computeContainerDimensionForBoundText(
|
const targetContainerHeight = computeContainerDimensionForBoundText(
|
||||||
textElementHeight,
|
height,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -190,10 +195,10 @@ export const textWysiwyg = ({
|
||||||
// is reached when text is removed
|
// is reached when text is removed
|
||||||
!isArrowElement(container) &&
|
!isArrowElement(container) &&
|
||||||
container.height > originalContainerData.height &&
|
container.height > originalContainerData.height &&
|
||||||
textElementHeight < maxHeight
|
height < maxHeight
|
||||||
) {
|
) {
|
||||||
const targetContainerHeight = computeContainerDimensionForBoundText(
|
const targetContainerHeight = computeContainerDimensionForBoundText(
|
||||||
textElementHeight,
|
height,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: targetContainerHeight });
|
mutateElement(container, { height: targetContainerHeight });
|
||||||
|
@ -226,30 +231,41 @@ export const textWysiwyg = ({
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
||||||
textElementWidth = Math.min(textElementWidth, maxWidth);
|
width = Math.min(width, maxWidth);
|
||||||
} else {
|
} else {
|
||||||
textElementWidth += 0.5;
|
width += 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add 5% buffer otherwise it causes wysiwyg to jump
|
||||||
|
height *= 1.05;
|
||||||
|
|
||||||
|
const font = getFontString(updatedTextElement);
|
||||||
|
|
||||||
|
// adding left and right padding buffer, so that browser does not cut the glyphs (does not work in Safari)
|
||||||
|
const padding = !isSafari
|
||||||
|
? Math.ceil(updatedTextElement.fontSize / 2)
|
||||||
|
: 0;
|
||||||
|
|
||||||
// Make sure text editor height doesn't go beyond viewport
|
// Make sure text editor height doesn't go beyond viewport
|
||||||
const editorMaxHeight =
|
const editorMaxHeight =
|
||||||
(appState.height - viewportY) / appState.zoom.value;
|
(appState.height - viewportY) / appState.zoom.value;
|
||||||
Object.assign(editable.style, {
|
Object.assign(editable.style, {
|
||||||
font: getFontString(updatedTextElement),
|
font,
|
||||||
// must be defined *after* font ¯\_(ツ)_/¯
|
// must be defined *after* font ¯\_(ツ)_/¯
|
||||||
lineHeight: updatedTextElement.lineHeight,
|
lineHeight: updatedTextElement.lineHeight,
|
||||||
width: `${textElementWidth}px`,
|
width: `${width}px`,
|
||||||
height: `${textElementHeight}px`,
|
height: `${height}px`,
|
||||||
left: `${viewportX}px`,
|
left: `${viewportX - padding}px`,
|
||||||
top: `${viewportY}px`,
|
top: `${viewportY}px`,
|
||||||
transform: getTransform(
|
transform: getTransform(
|
||||||
textElementWidth,
|
width,
|
||||||
textElementHeight,
|
height,
|
||||||
getTextElementAngle(updatedTextElement, container),
|
getTextElementAngle(updatedTextElement, container),
|
||||||
appState,
|
appState,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
editorMaxHeight,
|
editorMaxHeight,
|
||||||
),
|
),
|
||||||
|
padding: `0 ${padding}px`,
|
||||||
textAlign,
|
textAlign,
|
||||||
verticalAlign,
|
verticalAlign,
|
||||||
color: updatedTextElement.strokeColor,
|
color: updatedTextElement.strokeColor,
|
||||||
|
@ -290,7 +306,6 @@ export const textWysiwyg = ({
|
||||||
minHeight: "1em",
|
minHeight: "1em",
|
||||||
backfaceVisibility: "hidden",
|
backfaceVisibility: "hidden",
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
|
||||||
border: 0,
|
border: 0,
|
||||||
outline: 0,
|
outline: 0,
|
||||||
resize: "none",
|
resize: "none",
|
||||||
|
@ -336,7 +351,7 @@ export const textWysiwyg = ({
|
||||||
font,
|
font,
|
||||||
getBoundTextMaxWidth(container, boundTextElement),
|
getBoundTextMaxWidth(container, boundTextElement),
|
||||||
);
|
);
|
||||||
const width = getTextWidth(wrappedText, font);
|
const width = getTextWidth(wrappedText, font, true);
|
||||||
editable.style.width = `${width}px`;
|
editable.style.width = `${width}px`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -485,8 +500,10 @@ export const textWysiwyg = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopEvent = (event: Event) => {
|
const stopEvent = (event: Event) => {
|
||||||
event.preventDefault();
|
if (event.target instanceof HTMLCanvasElement) {
|
||||||
event.stopPropagation();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// using a state variable instead of passing it to the handleSubmit callback
|
// using a state variable instead of passing it to the handleSubmit callback
|
||||||
|
@ -579,46 +596,15 @@ export const textWysiwyg = ({
|
||||||
// in that same tick.
|
// in that same tick.
|
||||||
const target = event?.target;
|
const target = event?.target;
|
||||||
|
|
||||||
const isTargetPickerTrigger =
|
const isPropertiesTrigger =
|
||||||
target instanceof HTMLElement &&
|
target instanceof HTMLElement &&
|
||||||
target.classList.contains("active-color");
|
target.classList.contains("properties-trigger");
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editable.onblur = handleSubmit;
|
editable.onblur = handleSubmit;
|
||||||
|
|
||||||
if (isTargetPickerTrigger) {
|
|
||||||
const callback = (
|
|
||||||
mutationList: MutationRecord[],
|
|
||||||
observer: MutationObserver,
|
|
||||||
) => {
|
|
||||||
const radixIsRemoved = mutationList.find(
|
|
||||||
(mutation) =>
|
|
||||||
mutation.removedNodes.length > 0 &&
|
|
||||||
(mutation.removedNodes[0] as HTMLElement).dataset
|
|
||||||
?.radixPopperContentWrapper !== undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (radixIsRemoved) {
|
|
||||||
// should work without this in theory
|
|
||||||
// and i think it does actually but radix probably somewhere,
|
|
||||||
// somehow sets the focus elsewhere
|
|
||||||
setTimeout(() => {
|
|
||||||
editable.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new MutationObserver(callback);
|
|
||||||
|
|
||||||
observer.observe(document.querySelector(".excalidraw-container")!, {
|
|
||||||
childList: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// case: clicking on the same property → no change → no update → no focus
|
// case: clicking on the same property → no change → no update → no focus
|
||||||
if (!isTargetPickerTrigger) {
|
if (!isPropertiesTrigger) {
|
||||||
editable.focus();
|
editable.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -626,16 +612,18 @@ export const textWysiwyg = ({
|
||||||
|
|
||||||
// prevent blur when changing properties from the menu
|
// prevent blur when changing properties from the menu
|
||||||
const onPointerDown = (event: MouseEvent) => {
|
const onPointerDown = (event: MouseEvent) => {
|
||||||
const isTargetPickerTrigger =
|
const target = event?.target;
|
||||||
event.target instanceof HTMLElement &&
|
|
||||||
event.target.classList.contains("active-color");
|
const isPropertiesTrigger =
|
||||||
|
target instanceof HTMLElement &&
|
||||||
|
target.classList.contains("properties-trigger");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
((event.target instanceof HTMLElement ||
|
((event.target instanceof HTMLElement ||
|
||||||
event.target instanceof SVGElement) &&
|
event.target instanceof SVGElement) &&
|
||||||
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
|
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
|
||||||
!isWritableElement(event.target)) ||
|
!isWritableElement(event.target)) ||
|
||||||
isTargetPickerTrigger
|
isPropertiesTrigger
|
||||||
) {
|
) {
|
||||||
editable.onblur = null;
|
editable.onblur = null;
|
||||||
window.addEventListener("pointerup", bindBlurEvent);
|
window.addEventListener("pointerup", bindBlurEvent);
|
||||||
|
@ -644,7 +632,7 @@ export const textWysiwyg = ({
|
||||||
window.addEventListener("blur", handleSubmit);
|
window.addEventListener("blur", handleSubmit);
|
||||||
} else if (
|
} else if (
|
||||||
event.target instanceof HTMLElement &&
|
event.target instanceof HTMLElement &&
|
||||||
!event.target.contains(editable) &&
|
event.target instanceof HTMLCanvasElement &&
|
||||||
// Vitest simply ignores stopPropagation, capture-mode, or rAF
|
// Vitest simply ignores stopPropagation, capture-mode, or rAF
|
||||||
// so without introducing crazier hacks, nothing we can do
|
// so without introducing crazier hacks, nothing we can do
|
||||||
!isTestEnv()
|
!isTestEnv()
|
||||||
|
@ -664,10 +652,10 @@ export const textWysiwyg = ({
|
||||||
// handle updates of textElement properties of editing element
|
// handle updates of textElement properties of editing element
|
||||||
const unbindUpdate = Scene.getScene(element)!.onUpdate(() => {
|
const unbindUpdate = Scene.getScene(element)!.onUpdate(() => {
|
||||||
updateWysiwygStyle();
|
updateWysiwygStyle();
|
||||||
const isColorPickerActive = !!document.activeElement?.closest(
|
const isPopupOpened = !!document.activeElement?.closest(
|
||||||
".color-picker-content",
|
".properties-content",
|
||||||
);
|
);
|
||||||
if (!isColorPickerActive) {
|
if (!isPopupOpened) {
|
||||||
editable.focus();
|
editable.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
78
packages/excalidraw/fonts/ExcalidrawFont.ts
Normal file
78
packages/excalidraw/fonts/ExcalidrawFont.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { stringToBase64, toByteString } from "../data/encode";
|
||||||
|
|
||||||
|
export interface Font {
|
||||||
|
url: URL;
|
||||||
|
fontFace: FontFace;
|
||||||
|
getContent(): Promise<string>;
|
||||||
|
}
|
||||||
|
export const UNPKG_PROD_URL = `https://unpkg.com/${
|
||||||
|
import.meta.env.VITE_PKG_NAME
|
||||||
|
}@${import.meta.env.PKG_VERSION}/dist/prod/`;
|
||||||
|
|
||||||
|
export class ExcalidrawFont implements Font {
|
||||||
|
public readonly url: URL;
|
||||||
|
public readonly fontFace: FontFace;
|
||||||
|
|
||||||
|
constructor(family: string, uri: string, descriptors?: FontFaceDescriptors) {
|
||||||
|
// absolute assets paths, which are found in tests and excalidraw-app build, won't work with base url, so we are stripping initial slash away
|
||||||
|
const assetUrl: string = uri.replace(/^\/+/, "");
|
||||||
|
let baseUrl: string | undefined = undefined;
|
||||||
|
|
||||||
|
// fallback to unpkg to form a valid URL in case of a passed relative assetUrl
|
||||||
|
let baseUrlBuilder = window.EXCALIDRAW_ASSET_PATH || UNPKG_PROD_URL;
|
||||||
|
|
||||||
|
// in case user passed a root-relative url (~absolute path),
|
||||||
|
// like "/" or "/some/path", or relative (starts with "./"),
|
||||||
|
// prepend it with `location.origin`
|
||||||
|
if (/^\.?\//.test(baseUrlBuilder)) {
|
||||||
|
baseUrlBuilder = new URL(
|
||||||
|
baseUrlBuilder.replace(/^\.?\/+/, ""),
|
||||||
|
window?.location?.origin,
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure there is a trailing slash, otherwise url won't be correctly concatenated
|
||||||
|
baseUrl = `${baseUrlBuilder.replace(/\/+$/, "")}/`;
|
||||||
|
|
||||||
|
this.url = new URL(assetUrl, baseUrl);
|
||||||
|
this.fontFace = new FontFace(family, `url(${this.url})`, {
|
||||||
|
display: "swap",
|
||||||
|
style: "normal",
|
||||||
|
weight: "400",
|
||||||
|
...descriptors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches woff2 content based on the registered url (browser).
|
||||||
|
*
|
||||||
|
* Use dataurl outside the browser environment.
|
||||||
|
*/
|
||||||
|
public async getContent(): Promise<string> {
|
||||||
|
if (this.url.protocol === "data:") {
|
||||||
|
// it's dataurl, the font is inlined as base64, no need to fetch
|
||||||
|
return this.url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(this.url, {
|
||||||
|
headers: {
|
||||||
|
Accept: "font/woff2",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(
|
||||||
|
`Couldn't fetch font-family "${this.fontFace.family}" from url "${this.url}"`,
|
||||||
|
response,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mimeType = await response.headers.get("Content-Type");
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
|
||||||
|
return `data:${mimeType};base64,${await stringToBase64(
|
||||||
|
await toByteString(buffer),
|
||||||
|
true,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
}
|
BIN
packages/excalidraw/fonts/assets/CascadiaMono-Regular.woff2
Normal file
BIN
packages/excalidraw/fonts/assets/CascadiaMono-Regular.woff2
Normal file
Binary file not shown.
BIN
packages/excalidraw/fonts/assets/ComicShanns-Regular.woff2
Normal file
BIN
packages/excalidraw/fonts/assets/ComicShanns-Regular.woff2
Normal file
Binary file not shown.
BIN
packages/excalidraw/fonts/assets/Excalifont-Regular.woff2
Normal file
BIN
packages/excalidraw/fonts/assets/Excalifont-Regular.woff2
Normal file
Binary file not shown.
BIN
packages/excalidraw/fonts/assets/LiberationSans-Regular.woff2
Normal file
BIN
packages/excalidraw/fonts/assets/LiberationSans-Regular.woff2
Normal file
Binary file not shown.
BIN
packages/excalidraw/fonts/assets/Virgil-Regular.woff2
Normal file
BIN
packages/excalidraw/fonts/assets/Virgil-Regular.woff2
Normal file
Binary file not shown.
34
packages/excalidraw/fonts/assets/fonts.css
Normal file
34
packages/excalidraw/fonts/assets/fonts.css
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/* Only UI fonts here, which are needed before the editor initializes. */
|
||||||
|
/* These also cannot be preprended with `EXCALIDRAW_ASSET_PATH`. */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Assistant";
|
||||||
|
src: url(./Assistant-Regular.woff2) format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
style: normal;
|
||||||
|
display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Assistant";
|
||||||
|
src: url(./Assistant-Medium.woff2) format("woff2");
|
||||||
|
font-weight: 500;
|
||||||
|
style: normal;
|
||||||
|
display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Assistant";
|
||||||
|
src: url(./Assistant-SemiBold.woff2) format("woff2");
|
||||||
|
font-weight: 600;
|
||||||
|
style: normal;
|
||||||
|
display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Assistant";
|
||||||
|
src: url(./Assistant-Bold.woff2) format("woff2");
|
||||||
|
font-weight: 700;
|
||||||
|
style: normal;
|
||||||
|
display: swap;
|
||||||
|
}
|
308
packages/excalidraw/fonts/index.ts
Normal file
308
packages/excalidraw/fonts/index.ts
Normal file
|
@ -0,0 +1,308 @@
|
||||||
|
import type Scene from "../scene/Scene";
|
||||||
|
import type { ValueOf } from "../utility-types";
|
||||||
|
import type { ExcalidrawTextElement, FontFamilyValues } from "../element/types";
|
||||||
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
import { isTextElement } from "../element";
|
||||||
|
import { getFontString } from "../utils";
|
||||||
|
import { FONT_FAMILY } from "../constants";
|
||||||
|
import {
|
||||||
|
LOCAL_FONT_PROTOCOL,
|
||||||
|
FONT_METADATA,
|
||||||
|
RANGES,
|
||||||
|
type FontMetadata,
|
||||||
|
} from "./metadata";
|
||||||
|
import { ExcalidrawFont, type Font } from "./ExcalidrawFont";
|
||||||
|
import { getContainerElement } from "../element/textElement";
|
||||||
|
|
||||||
|
import Virgil from "./assets/Virgil-Regular.woff2";
|
||||||
|
import Excalifont from "./assets/Excalifont-Regular.woff2";
|
||||||
|
import Cascadia from "./assets/CascadiaMono-Regular.woff2";
|
||||||
|
import ComicShanns from "./assets/ComicShanns-Regular.woff2";
|
||||||
|
import LiberationSans from "./assets/LiberationSans-Regular.woff2";
|
||||||
|
|
||||||
|
import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
|
||||||
|
import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
|
||||||
|
|
||||||
|
import NunitoLatin from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
|
||||||
|
import NunitoLatinExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
|
||||||
|
import NunitoCyrilic from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
|
||||||
|
import NunitoCyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
|
||||||
|
import NunitoVietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
|
||||||
|
|
||||||
|
export class Fonts {
|
||||||
|
// it's ok to track fonts across multiple instances only once, so let's use
|
||||||
|
// a static member to reduce memory footprint
|
||||||
|
public static readonly loadedFontsCache = new Set<string>();
|
||||||
|
|
||||||
|
private static _registered:
|
||||||
|
| Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
metadata: FontMetadata;
|
||||||
|
fontFaces: Font[];
|
||||||
|
}
|
||||||
|
>
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
public static get registered() {
|
||||||
|
if (!Fonts._registered) {
|
||||||
|
// lazy load the fonts
|
||||||
|
Fonts._registered = Fonts.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Fonts._registered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get registered() {
|
||||||
|
return Fonts.registered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly scene: Scene;
|
||||||
|
|
||||||
|
public get sceneFamilies() {
|
||||||
|
return Array.from(
|
||||||
|
this.scene.getNonDeletedElements().reduce((families, element) => {
|
||||||
|
if (isTextElement(element)) {
|
||||||
|
families.add(element.fontFamily);
|
||||||
|
}
|
||||||
|
return families;
|
||||||
|
}, new Set<number>()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor({ scene }: { scene: Scene }) {
|
||||||
|
this.scene = scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if we load a (new) font, it's likely that text elements using it have
|
||||||
|
* already been rendered using a fallback font. Thus, we want invalidate
|
||||||
|
* their shapes and rerender. See #637.
|
||||||
|
*
|
||||||
|
* Invalidates text elements and rerenders scene, provided that at least one
|
||||||
|
* of the supplied fontFaces has not already been processed.
|
||||||
|
*/
|
||||||
|
public onLoaded = (fontFaces: readonly FontFace[]) => {
|
||||||
|
if (
|
||||||
|
// bail if all fonts with have been processed. We're checking just a
|
||||||
|
// subset of the font properties (though it should be enough), so it
|
||||||
|
// can technically bail on a false positive.
|
||||||
|
fontFaces.every((fontFace) => {
|
||||||
|
const sig = `${fontFace.family}-${fontFace.style}-${fontFace.weight}-${fontFace.unicodeRange}`;
|
||||||
|
if (Fonts.loadedFontsCache.has(sig)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Fonts.loadedFontsCache.add(sig);
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let didUpdate = false;
|
||||||
|
|
||||||
|
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
|
for (const element of this.scene.getNonDeletedElements()) {
|
||||||
|
if (isTextElement(element)) {
|
||||||
|
didUpdate = true;
|
||||||
|
ShapeCache.delete(element);
|
||||||
|
const container = getContainerElement(element, elementsMap);
|
||||||
|
if (container) {
|
||||||
|
ShapeCache.delete(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (didUpdate) {
|
||||||
|
this.scene.triggerUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public load = async () => {
|
||||||
|
// Add all registered font faces into the `document.fonts` (if not added already)
|
||||||
|
for (const { fontFaces } of Fonts.registered.values()) {
|
||||||
|
for (const { fontFace, url } of fontFaces) {
|
||||||
|
if (
|
||||||
|
url.protocol !== LOCAL_FONT_PROTOCOL &&
|
||||||
|
!window.document.fonts.has(fontFace)
|
||||||
|
) {
|
||||||
|
window.document.fonts.add(fontFace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loaded = await Promise.all(
|
||||||
|
this.sceneFamilies.map(async (fontFamily) => {
|
||||||
|
const fontString = getFontString({
|
||||||
|
fontFamily,
|
||||||
|
fontSize: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
// WARN: without "text" param it does not have to mean that all font faces are loaded, instead it could be just one!
|
||||||
|
if (!window.document.fonts.check(fontString)) {
|
||||||
|
try {
|
||||||
|
// WARN: browser prioritizes loading only font faces with unicode ranges for characters which are present in the document (html & canvas), other font faces could stay unloaded
|
||||||
|
// we might want to retry here, i.e. in case CDN is down, but so far I didn't experience any issues - maybe it handles retry-like logic under the hood
|
||||||
|
return await window.document.fonts.load(fontString);
|
||||||
|
} catch (e) {
|
||||||
|
// don't let it all fail if just one font fails to load
|
||||||
|
console.error(
|
||||||
|
`Failed to load font: "${fontString}" with error "${e}", given the following registered font:`,
|
||||||
|
JSON.stringify(Fonts.registered.get(fontFamily), undefined, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.onLoaded(loaded.flat().filter(Boolean) as FontFace[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WARN: should be called just once on init, even across multiple instances.
|
||||||
|
*/
|
||||||
|
private static init() {
|
||||||
|
const fonts = {
|
||||||
|
registered: new Map<
|
||||||
|
ValueOf<typeof FONT_FAMILY>,
|
||||||
|
{ metadata: FontMetadata; fontFaces: Font[] }
|
||||||
|
>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const _register = register.bind(fonts);
|
||||||
|
|
||||||
|
_register("Virgil", FONT_METADATA[FONT_FAMILY.Virgil], {
|
||||||
|
uri: Virgil,
|
||||||
|
});
|
||||||
|
|
||||||
|
_register("Excalifont", FONT_METADATA[FONT_FAMILY.Excalifont], {
|
||||||
|
uri: Excalifont,
|
||||||
|
});
|
||||||
|
|
||||||
|
// keeping for backwards compatibility reasons, uses system font (Helvetica on MacOS, Arial on Win)
|
||||||
|
_register("Helvetica", FONT_METADATA[FONT_FAMILY.Helvetica], {
|
||||||
|
uri: LOCAL_FONT_PROTOCOL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// used for server-side pdf & png export instead of helvetica (technically does not need metrics, but kept in for consistency)
|
||||||
|
_register(
|
||||||
|
"Liberation Sans",
|
||||||
|
FONT_METADATA[FONT_FAMILY["Liberation Sans"]],
|
||||||
|
{
|
||||||
|
uri: LiberationSans,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_register("Cascadia", FONT_METADATA[FONT_FAMILY.Cascadia], {
|
||||||
|
uri: Cascadia,
|
||||||
|
});
|
||||||
|
|
||||||
|
_register("Comic Shanns", FONT_METADATA[FONT_FAMILY["Comic Shanns"]], {
|
||||||
|
uri: ComicShanns,
|
||||||
|
});
|
||||||
|
|
||||||
|
_register(
|
||||||
|
"Lilita One",
|
||||||
|
FONT_METADATA[FONT_FAMILY["Lilita One"]],
|
||||||
|
{ uri: LilitaLatinExt, descriptors: { unicodeRange: RANGES.LATIN_EXT } },
|
||||||
|
{ uri: LilitaLatin, descriptors: { unicodeRange: RANGES.LATIN } },
|
||||||
|
);
|
||||||
|
|
||||||
|
_register(
|
||||||
|
"Nunito",
|
||||||
|
FONT_METADATA[FONT_FAMILY.Nunito],
|
||||||
|
{
|
||||||
|
uri: NunitoCyrilicExt,
|
||||||
|
descriptors: { unicodeRange: RANGES.CYRILIC_EXT, weight: "500" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: NunitoCyrilic,
|
||||||
|
descriptors: { unicodeRange: RANGES.CYRILIC, weight: "500" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: NunitoVietnamese,
|
||||||
|
descriptors: { unicodeRange: RANGES.VIETNAMESE, weight: "500" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: NunitoLatinExt,
|
||||||
|
descriptors: { unicodeRange: RANGES.LATIN_EXT, weight: "500" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: NunitoLatin,
|
||||||
|
descriptors: { unicodeRange: RANGES.LATIN, weight: "500" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return fonts.registered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new font.
|
||||||
|
*
|
||||||
|
* @param family font family
|
||||||
|
* @param metadata font metadata
|
||||||
|
* @param params array of the rest of the FontFace parameters [uri: string, descriptors: FontFaceDescriptors?] ,
|
||||||
|
*/
|
||||||
|
function register(
|
||||||
|
this:
|
||||||
|
| Fonts
|
||||||
|
| {
|
||||||
|
registered: Map<
|
||||||
|
ValueOf<typeof FONT_FAMILY>,
|
||||||
|
{ metadata: FontMetadata; fontFaces: Font[] }
|
||||||
|
>;
|
||||||
|
},
|
||||||
|
family: string,
|
||||||
|
metadata: FontMetadata,
|
||||||
|
...params: Array<{ uri: string; descriptors?: FontFaceDescriptors }>
|
||||||
|
) {
|
||||||
|
// TODO: likely we will need to abandon number "id" in order to support custom fonts
|
||||||
|
const familyId = FONT_FAMILY[family as keyof typeof FONT_FAMILY];
|
||||||
|
const registeredFamily = this.registered.get(familyId);
|
||||||
|
|
||||||
|
if (!registeredFamily) {
|
||||||
|
this.registered.set(familyId, {
|
||||||
|
metadata,
|
||||||
|
fontFaces: params.map(
|
||||||
|
({ uri, descriptors }) => new ExcalidrawFont(family, uri, descriptors),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.registered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates vertical offset for a text with alphabetic baseline.
|
||||||
|
*/
|
||||||
|
export const getVerticalOffset = (
|
||||||
|
fontFamily: ExcalidrawTextElement["fontFamily"],
|
||||||
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
lineHeightPx: number,
|
||||||
|
) => {
|
||||||
|
const { unitsPerEm, ascender, descender } =
|
||||||
|
Fonts.registered.get(fontFamily)?.metadata.metrics ||
|
||||||
|
FONT_METADATA[FONT_FAMILY.Virgil].metrics;
|
||||||
|
|
||||||
|
const fontSizeEm = fontSize / unitsPerEm;
|
||||||
|
const lineGap =
|
||||||
|
(lineHeightPx - fontSizeEm * ascender + fontSizeEm * descender) / 2;
|
||||||
|
|
||||||
|
const verticalOffset = fontSizeEm * ascender + lineGap;
|
||||||
|
return verticalOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets line height forr a selected family.
|
||||||
|
*/
|
||||||
|
export const getLineHeight = (fontFamily: FontFamilyValues) => {
|
||||||
|
const { lineHeight } =
|
||||||
|
Fonts.registered.get(fontFamily)?.metadata.metrics ||
|
||||||
|
FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
|
||||||
|
|
||||||
|
return lineHeight as ExcalidrawTextElement["lineHeight"];
|
||||||
|
};
|
125
packages/excalidraw/fonts/metadata.ts
Normal file
125
packages/excalidraw/fonts/metadata.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import {
|
||||||
|
FontFamilyCodeIcon,
|
||||||
|
FontFamilyHeadingIcon,
|
||||||
|
FontFamilyNormalIcon,
|
||||||
|
FreedrawIcon,
|
||||||
|
} from "../components/icons";
|
||||||
|
import { FONT_FAMILY } from "../constants";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates font metrics with additional font metadata.
|
||||||
|
* */
|
||||||
|
export interface FontMetadata {
|
||||||
|
/** for head & hhea metrics read the woff2 with https://fontdrop.info/ */
|
||||||
|
metrics: {
|
||||||
|
/** head.unitsPerEm metric */
|
||||||
|
unitsPerEm: 1000 | 1024 | 2048;
|
||||||
|
/** hhea.ascender metric */
|
||||||
|
ascender: number;
|
||||||
|
/** hhea.descender metric */
|
||||||
|
descender: number;
|
||||||
|
/** harcoded unitless line-height, https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971 */
|
||||||
|
lineHeight: number;
|
||||||
|
};
|
||||||
|
/** element to be displayed as an icon */
|
||||||
|
icon: JSX.Element;
|
||||||
|
/** flag to indicate a deprecated font */
|
||||||
|
deprecated?: true;
|
||||||
|
/** flag to indicate a server-side only font */
|
||||||
|
serverSide?: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||||
|
[FONT_FAMILY.Excalifont]: {
|
||||||
|
metrics: {
|
||||||
|
unitsPerEm: 1000,
|
||||||
|
ascender: 886,
|
||||||
|
descender: -374,
|
||||||
|
lineHeight: 1.25,
|
||||||
|
},
|
||||||
|
icon: FreedrawIcon,
|
||||||
|
},
|
||||||
|
[FONT_FAMILY.Nunito]: {
|
||||||
|
metrics: {
|
||||||
|
unitsPerEm: 1000,
|
||||||
|
ascender: 1011,
|
||||||
|
descender: -353,
|
||||||
|
lineHeight: 1.35,
|
||||||
|
},
|
||||||
|
icon: FontFamilyNormalIcon,
|
||||||
|
},
|
||||||
|
[FONT_FAMILY["Lilita One"]]: {
|
||||||
|
metrics: {
|
||||||
|
unitsPerEm: 1000,
|
||||||
|
ascender: 923,
|
||||||
|
descender: -220,
|
||||||
|
lineHeight: 1.15,
|
||||||
|
},
|
||||||
|
icon: FontFamilyHeadingIcon,
|
||||||
|
},
|
||||||
|
[FONT_FAMILY["Comic Shanns"]]: {
|
||||||
|
metrics: {
|
||||||
|
unitsPerEm: 1000,
|
||||||
|
ascender: 750,
|
||||||
|
descender: -250,
|
||||||
|
lineHeight: 1.25,
|
||||||
|
},
|
||||||
|
icon: FontFamilyCodeIcon,
|
||||||
|
},
|
||||||
|
[FONT_FAMILY.Virgil]: {
|
||||||
|
metrics: {
|
||||||
|
unitsPerEm: 1000,
|
||||||
|
ascender: 886,
|
||||||
|
descender: -374,
|
||||||
|
lineHeight: 1.25,
|
||||||
|
},
|
||||||
|
icon: FreedrawIcon,
|
||||||
|
deprecated: true,
|
||||||
|
},
|
||||||
|
[FONT_FAMILY.Helvetica]: {
|
||||||
|
metrics: {
|
||||||
|
unitsPerEm: 2048,
|
||||||
|
ascender: 1577,
|
||||||
|
descender: -471,
|
||||||
|
lineHeight: 1.15,
|
||||||
|
},
|
||||||
|
icon: FontFamilyNormalIcon,
|
||||||
|
deprecated: true,
|
||||||
|
},
|
||||||
|
[FONT_FAMILY.Cascadia]: {
|
||||||
|
metrics: {
|
||||||
|
unitsPerEm: 2048,
|
||||||
|
ascender: 1900,
|
||||||
|
descender: -480,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
},
|
||||||
|
icon: FontFamilyCodeIcon,
|
||||||
|
deprecated: true,
|
||||||
|
},
|
||||||
|
[FONT_FAMILY["Liberation Sans"]]: {
|
||||||
|
metrics: {
|
||||||
|
unitsPerEm: 2048,
|
||||||
|
ascender: 1854,
|
||||||
|
descender: -434,
|
||||||
|
lineHeight: 1.15,
|
||||||
|
},
|
||||||
|
icon: FontFamilyNormalIcon,
|
||||||
|
serverSide: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Unicode ranges */
|
||||||
|
export const RANGES = {
|
||||||
|
LATIN:
|
||||||
|
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||||
|
LATIN_EXT:
|
||||||
|
"U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF",
|
||||||
|
CYRILIC_EXT:
|
||||||
|
"U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F",
|
||||||
|
CYRILIC: "U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116",
|
||||||
|
VIETNAMESE:
|
||||||
|
"U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** local protocol to skip the local font from registering or inlining */
|
||||||
|
export const LOCAL_FONT_PROTOCOL = "local:";
|
|
@ -5,7 +5,7 @@ import { isShallowEqual } from "./utils";
|
||||||
|
|
||||||
import "./css/app.scss";
|
import "./css/app.scss";
|
||||||
import "./css/styles.scss";
|
import "./css/styles.scss";
|
||||||
import "../../public/fonts/fonts.css";
|
import "./fonts/assets/fonts.css";
|
||||||
import polyfill from "./polyfill";
|
import polyfill from "./polyfill";
|
||||||
|
|
||||||
import type { AppProps, ExcalidrawProps } from "./types";
|
import type { AppProps, ExcalidrawProps } from "./types";
|
||||||
|
@ -50,6 +50,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
validateEmbeddable,
|
validateEmbeddable,
|
||||||
renderEmbeddable,
|
renderEmbeddable,
|
||||||
aiEnabled,
|
aiEnabled,
|
||||||
|
showDeprecatedFonts,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const canvasActions = props.UIOptions?.canvasActions;
|
const canvasActions = props.UIOptions?.canvasActions;
|
||||||
|
@ -137,6 +138,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
validateEmbeddable={validateEmbeddable}
|
validateEmbeddable={validateEmbeddable}
|
||||||
renderEmbeddable={renderEmbeddable}
|
renderEmbeddable={renderEmbeddable}
|
||||||
aiEnabled={aiEnabled !== false}
|
aiEnabled={aiEnabled !== false}
|
||||||
|
showDeprecatedFonts={showDeprecatedFonts}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</App>
|
</App>
|
||||||
|
|
|
@ -109,6 +109,7 @@
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"showStroke": "Show stroke color picker",
|
"showStroke": "Show stroke color picker",
|
||||||
"showBackground": "Show background color picker",
|
"showBackground": "Show background color picker",
|
||||||
|
"showFonts": "Show font picker",
|
||||||
"toggleTheme": "Toggle light/dark theme",
|
"toggleTheme": "Toggle light/dark theme",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"personalLib": "Personal Library",
|
"personalLib": "Personal Library",
|
||||||
|
@ -557,11 +558,19 @@
|
||||||
"syntax": "Mermaid Syntax",
|
"syntax": "Mermaid Syntax",
|
||||||
"preview": "Preview"
|
"preview": "Preview"
|
||||||
},
|
},
|
||||||
"userList": {
|
"quickSearch": {
|
||||||
"search": {
|
"placeholder": "Quick search"
|
||||||
"placeholder": "Quick search",
|
},
|
||||||
"empty": "No users found"
|
"fontList": {
|
||||||
|
"badge": {
|
||||||
|
"old": "old"
|
||||||
},
|
},
|
||||||
|
"sceneFonts": "In this scene",
|
||||||
|
"availableFonts": "Available fonts",
|
||||||
|
"empty": "No fonts found"
|
||||||
|
},
|
||||||
|
"userList": {
|
||||||
|
"empty": "No users found",
|
||||||
"hint": {
|
"hint": {
|
||||||
"text": "Click on user to follow",
|
"text": "Click on user to follow",
|
||||||
"followStatus": "You're currently following this user",
|
"followStatus": "You're currently following this user",
|
||||||
|
|
|
@ -53,12 +53,12 @@ import {
|
||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
getBoundTextMaxHeight,
|
getBoundTextMaxHeight,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
getVerticalOffset,
|
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
import { getContainingFrame } from "../frame";
|
import { getContainingFrame } from "../frame";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
import { getVerticalOffset } from "../fonts";
|
||||||
|
|
||||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
|
@ -89,8 +89,16 @@ const shouldResetImageFilter = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCanvasPadding = (element: ExcalidrawElement) =>
|
const getCanvasPadding = (element: ExcalidrawElement) => {
|
||||||
element.type === "freedraw" ? element.strokeWidth * 12 : 20;
|
switch (element.type) {
|
||||||
|
case "freedraw":
|
||||||
|
return element.strokeWidth * 12;
|
||||||
|
case "text":
|
||||||
|
return element.fontSize / 2;
|
||||||
|
default:
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getRenderOpacity = (
|
export const getRenderOpacity = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
|
@ -202,7 +210,7 @@ const generateElementCanvas = (
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
|
|
||||||
let canvasOffsetX = 0;
|
let canvasOffsetX = -100;
|
||||||
let canvasOffsetY = 0;
|
let canvasOffsetY = 0;
|
||||||
|
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getLineHeightInPx,
|
getLineHeightInPx,
|
||||||
getVerticalOffset,
|
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
|
@ -37,6 +36,7 @@ import type { RenderableElementsMap, SVGRenderConfig } from "../scene/types";
|
||||||
import type { AppState, BinaryFiles } from "../types";
|
import type { AppState, BinaryFiles } from "../types";
|
||||||
import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
||||||
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
||||||
|
import { getVerticalOffset } from "../fonts";
|
||||||
|
|
||||||
const roughSVGDrawWithPrecision = (
|
const roughSVGDrawWithPrecision = (
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
import { isTextElement } from "../element";
|
|
||||||
import { getContainerElement } from "../element/textElement";
|
|
||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawTextElement,
|
|
||||||
} from "../element/types";
|
|
||||||
import { getFontString } from "../utils";
|
|
||||||
import type Scene from "./Scene";
|
|
||||||
import { ShapeCache } from "./ShapeCache";
|
|
||||||
|
|
||||||
export class Fonts {
|
|
||||||
private scene: Scene;
|
|
||||||
|
|
||||||
constructor({ scene }: { scene: Scene }) {
|
|
||||||
this.scene = scene;
|
|
||||||
}
|
|
||||||
|
|
||||||
// it's ok to track fonts across multiple instances only once, so let's use
|
|
||||||
// a static member to reduce memory footprint
|
|
||||||
private static loadedFontFaces = new Set<string>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if we load a (new) font, it's likely that text elements using it have
|
|
||||||
* already been rendered using a fallback font. Thus, we want invalidate
|
|
||||||
* their shapes and rerender. See #637.
|
|
||||||
*
|
|
||||||
* Invalidates text elements and rerenders scene, provided that at least one
|
|
||||||
* of the supplied fontFaces has not already been processed.
|
|
||||||
*/
|
|
||||||
public onFontsLoaded = (fontFaces: readonly FontFace[]) => {
|
|
||||||
if (
|
|
||||||
// bail if all fonts with have been processed. We're checking just a
|
|
||||||
// subset of the font properties (though it should be enough), so it
|
|
||||||
// can technically bail on a false positive.
|
|
||||||
fontFaces.every((fontFace) => {
|
|
||||||
const sig = `${fontFace.family}-${fontFace.style}-${fontFace.weight}`;
|
|
||||||
if (Fonts.loadedFontFaces.has(sig)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
Fonts.loadedFontFaces.add(sig);
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let didUpdate = false;
|
|
||||||
|
|
||||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
|
||||||
|
|
||||||
for (const element of this.scene.getNonDeletedElements()) {
|
|
||||||
if (isTextElement(element)) {
|
|
||||||
didUpdate = true;
|
|
||||||
ShapeCache.delete(element);
|
|
||||||
const container = getContainerElement(element, elementsMap);
|
|
||||||
if (container) {
|
|
||||||
ShapeCache.delete(container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (didUpdate) {
|
|
||||||
this.scene.triggerUpdate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public loadFontsForElements = async (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
) => {
|
|
||||||
const fontFaces = await Promise.all(
|
|
||||||
[
|
|
||||||
...new Set(
|
|
||||||
elements
|
|
||||||
.filter((element) => isTextElement(element))
|
|
||||||
.map((element) => (element as ExcalidrawTextElement).fontFamily),
|
|
||||||
),
|
|
||||||
].map((fontFamily) => {
|
|
||||||
const fontString = getFontString({
|
|
||||||
fontFamily,
|
|
||||||
fontSize: 16,
|
|
||||||
});
|
|
||||||
if (!document.fonts?.check?.(fontString)) {
|
|
||||||
return document.fonts?.load?.(fontString);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.onFontsLoaded(fontFaces.flat().filter(Boolean) as FontFace[]);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -13,8 +13,8 @@ import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
|
||||||
import type { AppState, BinaryFiles } from "../types";
|
import type { AppState, BinaryFiles } from "../types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_EXPORT_PADDING,
|
DEFAULT_EXPORT_PADDING,
|
||||||
FONT_FAMILY,
|
|
||||||
FRAME_STYLE,
|
FRAME_STYLE,
|
||||||
|
FONT_FAMILY,
|
||||||
SVG_NS,
|
SVG_NS,
|
||||||
THEME,
|
THEME,
|
||||||
THEME_FILTER,
|
THEME_FILTER,
|
||||||
|
@ -32,12 +32,18 @@ import {
|
||||||
getRootElements,
|
getRootElements,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
import { newTextElement } from "../element";
|
import { newTextElement } from "../element";
|
||||||
import type { Mutable } from "../utility-types";
|
import { type Mutable } from "../utility-types";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { isFrameElement, isFrameLikeElement } from "../element/typeChecks";
|
import {
|
||||||
|
isFrameElement,
|
||||||
|
isFrameLikeElement,
|
||||||
|
isTextElement,
|
||||||
|
} from "../element/typeChecks";
|
||||||
import type { RenderableElementsMap } from "./types";
|
import type { RenderableElementsMap } from "./types";
|
||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { renderStaticScene } from "../renderer/staticScene";
|
import { renderStaticScene } from "../renderer/staticScene";
|
||||||
|
import { Fonts } from "../fonts";
|
||||||
|
import { LOCAL_FONT_PROTOCOL } from "../fonts/metadata";
|
||||||
|
|
||||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
|
@ -95,7 +101,7 @@ const addFrameLabelsAsTextElements = (
|
||||||
let textElement: Mutable<ExcalidrawTextElement> = newTextElement({
|
let textElement: Mutable<ExcalidrawTextElement> = newTextElement({
|
||||||
x: element.x,
|
x: element.x,
|
||||||
y: element.y - FRAME_STYLE.nameOffsetY,
|
y: element.y - FRAME_STYLE.nameOffsetY,
|
||||||
fontFamily: FONT_FAMILY.Assistant,
|
fontFamily: FONT_FAMILY.Helvetica,
|
||||||
fontSize: FRAME_STYLE.nameFontSize,
|
fontSize: FRAME_STYLE.nameFontSize,
|
||||||
lineHeight:
|
lineHeight:
|
||||||
FRAME_STYLE.nameLineHeight as ExcalidrawTextElement["lineHeight"],
|
FRAME_STYLE.nameLineHeight as ExcalidrawTextElement["lineHeight"],
|
||||||
|
@ -269,6 +275,7 @@ export const exportToSvg = async (
|
||||||
*/
|
*/
|
||||||
renderEmbeddables?: boolean;
|
renderEmbeddables?: boolean;
|
||||||
exportingFrame?: ExcalidrawFrameLikeElement | null;
|
exportingFrame?: ExcalidrawFrameLikeElement | null;
|
||||||
|
skipInliningFonts?: true;
|
||||||
},
|
},
|
||||||
): Promise<SVGSVGElement> => {
|
): Promise<SVGSVGElement> => {
|
||||||
const frameRendering = getFrameRenderingConfig(
|
const frameRendering = getFrameRenderingConfig(
|
||||||
|
@ -333,21 +340,6 @@ export const exportToSvg = async (
|
||||||
svgRoot.setAttribute("filter", THEME_FILTER);
|
svgRoot.setAttribute("filter", THEME_FILTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
let assetPath = "https://excalidraw.com/";
|
|
||||||
// Asset path needs to be determined only when using package
|
|
||||||
if (import.meta.env.VITE_IS_EXCALIDRAW_NPM_PACKAGE) {
|
|
||||||
assetPath =
|
|
||||||
window.EXCALIDRAW_ASSET_PATH ||
|
|
||||||
`https://unpkg.com/${import.meta.env.VITE_PKG_NAME}@${
|
|
||||||
import.meta.env.VITE_PKG_VERSION
|
|
||||||
}`;
|
|
||||||
|
|
||||||
if (assetPath?.startsWith("/")) {
|
|
||||||
assetPath = assetPath.replace("/", `${window.location.origin}/`);
|
|
||||||
}
|
|
||||||
assetPath = `${assetPath}/dist/excalidraw-assets/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const offsetX = -minX + exportPadding;
|
const offsetX = -minX + exportPadding;
|
||||||
const offsetY = -minY + exportPadding;
|
const offsetY = -minY + exportPadding;
|
||||||
|
|
||||||
|
@ -371,23 +363,57 @@ export const exportToSvg = async (
|
||||||
</clipPath>`;
|
</clipPath>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fontFamilies = elements.reduce((acc, element) => {
|
||||||
|
if (isTextElement(element)) {
|
||||||
|
acc.add(element.fontFamily);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, new Set<number>());
|
||||||
|
|
||||||
|
const fontFaces = opts?.skipInliningFonts
|
||||||
|
? []
|
||||||
|
: await Promise.all(
|
||||||
|
Array.from(fontFamilies).map(async (x) => {
|
||||||
|
const { fontFaces } = Fonts.registered.get(x) ?? {};
|
||||||
|
|
||||||
|
if (!Array.isArray(fontFaces)) {
|
||||||
|
console.error(
|
||||||
|
`Couldn't find registered font-faces for font-family "${x}"`,
|
||||||
|
Fonts.registered,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
fontFaces
|
||||||
|
.filter((font) => font.url.protocol !== LOCAL_FONT_PROTOCOL)
|
||||||
|
.map(async (font) => {
|
||||||
|
try {
|
||||||
|
const content = await font.getContent();
|
||||||
|
|
||||||
|
return `@font-face {
|
||||||
|
font-family: ${font.fontFace.family};
|
||||||
|
src: url(${content});
|
||||||
|
}`;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Skipped inlining font with URL "${font.url.toString()}"`,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
svgRoot.innerHTML = `
|
svgRoot.innerHTML = `
|
||||||
${SVG_EXPORT_TAG}
|
${SVG_EXPORT_TAG}
|
||||||
${metadata}
|
${metadata}
|
||||||
<defs>
|
<defs>
|
||||||
<style class="style-fonts">
|
<style class="style-fonts">
|
||||||
@font-face {
|
${fontFaces.flat().filter(Boolean).join("\n")}
|
||||||
font-family: "Virgil";
|
|
||||||
src: url("${assetPath}Virgil.woff2");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Cascadia";
|
|
||||||
src: url("${assetPath}Cascadia.woff2");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Assistant";
|
|
||||||
src: url("${assetPath}Assistant-Regular.woff2");
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
${exportingFrameClipPath}
|
${exportingFrameClipPath}
|
||||||
</defs>
|
</defs>
|
||||||
|
|
|
@ -795,10 +795,11 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||||
"top": 40,
|
"top": 40,
|
||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -996,10 +997,11 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -1207,10 +1209,11 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -1533,10 +1536,11 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -1859,10 +1863,11 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2070,10 +2075,11 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2305,10 +2311,11 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2601,10 +2608,11 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2965,10 +2973,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "#a5d8ff",
|
"currentItemBackgroundColor": "#a5d8ff",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "cross-hatch",
|
"currentItemFillStyle": "cross-hatch",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 60,
|
"currentItemOpacity": 60,
|
||||||
"currentItemRoughness": 2,
|
"currentItemRoughness": 2,
|
||||||
|
@ -3435,10 +3444,11 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -3753,10 +3763,11 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -4071,10 +4082,11 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -5252,10 +5264,11 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||||
"top": -7,
|
"top": -7,
|
||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -6374,10 +6387,11 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||||
"top": -7,
|
"top": -7,
|
||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -7304,10 +7318,11 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||||
"top": -9,
|
"top": -9,
|
||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -8211,10 +8226,11 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||||
"top": -7,
|
"top": -7,
|
||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -9100,10 +9116,11 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||||
"top": 90,
|
"top": 90,
|
||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
|
|
@ -11,11 +11,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__icon"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu-item__text"
|
class="dropdown-menu-item__text"
|
||||||
>
|
>
|
||||||
|
@ -28,9 +24,6 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
class="dropdown-menu-item__icon"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu-item__text"
|
class="dropdown-menu-item__text"
|
||||||
>
|
>
|
||||||
|
@ -51,7 +44,6 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
data-testid="help-menu-item"
|
data-testid="help-menu-item"
|
||||||
title="Help"
|
title="Help"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu-item__icon"
|
class="dropdown-menu-item__icon"
|
||||||
|
@ -122,7 +114,6 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
data-testid="load-button"
|
data-testid="load-button"
|
||||||
title="Open"
|
title="Open"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu-item__icon"
|
class="dropdown-menu-item__icon"
|
||||||
|
@ -160,7 +151,6 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
data-testid="json-export-button"
|
data-testid="json-export-button"
|
||||||
title="Save to..."
|
title="Save to..."
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu-item__icon"
|
class="dropdown-menu-item__icon"
|
||||||
|
@ -193,7 +183,6 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
data-testid="image-export-button"
|
data-testid="image-export-button"
|
||||||
title="Export image..."
|
title="Export image..."
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu-item__icon"
|
class="dropdown-menu-item__icon"
|
||||||
|
@ -255,7 +244,6 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
data-testid="help-menu-item"
|
data-testid="help-menu-item"
|
||||||
title="Help"
|
title="Help"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu-item__icon"
|
class="dropdown-menu-item__icon"
|
||||||
|
@ -313,7 +301,6 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
data-testid="clear-canvas-button"
|
data-testid="clear-canvas-button"
|
||||||
title="Reset the canvas"
|
title="Reset the canvas"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu-item__icon"
|
class="dropdown-menu-item__icon"
|
||||||
|
@ -481,7 +468,6 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
data-testid="toggle-dark-mode"
|
data-testid="toggle-dark-mode"
|
||||||
title="Dark mode"
|
title="Dark mode"
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="dropdown-menu-item__icon"
|
class="dropdown-menu-item__icon"
|
||||||
|
@ -593,14 +579,14 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style="width: 1px; height: 100%; margin: 0px auto;"
|
style="width: 1px; height: 1rem; margin: 0px auto;"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
aria-controls="radix-:r0:"
|
aria-controls="radix-:r0:"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
aria-label="Canvas background"
|
aria-label="Canvas background"
|
||||||
class="color-picker__button active-color"
|
class="color-picker__button active-color properties-trigger"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
style="--swatch-color: #ffffff;"
|
style="--swatch-color: #ffffff;"
|
||||||
title="Show background color picker"
|
title="Show background color picker"
|
||||||
|
|
|
@ -6,18 +6,7 @@ exports[`export > exporting svg containing transformed images > svg export outpu
|
||||||
|
|
||||||
<defs>
|
<defs>
|
||||||
<style class="style-fonts">
|
<style class="style-fonts">
|
||||||
@font-face {
|
|
||||||
font-family: "Virgil";
|
|
||||||
src: url("https://excalidraw.com/Virgil.woff2");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Cascadia";
|
|
||||||
src: url("https://excalidraw.com/Cascadia.woff2");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Assistant";
|
|
||||||
src: url("https://excalidraw.com/Assistant-Regular.woff2");
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</defs>
|
</defs>
|
||||||
|
|
|
@ -12,10 +12,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -593,10 +594,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -1090,10 +1092,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -1436,10 +1439,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -1782,10 +1786,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2044,10 +2049,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2473,10 +2479,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2611,7 +2618,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2652,7 +2659,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": "id142",
|
"containerId": "id142",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2767,10 +2774,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2905,7 +2913,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": "id145",
|
"containerId": "id145",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -2946,7 +2954,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": "id145",
|
"containerId": "id145",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -3046,10 +3054,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -3184,7 +3193,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": "id132",
|
"containerId": "id132",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -3225,7 +3234,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -3335,10 +3344,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -3505,7 +3515,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": "id137",
|
"containerId": "id137",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -3616,10 +3626,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -3749,7 +3760,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -3846,10 +3857,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -3984,7 +3996,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": "id138",
|
"containerId": "id138",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -4100,10 +4112,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -4238,7 +4251,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": "id140",
|
"containerId": "id140",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -4299,7 +4312,7 @@ History {
|
||||||
"containerId": "id140",
|
"containerId": "id140",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -4368,10 +4381,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -4506,7 +4520,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": "id154",
|
"containerId": "id154",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -4594,10 +4608,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -4732,7 +4747,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": "id152",
|
"containerId": "id152",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -4820,10 +4835,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -4953,7 +4969,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": "id148",
|
"containerId": "id148",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -5044,10 +5060,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -5182,7 +5199,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -5268,10 +5285,11 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -5522,10 +5540,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -5848,10 +5867,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "#ffc9c9",
|
"currentItemBackgroundColor": "#ffc9c9",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -6268,10 +6288,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -6641,10 +6662,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -6955,10 +6977,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -7246,10 +7269,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "#ffc9c9",
|
"currentItemBackgroundColor": "#ffc9c9",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -7470,10 +7494,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -7820,10 +7845,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -8170,10 +8196,11 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -8569,10 +8596,11 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -8851,10 +8879,11 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -9111,10 +9140,11 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "#ffc9c9",
|
"currentItemBackgroundColor": "#ffc9c9",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -9370,10 +9400,11 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "#ffc9c9",
|
"currentItemBackgroundColor": "#ffc9c9",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -9596,10 +9627,11 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -9892,10 +9924,11 @@ exports[`history > multiplayer undo/redo > should override remotely added points
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -10225,10 +10258,11 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -10455,10 +10489,11 @@ exports[`history > multiplayer undo/redo > should update history entries after r
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "#a5d8ff",
|
"currentItemBackgroundColor": "#a5d8ff",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -10704,10 +10739,11 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -10938,10 +10974,11 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -11174,10 +11211,11 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -11570,10 +11608,11 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -11812,10 +11851,11 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -12048,10 +12088,11 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -12284,10 +12325,11 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -12526,10 +12568,11 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -12853,10 +12896,11 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -13020,10 +13064,11 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -13303,10 +13348,11 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -13565,10 +13611,11 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "#a5d8ff",
|
"currentItemBackgroundColor": "#a5d8ff",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -13835,10 +13882,11 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -13991,10 +14039,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -14134,7 +14183,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"containerId": "id50",
|
"containerId": "id50",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -14394,7 +14443,7 @@ History {
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -14676,10 +14725,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -14819,7 +14869,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"containerId": "id44",
|
"containerId": "id44",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -15003,7 +15053,7 @@ History {
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -15285,10 +15335,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -15428,7 +15479,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"containerId": "id56",
|
"containerId": "id56",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -15612,7 +15663,7 @@ History {
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -15894,10 +15945,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -16035,7 +16087,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"containerId": "id62",
|
"containerId": "id62",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -16289,7 +16341,7 @@ History {
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -16594,10 +16646,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -16738,7 +16791,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||||
"containerId": "id69",
|
"containerId": "id69",
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -17007,7 +17060,7 @@ History {
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 20,
|
"fontSize": 20,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
@ -17331,10 +17384,11 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -17800,10 +17854,11 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -18317,10 +18372,11 @@ exports[`history > singleplayer undo/redo > should support element creation, del
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -18768,10 +18824,11 @@ exports[`history > singleplayer undo/redo > should support linear element creati
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
|
|
@ -5,7 +5,7 @@ exports[`Test Linear Elements > Test bound text element > should match styles fo
|
||||||
class="excalidraw-wysiwyg"
|
class="excalidraw-wysiwyg"
|
||||||
data-type="wysiwyg"
|
data-type="wysiwyg"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; 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: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
|
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 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: 26.25px; left: 25px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); padding: 0px 10px; text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Excalifont, Segoe UI Emoji;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
wrap="off"
|
wrap="off"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -12,10 +12,11 @@ exports[`given element A and group of elements B and given both are selected whe
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -419,10 +420,11 @@ exports[`given element A and group of elements B and given both are selected whe
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -817,10 +819,11 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -1354,10 +1357,11 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -1550,10 +1554,11 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -1917,10 +1922,11 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2149,10 +2155,11 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2321,10 +2328,11 @@ exports[`regression tests > can drag element that covers another element, while
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2633,10 +2641,11 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "#ffc9c9",
|
"currentItemBackgroundColor": "#ffc9c9",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -2871,10 +2880,11 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -3106,10 +3116,11 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -3328,10 +3339,11 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -3576,10 +3588,11 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -3879,10 +3892,11 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -4285,10 +4299,11 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -4590,10 +4605,11 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -4865,10 +4881,11 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -5097,10 +5114,11 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -5288,10 +5306,11 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -5662,10 +5681,11 @@ exports[`regression tests > drags selected elements from point inside common bou
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -5944,10 +5964,11 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -6742,10 +6763,11 @@ exports[`regression tests > given a group of selected elements with an element t
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -7064,10 +7086,11 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "#ffc9c9",
|
"currentItemBackgroundColor": "#ffc9c9",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -7332,10 +7355,11 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -7558,10 +7582,11 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -7787,10 +7812,11 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -7959,10 +7985,11 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -8131,10 +8158,11 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -8303,10 +8331,11 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -8515,10 +8544,11 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -8727,10 +8757,11 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -8913,10 +8944,11 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -9125,10 +9157,11 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -9297,10 +9330,11 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -9509,10 +9543,11 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -9681,10 +9716,11 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -9867,10 +9903,11 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -10039,10 +10076,11 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -10545,10 +10583,11 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -10814,10 +10853,11 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -10932,10 +10972,11 @@ exports[`regression tests > shift click on selected element should deselect it o
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -11123,10 +11164,11 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -11426,10 +11468,11 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -11830,10 +11873,11 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -12435,10 +12479,11 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -12556,10 +12601,11 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -13132,10 +13178,11 @@ exports[`regression tests > switches from group of selected elements to another
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -13492,10 +13539,11 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -13779,10 +13827,11 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -13897,10 +13946,11 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -14267,10 +14317,11 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 3,
|
"currentItemFontFamily": 8,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
@ -14385,10 +14436,11 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
||||||
"collaborators": Map {},
|
"collaborators": Map {},
|
||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
|
"currentHoveredFontFamily": null,
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
"currentItemFontFamily": 1,
|
"currentItemFontFamily": 5,
|
||||||
"currentItemFontSize": 20,
|
"currentItemFontSize": 20,
|
||||||
"currentItemOpacity": 100,
|
"currentItemOpacity": 100,
|
||||||
"currentItemRoughness": 1,
|
"currentItemRoughness": 1,
|
||||||
|
|
|
@ -4,16 +4,14 @@ import { render, waitFor, GlobalTestState } from "./test-utils";
|
||||||
import { Pointer, Keyboard } from "./helpers/ui";
|
import { Pointer, Keyboard } from "./helpers/ui";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import {
|
import { getLineHeightInPx } from "../element/textElement";
|
||||||
getDefaultLineHeight,
|
|
||||||
getLineHeightInPx,
|
|
||||||
} from "../element/textElement";
|
|
||||||
import { getElementBounds } from "../element";
|
import { getElementBounds } from "../element";
|
||||||
import type { NormalizedZoomValue } from "../types";
|
import type { NormalizedZoomValue } from "../types";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
|
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { mockMermaidToExcalidraw } from "./helpers/mocks";
|
import { mockMermaidToExcalidraw } from "./helpers/mocks";
|
||||||
|
import { getLineHeight } from "../fonts";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
|
@ -146,7 +144,7 @@ describe("paste text as single lines", () => {
|
||||||
const lineHeightPx =
|
const lineHeightPx =
|
||||||
getLineHeightInPx(
|
getLineHeightInPx(
|
||||||
h.app.state.currentItemFontSize,
|
h.app.state.currentItemFontSize,
|
||||||
getDefaultLineHeight(h.state.currentItemFontFamily),
|
getLineHeight(h.state.currentItemFontFamily),
|
||||||
) +
|
) +
|
||||||
10 / h.app.state.zoom.value;
|
10 / h.app.state.zoom.value;
|
||||||
mouse.moveTo(100, 100);
|
mouse.moveTo(100, 100);
|
||||||
|
@ -168,7 +166,7 @@ describe("paste text as single lines", () => {
|
||||||
const lineHeightPx =
|
const lineHeightPx =
|
||||||
getLineHeightInPx(
|
getLineHeightInPx(
|
||||||
h.app.state.currentItemFontSize,
|
h.app.state.currentItemFontSize,
|
||||||
getDefaultLineHeight(h.state.currentItemFontFamily),
|
getLineHeight(h.state.currentItemFontFamily),
|
||||||
) +
|
) +
|
||||||
10 / h.app.state.zoom.value;
|
10 / h.app.state.zoom.value;
|
||||||
mouse.moveTo(100, 100);
|
mouse.moveTo(100, 100);
|
||||||
|
|
|
@ -351,7 +351,7 @@ exports[`restoreElements > should restore text element correctly with unknown fo
|
||||||
"containerId": null,
|
"containerId": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"fontFamily": 1,
|
"fontFamily": 5,
|
||||||
"fontSize": 10,
|
"fontSize": 10,
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DEFAULT_FONT_FAMILY } from "../../constants";
|
||||||
import type { ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
|
|
||||||
const elementBase: Omit<ExcalidrawElement, "type"> = {
|
const elementBase: Omit<ExcalidrawElement, "type"> = {
|
||||||
|
@ -49,3 +50,17 @@ export const rectangleWithLinkFixture: ExcalidrawElement = {
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
link: "excalidraw.com",
|
link: "excalidraw.com",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const textFixture: ExcalidrawElement = {
|
||||||
|
...elementBase,
|
||||||
|
type: "text",
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: DEFAULT_FONT_FAMILY,
|
||||||
|
text: "original text",
|
||||||
|
originalText: "original text",
|
||||||
|
textAlign: "left",
|
||||||
|
verticalAlign: "top",
|
||||||
|
containerId: null,
|
||||||
|
lineHeight: 1.25 as any,
|
||||||
|
autoResize: false,
|
||||||
|
};
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { URL } from "node:url";
|
||||||
|
|
||||||
class ClipboardEvent {
|
class ClipboardEvent {
|
||||||
constructor(
|
constructor(
|
||||||
type: "paste" | "copy",
|
type: "paste" | "copy",
|
||||||
|
@ -88,4 +90,6 @@ export const testPolyfills = {
|
||||||
ClipboardEvent,
|
ClipboardEvent,
|
||||||
DataTransfer,
|
DataTransfer,
|
||||||
DataTransferItem,
|
DataTransferItem,
|
||||||
|
// https://github.com/vitest-dev/vitest/pull/4164#issuecomment-2172729965
|
||||||
|
URL,
|
||||||
};
|
};
|
||||||
|
|
|
@ -644,9 +644,9 @@ describe("regression tests", () => {
|
||||||
|
|
||||||
it("updates fontSize & fontFamily appState", () => {
|
it("updates fontSize & fontFamily appState", () => {
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY.Virgil);
|
expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY.Excalifont);
|
||||||
fireEvent.click(screen.getByTitle(/code/i));
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY.Cascadia);
|
expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY["Comic Shanns"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => {
|
it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -4,14 +4,14 @@ import {
|
||||||
diamondFixture,
|
diamondFixture,
|
||||||
ellipseFixture,
|
ellipseFixture,
|
||||||
rectangleWithLinkFixture,
|
rectangleWithLinkFixture,
|
||||||
|
textFixture,
|
||||||
} from "../fixtures/elementFixture";
|
} from "../fixtures/elementFixture";
|
||||||
import { API } from "../helpers/api";
|
import { API } from "../helpers/api";
|
||||||
import { exportToCanvas, exportToSvg } from "../../../utils";
|
import { exportToCanvas, exportToSvg } from "../../../utils";
|
||||||
import { FRAME_STYLE } from "../../constants";
|
import { FONT_FAMILY, FRAME_STYLE } from "../../constants";
|
||||||
import { prepareElementsForExport } from "../../data";
|
import { prepareElementsForExport } from "../../data";
|
||||||
|
|
||||||
describe("exportToSvg", () => {
|
describe("exportToSvg", () => {
|
||||||
window.EXCALIDRAW_ASSET_PATH = "/";
|
|
||||||
const ELEMENT_HEIGHT = 100;
|
const ELEMENT_HEIGHT = 100;
|
||||||
const ELEMENT_WIDTH = 100;
|
const ELEMENT_WIDTH = 100;
|
||||||
const ELEMENTS = [
|
const ELEMENTS = [
|
||||||
|
@ -27,6 +27,19 @@ describe("exportToSvg", () => {
|
||||||
width: ELEMENT_WIDTH,
|
width: ELEMENT_WIDTH,
|
||||||
index: "a1",
|
index: "a1",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
...textFixture,
|
||||||
|
height: ELEMENT_HEIGHT,
|
||||||
|
width: ELEMENT_WIDTH,
|
||||||
|
index: "a2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...textFixture,
|
||||||
|
fontFamily: FONT_FAMILY.Nunito, // test embedding external font
|
||||||
|
height: ELEMENT_HEIGHT,
|
||||||
|
width: ELEMENT_WIDTH,
|
||||||
|
index: "a3",
|
||||||
|
},
|
||||||
] as NonDeletedExcalidrawElement[];
|
] as NonDeletedExcalidrawElement[];
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
const DEFAULT_OPTIONS = {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue