Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4c3744dc4 | |||
| 7b36de0476 | |||
| 2427e622b0 | |||
| 62228e0bbb | |||
| 4c5408263c | |||
| bd7b778f41 | |||
| 43b2476dfe | |||
| df8875a497 | |||
| 6fbc44fd1f | |||
| d25a7d365b | |||
| e52c2cd0b6 | |||
| 96eeec5119 | |||
| f5221d521b | |||
| db2c235cd4 | |||
| 148b895f46 | |||
| d9258a736b | |||
| 2e1f08c796 | |||
| 1d5b41dabb | |||
| 66a2f24296 | |||
| 04668d8263 | |||
| abbeed3d5f | |||
| ba8c09d529 | |||
| 744b3e5d09 | |||
| 6ba9bd60e8 | |||
| a1ffa064df | |||
| 4dc4590f24 | |||
| d2f67e619f | |||
| 22b39277f5 | |||
| 63dee03ef0 | |||
| 08b13f971d | |||
| 69f4cc70cb | |||
| 860308eb27 | |||
| 4eb9463f26 | |||
| 6ed6131169 | |||
| 1ed98f9c93 | |||
| a71bb63d1f | |||
| 661d6a4a75 | |||
| defd34923a | |||
| c540bd68aa | |||
| eddbe55f50 | |||
| 2f9526da24 | |||
| 1b6e3fe05b | |||
| afe52c89a7 | |||
| be4e127f6c | |||
| ff0b4394b1 | |||
| 7d8b7fc14d | |||
| 971b4d4ae6 | |||
| cc4c51996c | |||
| 79257a1923 | |||
| dc66261c19 | |||
| 273ba803d9 | |||
| 301e83805d | |||
| ed5ce8d3de | |||
| 1ed53b153c | |||
| c1926f33bb | |||
| 6539029d2a | |||
| d1f37cc64f | |||
| f0d25e34c3 |
@@ -4,8 +4,15 @@
|
|||||||
!.eslintrc.json
|
!.eslintrc.json
|
||||||
!.npmrc
|
!.npmrc
|
||||||
!.prettierrc
|
!.prettierrc
|
||||||
|
!excalidraw-app/
|
||||||
!package.json
|
!package.json
|
||||||
!public/
|
!public/
|
||||||
!packages/
|
!packages/
|
||||||
!tsconfig.json
|
!tsconfig.json
|
||||||
!yarn.lock
|
!yarn.lock
|
||||||
|
|
||||||
|
# keep (sub)sub directories at the end to exclude from explicit included
|
||||||
|
# e.g. ./packages/excalidraw/{dist,node_modules}
|
||||||
|
**/build
|
||||||
|
**/dist
|
||||||
|
**/node_modules
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ VITE_APP_DEV_ENABLE_SW=
|
|||||||
# whether to disable live reload / HMR. Usuaully what you want to do when
|
# whether to disable live reload / HMR. Usuaully what you want to do when
|
||||||
# debugging Service Workers.
|
# debugging Service Workers.
|
||||||
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
VITE_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
VITE_APP_DISABLE_TRACKING=true
|
VITE_APP_ENABLE_TRACKING=true
|
||||||
|
|
||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -14,4 +14,4 @@ VITE_APP_WS_SERVER_URL=https://oss-collab.excalidraw.com
|
|||||||
|
|
||||||
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
VITE_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
|
||||||
|
|
||||||
VITE_APP_DISABLE_TRACKING=
|
VITE_APP_ENABLE_TRACKING=false
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
"extends": ["@excalidraw/eslint-config", "react-app"],
|
"extends": ["@excalidraw/eslint-config", "react-app"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"import/no-anonymous-default-export": "off",
|
"import/no-anonymous-default-export": "off",
|
||||||
"no-restricted-globals": "off"
|
"no-restricted-globals": "off",
|
||||||
|
"@typescript-eslint/consistent-type-imports": ["error", { "prefer": "type-imports", "disallowTypeAnnotations": false, "fixStyle": "separate-type-imports" }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on: pull_request
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Node.js 18.x
|
- name: Setup Node.js 18.x
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
- name: Install and test
|
- name: Install and test
|
||||||
|
|||||||
+7
-5
@@ -2,16 +2,18 @@ FROM node:18 AS build
|
|||||||
|
|
||||||
WORKDIR /opt/node_app
|
WORKDIR /opt/node_app
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY . .
|
||||||
RUN yarn --ignore-optional --network-timeout 600000
|
|
||||||
|
# do not ignore optional dependencies:
|
||||||
|
# Error: Cannot find module @rollup/rollup-linux-x64-gnu
|
||||||
|
RUN yarn --network-timeout 600000
|
||||||
|
|
||||||
ARG NODE_ENV=production
|
ARG NODE_ENV=production
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN yarn build:app:docker
|
RUN yarn build:app:docker
|
||||||
|
|
||||||
FROM nginx:1.21-alpine
|
FROM nginx:1.24-alpine
|
||||||
|
|
||||||
COPY --from=build /opt/node_app/build /usr/share/nginx/html
|
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
|
||||||
|
|
||||||
HEALTHCHECK CMD wget -q -O /dev/null http://localhost || exit 1
|
HEALTHCHECK CMD wget -q -O /dev/null http://localhost || exit 1
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Once the callback is triggered, you will need to store the api in state to acces
|
|||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
||||||
return <Excalidraw excalidrawAPI={{(api)=> setExcalidrawAPI(api)}} />;
|
return <Excalidraw excalidrawAPI={(api)=> setExcalidrawAPI(api)} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ function App() {
|
|||||||
<img src={canvasUrl} alt="" />
|
<img src={canvasUrl} alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: "400px" }}>
|
<div style={{ height: "400px" }}>
|
||||||
<Excalidraw ref={(api) => setExcalidrawAPI(api)}
|
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import type * as TExcalidraw from "@excalidraw/excalidraw";
|
|||||||
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
import type { ResolvablePromise } from "../utils";
|
||||||
import {
|
import {
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
ResolvablePromise,
|
|
||||||
distance2d,
|
distance2d,
|
||||||
fileOpen,
|
fileOpen,
|
||||||
withBatchedUpdates,
|
withBatchedUpdates,
|
||||||
@@ -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());
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/dist/excalidraw/types";
|
||||||
import CustomFooter from "./CustomFooter";
|
import CustomFooter from "./CustomFooter";
|
||||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
|
|||||||
@@ -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: [
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { unstable_batchedUpdates } from "react-dom";
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
import { fileOpen as _fileOpen } from "browser-fs-access";
|
import { fileOpen as _fileOpen } from "browser-fs-access";
|
||||||
import type { MIME_TYPES } from "@excalidraw/excalidraw";
|
import { MIME_TYPES } from "@excalidraw/excalidraw";
|
||||||
import { AbortError } from "../../packages/excalidraw/errors";
|
import { AbortError } from "../../packages/excalidraw/errors";
|
||||||
|
|
||||||
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-35
@@ -1,5 +1,4 @@
|
|||||||
import polyfill from "../packages/excalidraw/polyfill";
|
import polyfill from "../packages/excalidraw/polyfill";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { trackEvent } from "../packages/excalidraw/analytics";
|
import { trackEvent } from "../packages/excalidraw/analytics";
|
||||||
import { getDefaultAppState } from "../packages/excalidraw/appState";
|
import { getDefaultAppState } from "../packages/excalidraw/appState";
|
||||||
@@ -13,7 +12,7 @@ import {
|
|||||||
VERSION_TIMEOUT,
|
VERSION_TIMEOUT,
|
||||||
} from "../packages/excalidraw/constants";
|
} from "../packages/excalidraw/constants";
|
||||||
import { loadFromBlob } from "../packages/excalidraw/data/blob";
|
import { loadFromBlob } from "../packages/excalidraw/data/blob";
|
||||||
import {
|
import type {
|
||||||
FileId,
|
FileId,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
@@ -22,27 +21,27 @@ import { useCallbackRefState } from "../packages/excalidraw/hooks/useCallbackRef
|
|||||||
import { t } from "../packages/excalidraw/i18n";
|
import { t } from "../packages/excalidraw/i18n";
|
||||||
import {
|
import {
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
defaultLang,
|
|
||||||
LiveCollaborationTrigger,
|
LiveCollaborationTrigger,
|
||||||
TTDDialog,
|
TTDDialog,
|
||||||
TTDDialogTrigger,
|
TTDDialogTrigger,
|
||||||
StoreAction,
|
StoreAction,
|
||||||
reconcileElements,
|
reconcileElements,
|
||||||
|
normalizeIndices,
|
||||||
} from "../packages/excalidraw";
|
} from "../packages/excalidraw";
|
||||||
import {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
ExcalidrawImperativeAPI,
|
ExcalidrawImperativeAPI,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
ExcalidrawInitialDataState,
|
ExcalidrawInitialDataState,
|
||||||
UIAppState,
|
UIAppState,
|
||||||
} from "../packages/excalidraw/types";
|
} from "../packages/excalidraw/types";
|
||||||
|
import type { ResolvablePromise } from "../packages/excalidraw/utils";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
getVersion,
|
getVersion,
|
||||||
getFrame,
|
getFrame,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
preventUnload,
|
preventUnload,
|
||||||
ResolvablePromise,
|
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
isRunningInIframe,
|
isRunningInIframe,
|
||||||
} from "../packages/excalidraw/utils";
|
} from "../packages/excalidraw/utils";
|
||||||
@@ -52,8 +51,8 @@ import {
|
|||||||
STORAGE_KEYS,
|
STORAGE_KEYS,
|
||||||
SYNC_BROWSER_TABS_TIMEOUT,
|
SYNC_BROWSER_TABS_TIMEOUT,
|
||||||
} from "./app_constants";
|
} from "./app_constants";
|
||||||
|
import type { CollabAPI } from "./collab/Collab";
|
||||||
import Collab, {
|
import Collab, {
|
||||||
CollabAPI,
|
|
||||||
collabAPIAtom,
|
collabAPIAtom,
|
||||||
isCollaboratingAtom,
|
isCollaboratingAtom,
|
||||||
isOfflineAtom,
|
isOfflineAtom,
|
||||||
@@ -69,11 +68,8 @@ import {
|
|||||||
importUsernameFromLocalStorage,
|
importUsernameFromLocalStorage,
|
||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import CustomStats from "./CustomStats";
|
import CustomStats from "./CustomStats";
|
||||||
import {
|
import type { RestoredDataState } from "../packages/excalidraw/data/restore";
|
||||||
restore,
|
import { restore, restoreAppState } from "../packages/excalidraw/data/restore";
|
||||||
restoreAppState,
|
|
||||||
RestoredDataState,
|
|
||||||
} from "../packages/excalidraw/data/restore";
|
|
||||||
import {
|
import {
|
||||||
ExportToExcalidrawPlus,
|
ExportToExcalidrawPlus,
|
||||||
exportToExcalidrawPlus,
|
exportToExcalidrawPlus,
|
||||||
@@ -96,12 +92,12 @@ import {
|
|||||||
import { AppMainMenu } from "./components/AppMainMenu";
|
import { AppMainMenu } from "./components/AppMainMenu";
|
||||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||||
import { AppFooter } from "./components/AppFooter";
|
import { AppFooter } from "./components/AppFooter";
|
||||||
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
import { Provider, useAtom, useAtomValue } from "jotai";
|
||||||
import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
|
import { useAtomWithInitialValue } from "../packages/excalidraw/jotai";
|
||||||
import { appJotaiStore } from "./app-jotai";
|
import { appJotaiStore } from "./app-jotai";
|
||||||
|
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import { ResolutionType } from "../packages/excalidraw/utility-types";
|
import type { ResolutionType } from "../packages/excalidraw/utility-types";
|
||||||
import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog";
|
import { ShareableLinkDialog } from "../packages/excalidraw/components/ShareableLinkDialog";
|
||||||
import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
|
import { openConfirmModal } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
|
||||||
import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
|
import { OverwriteConfirmDialog } from "../packages/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
|
||||||
@@ -124,11 +120,45 @@ import {
|
|||||||
youtubeIcon,
|
youtubeIcon,
|
||||||
} from "../packages/excalidraw/components/icons";
|
} from "../packages/excalidraw/components/icons";
|
||||||
import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
|
import { appThemeAtom, useHandleAppTheme } from "./useHandleAppTheme";
|
||||||
|
import { getPreferredLanguage } from "./app-language/language-detector";
|
||||||
|
import { useAppLangCode } from "./app-language/language-state";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
window.EXCALIDRAW_THROTTLE_RENDER = true;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface BeforeInstallPromptEventChoiceResult {
|
||||||
|
outcome: "accepted" | "dismissed";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BeforeInstallPromptEvent extends Event {
|
||||||
|
prompt(): Promise<void>;
|
||||||
|
userChoice: Promise<BeforeInstallPromptEventChoiceResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WindowEventMap {
|
||||||
|
beforeinstallprompt: BeforeInstallPromptEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pwaEvent: BeforeInstallPromptEvent | null = null;
|
||||||
|
|
||||||
|
// Adding a listener outside of the component as it may (?) need to be
|
||||||
|
// subscribed early to catch the event.
|
||||||
|
//
|
||||||
|
// Also note that it will fire only if certain heuristics are met (user has
|
||||||
|
// used the app for some time, etc.)
|
||||||
|
window.addEventListener(
|
||||||
|
"beforeinstallprompt",
|
||||||
|
(event: BeforeInstallPromptEvent) => {
|
||||||
|
// prevent Chrome <= 67 from automatically showing the prompt
|
||||||
|
event.preventDefault();
|
||||||
|
// cache for later use
|
||||||
|
pwaEvent = event;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let isSelfEmbedding = false;
|
let isSelfEmbedding = false;
|
||||||
|
|
||||||
if (window.self !== window.top) {
|
if (window.self !== window.top) {
|
||||||
@@ -143,11 +173,6 @@ if (window.self !== window.top) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const languageDetector = new LanguageDetector();
|
|
||||||
languageDetector.init({
|
|
||||||
languageUtils: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
const shareableLinkConfirmDialog = {
|
const shareableLinkConfirmDialog = {
|
||||||
title: t("overwriteConfirm.modal.shareableLink.title"),
|
title: t("overwriteConfirm.modal.shareableLink.title"),
|
||||||
description: (
|
description: (
|
||||||
@@ -281,31 +306,34 @@ const initializeScene = async (opts: {
|
|||||||
key: roomLinkData.roomKey,
|
key: roomLinkData.roomKey,
|
||||||
};
|
};
|
||||||
} else if (scene) {
|
} else if (scene) {
|
||||||
|
const normalizedScene = {
|
||||||
|
...scene,
|
||||||
|
// non-collab scenes are always always normalized on init
|
||||||
|
// collab scenes are normalized only on "first-in-room" as part of collabAPI
|
||||||
|
elements: normalizeIndices(scene.elements),
|
||||||
|
};
|
||||||
|
|
||||||
return isExternalScene && jsonBackendMatch
|
return isExternalScene && jsonBackendMatch
|
||||||
? {
|
? {
|
||||||
scene,
|
scene: normalizedScene,
|
||||||
isExternalScene,
|
isExternalScene,
|
||||||
id: jsonBackendMatch[1],
|
id: jsonBackendMatch[1],
|
||||||
key: jsonBackendMatch[2],
|
key: jsonBackendMatch[2],
|
||||||
}
|
}
|
||||||
: { scene, isExternalScene: false };
|
: { scene: normalizedScene, isExternalScene: false };
|
||||||
}
|
}
|
||||||
return { scene: null, isExternalScene: false };
|
return { scene: null, isExternalScene: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
const detectedLangCode = languageDetector.detect() || defaultLang.code;
|
|
||||||
export const appLangCodeAtom = atom(
|
|
||||||
Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
|
|
||||||
);
|
|
||||||
|
|
||||||
const ExcalidrawWrapper = () => {
|
const ExcalidrawWrapper = () => {
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
|
||||||
const isCollabDisabled = isRunningInIframe();
|
const isCollabDisabled = isRunningInIframe();
|
||||||
|
|
||||||
const [appTheme, setAppTheme] = useAtom(appThemeAtom);
|
const [appTheme, setAppTheme] = useAtom(appThemeAtom);
|
||||||
const { editorTheme } = useHandleAppTheme();
|
const { editorTheme } = useHandleAppTheme();
|
||||||
|
|
||||||
|
const [langCode, setLangCode] = useAppLangCode();
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -461,11 +489,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
||||||
const localDataState = importFromLocalStorage();
|
const localDataState = importFromLocalStorage();
|
||||||
const username = importUsernameFromLocalStorage();
|
const username = importUsernameFromLocalStorage();
|
||||||
let langCode = languageDetector.detect() || defaultLang.code;
|
setLangCode(getPreferredLanguage());
|
||||||
if (Array.isArray(langCode)) {
|
|
||||||
langCode = langCode[0];
|
|
||||||
}
|
|
||||||
setLangCode(langCode);
|
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
...localDataState,
|
...localDataState,
|
||||||
storeAction: StoreAction.UPDATE,
|
storeAction: StoreAction.UPDATE,
|
||||||
@@ -566,10 +590,6 @@ const ExcalidrawWrapper = () => {
|
|||||||
};
|
};
|
||||||
}, [excalidrawAPI]);
|
}, [excalidrawAPI]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
languageDetector.cacheUserLanguage(langCode);
|
|
||||||
}, [langCode]);
|
|
||||||
|
|
||||||
const onChange = (
|
const onChange = (
|
||||||
elements: readonly OrderedExcalidrawElement[],
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
@@ -1103,6 +1123,21 @@ const ExcalidrawWrapper = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t("labels.installPWA"),
|
||||||
|
category: DEFAULT_CATEGORIES.app,
|
||||||
|
predicate: () => !!pwaEvent,
|
||||||
|
perform: () => {
|
||||||
|
if (pwaEvent) {
|
||||||
|
pwaEvent.prompt();
|
||||||
|
pwaEvent.userChoice.then(() => {
|
||||||
|
// event cannot be reused, but we'll hopefully
|
||||||
|
// grab new one as the event should be fired again
|
||||||
|
pwaEvent = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
import { DEFAULT_VERSION } from "../packages/excalidraw/constants";
|
import { DEFAULT_VERSION } from "../packages/excalidraw/constants";
|
||||||
import { t } from "../packages/excalidraw/i18n";
|
import { t } from "../packages/excalidraw/i18n";
|
||||||
import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard";
|
import { copyTextToSystemClipboard } from "../packages/excalidraw/clipboard";
|
||||||
import { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "../packages/excalidraw/element/types";
|
||||||
import { UIAppState } from "../packages/excalidraw/types";
|
import type { UIAppState } from "../packages/excalidraw/types";
|
||||||
|
|
||||||
type StorageSizes = { scene: number; total: number };
|
type StorageSizes = { scene: number; total: number };
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -1,8 +1,7 @@
|
|||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { appLangCodeAtom } from "../App";
|
import { useI18n, languages } from "../../packages/excalidraw/i18n";
|
||||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
import { appLangCodeAtom } from "./language-state";
|
||||||
import { languages } from "../../packages/excalidraw/i18n";
|
|
||||||
|
|
||||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||||
const { t, langCode } = useI18n();
|
const { t, langCode } = useI18n();
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
import { defaultLang, languages } from "../../packages/excalidraw";
|
||||||
|
|
||||||
|
export const languageDetector = new LanguageDetector();
|
||||||
|
|
||||||
|
languageDetector.init({
|
||||||
|
languageUtils: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getPreferredLanguage = () => {
|
||||||
|
const detectedLanguages = languageDetector.detect();
|
||||||
|
|
||||||
|
const detectedLanguage = Array.isArray(detectedLanguages)
|
||||||
|
? detectedLanguages[0]
|
||||||
|
: detectedLanguages;
|
||||||
|
|
||||||
|
const initialLanguage =
|
||||||
|
(detectedLanguage
|
||||||
|
? // region code may not be defined if user uses generic preferred language
|
||||||
|
// (e.g. chinese vs instead of chinese-simplified)
|
||||||
|
languages.find((lang) => lang.code.startsWith(detectedLanguage))?.code
|
||||||
|
: null) || defaultLang.code;
|
||||||
|
|
||||||
|
return initialLanguage;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { atom, useAtom } from "jotai";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { getPreferredLanguage, languageDetector } from "./language-detector";
|
||||||
|
|
||||||
|
export const appLangCodeAtom = atom(getPreferredLanguage());
|
||||||
|
|
||||||
|
export const useAppLangCode = () => {
|
||||||
|
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
languageDetector.cacheUserLanguage(langCode);
|
||||||
|
}, [langCode]);
|
||||||
|
|
||||||
|
return [langCode, setLangCode] as const;
|
||||||
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import { PureComponent } from "react";
|
import { PureComponent } from "react";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawImperativeAPI,
|
ExcalidrawImperativeAPI,
|
||||||
SocketId,
|
SocketId,
|
||||||
} from "../../packages/excalidraw/types";
|
} from "../../packages/excalidraw/types";
|
||||||
import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
|
import { ErrorDialog } from "../../packages/excalidraw/components/ErrorDialog";
|
||||||
import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
|
import { APP_NAME, ENV, EVENT } from "../../packages/excalidraw/constants";
|
||||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
@@ -18,8 +18,9 @@ import {
|
|||||||
restoreElements,
|
restoreElements,
|
||||||
zoomToFitBounds,
|
zoomToFitBounds,
|
||||||
reconcileElements,
|
reconcileElements,
|
||||||
|
normalizeIndices,
|
||||||
} from "../../packages/excalidraw";
|
} from "../../packages/excalidraw";
|
||||||
import { Collaborator, Gesture } from "../../packages/excalidraw/types";
|
import type { Collaborator, Gesture } from "../../packages/excalidraw/types";
|
||||||
import {
|
import {
|
||||||
assertNever,
|
assertNever,
|
||||||
preventUnload,
|
preventUnload,
|
||||||
@@ -36,12 +37,14 @@ import {
|
|||||||
SYNC_FULL_SCENE_INTERVAL_MS,
|
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||||
WS_EVENTS,
|
WS_EVENTS,
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
|
import type {
|
||||||
|
SocketUpdateDataSource,
|
||||||
|
SyncableExcalidrawElement,
|
||||||
|
} from "../data";
|
||||||
import {
|
import {
|
||||||
generateCollaborationLinkData,
|
generateCollaborationLinkData,
|
||||||
getCollaborationLink,
|
getCollaborationLink,
|
||||||
getSyncableElements,
|
getSyncableElements,
|
||||||
SocketUpdateDataSource,
|
|
||||||
SyncableExcalidrawElement,
|
|
||||||
} from "../data";
|
} from "../data";
|
||||||
import {
|
import {
|
||||||
isSavedToFirebase,
|
isSavedToFirebase,
|
||||||
@@ -77,7 +80,7 @@ import { resetBrowserStateVersions } from "../data/tabSync";
|
|||||||
import { LocalData } from "../data/LocalData";
|
import { LocalData } from "../data/LocalData";
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { appJotaiStore } from "../app-jotai";
|
import { appJotaiStore } from "../app-jotai";
|
||||||
import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
|
import type { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
|
||||||
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
|
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
|
||||||
import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
|
import { withBatchedUpdates } from "../../packages/excalidraw/reactUtils";
|
||||||
import { collabErrorIndicatorAtom } from "./CollabError";
|
import { collabErrorIndicatorAtom } from "./CollabError";
|
||||||
@@ -635,7 +638,16 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
fetchScene: true,
|
fetchScene: true,
|
||||||
roomLinkData: existingRoomLinkData,
|
roomLinkData: existingRoomLinkData,
|
||||||
});
|
});
|
||||||
scenePromise.resolve(sceneData);
|
|
||||||
|
if (sceneData) {
|
||||||
|
scenePromise.resolve({
|
||||||
|
...sceneData,
|
||||||
|
// normalize fractional indices on init for shared scenes, while making sure there are no other collaborators
|
||||||
|
elements: normalizeIndices([...sceneData.elements]),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
scenePromise.resolve(null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.portal.socket.on(
|
this.portal.socket.on(
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {
|
import type {
|
||||||
isSyncableElement,
|
|
||||||
SocketUpdateData,
|
SocketUpdateData,
|
||||||
SocketUpdateDataSource,
|
SocketUpdateDataSource,
|
||||||
SyncableExcalidrawElement,
|
SyncableExcalidrawElement,
|
||||||
} from "../data";
|
} from "../data";
|
||||||
|
import { isSyncableElement } from "../data";
|
||||||
|
|
||||||
import { TCollabClass } from "./Collab";
|
import type { TCollabClass } from "./Collab";
|
||||||
|
|
||||||
import { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types";
|
import type { OrderedExcalidrawElement } from "../../packages/excalidraw/element/types";
|
||||||
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
||||||
import {
|
import type {
|
||||||
OnUserFollowedPayload,
|
OnUserFollowedPayload,
|
||||||
SocketId,
|
SocketId,
|
||||||
UserIdleState,
|
UserIdleState,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
arrowBarToLeftIcon,
|
loginIcon,
|
||||||
ExcalLogo,
|
ExcalLogo,
|
||||||
} from "../../packages/excalidraw/components/icons";
|
} from "../../packages/excalidraw/components/icons";
|
||||||
import { Theme } from "../../packages/excalidraw/element/types";
|
import type { Theme } from "../../packages/excalidraw/element/types";
|
||||||
import { MainMenu } from "../../packages/excalidraw/index";
|
import { MainMenu } from "../../packages/excalidraw/index";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
import { LanguageList } from "./LanguageList";
|
import { LanguageList } from "../app-language/LanguageList";
|
||||||
|
|
||||||
export const AppMainMenu: React.FC<{
|
export const AppMainMenu: React.FC<{
|
||||||
onCollabDialogOpen: () => any;
|
onCollabDialogOpen: () => any;
|
||||||
@@ -34,7 +34,7 @@ export const AppMainMenu: React.FC<{
|
|||||||
<MainMenu.ItemLink
|
<MainMenu.ItemLink
|
||||||
icon={ExcalLogo}
|
icon={ExcalLogo}
|
||||||
href={`${
|
href={`${
|
||||||
import.meta.env.VITE_APP_PLUS_APP
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`}
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=hamburger`}
|
||||||
className=""
|
className=""
|
||||||
>
|
>
|
||||||
@@ -42,7 +42,7 @@ export const AppMainMenu: React.FC<{
|
|||||||
</MainMenu.ItemLink>
|
</MainMenu.ItemLink>
|
||||||
<MainMenu.DefaultItems.Socials />
|
<MainMenu.DefaultItems.Socials />
|
||||||
<MainMenu.ItemLink
|
<MainMenu.ItemLink
|
||||||
icon={arrowBarToLeftIcon}
|
icon={loginIcon}
|
||||||
href={`${import.meta.env.VITE_APP_PLUS_APP}${
|
href={`${import.meta.env.VITE_APP_PLUS_APP}${
|
||||||
isExcalidrawPlusSignedUser ? "" : "/sign-up"
|
isExcalidrawPlusSignedUser ? "" : "/sign-up"
|
||||||
}?utm_source=signin&utm_medium=app&utm_content=hamburger`}
|
}?utm_source=signin&utm_medium=app&utm_content=hamburger`}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { arrowBarToLeftIcon } from "../../packages/excalidraw/components/icons";
|
import { loginIcon } from "../../packages/excalidraw/components/icons";
|
||||||
import { useI18n } from "../../packages/excalidraw/i18n";
|
import { useI18n } from "../../packages/excalidraw/i18n";
|
||||||
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
@@ -61,7 +61,7 @@ export const AppWelcomeScreen: React.FC<{
|
|||||||
import.meta.env.VITE_APP_PLUS_LP
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest`}
|
||||||
shortcut={null}
|
shortcut={null}
|
||||||
icon={arrowBarToLeftIcon}
|
icon={loginIcon}
|
||||||
>
|
>
|
||||||
Sign up
|
Sign up
|
||||||
</WelcomeScreen.Center.MenuItemLink>
|
</WelcomeScreen.Center.MenuItemLink>
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { Card } from "../../packages/excalidraw/components/Card";
|
|||||||
import { ToolButton } from "../../packages/excalidraw/components/ToolButton";
|
import { ToolButton } from "../../packages/excalidraw/components/ToolButton";
|
||||||
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
||||||
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
||||||
import {
|
import type {
|
||||||
FileId,
|
FileId,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { THEME } from "../../packages/excalidraw/constants";
|
import { THEME } from "../../packages/excalidraw/constants";
|
||||||
import { Theme } from "../../packages/excalidraw/element/types";
|
import type { Theme } from "../../packages/excalidraw/element/types";
|
||||||
|
|
||||||
// https://github.com/tholman/github-corners
|
// https://github.com/tholman/github-corners
|
||||||
export const GitHubCorner = React.memo(
|
export const GitHubCorner = React.memo(
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { StoreAction } from "../../packages/excalidraw";
|
|||||||
import { compressData } from "../../packages/excalidraw/data/encode";
|
import { compressData } from "../../packages/excalidraw/data/encode";
|
||||||
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
import { newElementWith } from "../../packages/excalidraw/element/mutateElement";
|
||||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
FileId,
|
FileId,
|
||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import { t } from "../../packages/excalidraw/i18n";
|
import { t } from "../../packages/excalidraw/i18n";
|
||||||
import {
|
import type {
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFileMetadata,
|
BinaryFileMetadata,
|
||||||
ExcalidrawImperativeAPI,
|
ExcalidrawImperativeAPI,
|
||||||
|
|||||||
@@ -20,19 +20,19 @@ import {
|
|||||||
get,
|
get,
|
||||||
} from "idb-keyval";
|
} from "idb-keyval";
|
||||||
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
|
import { clearAppStateForLocalStorage } from "../../packages/excalidraw/appState";
|
||||||
import { LibraryPersistedData } from "../../packages/excalidraw/data/library";
|
import type { LibraryPersistedData } from "../../packages/excalidraw/data/library";
|
||||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||||
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
import { clearElementsForLocalStorage } from "../../packages/excalidraw/element";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "../../packages/excalidraw/types";
|
} from "../../packages/excalidraw/types";
|
||||||
import { MaybePromise } from "../../packages/excalidraw/utility-types";
|
import type { MaybePromise } from "../../packages/excalidraw/utility-types";
|
||||||
import { debounce } from "../../packages/excalidraw/utils";
|
import { debounce } from "../../packages/excalidraw/utils";
|
||||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
||||||
import { FileManager } from "./FileManager";
|
import { FileManager } from "./FileManager";
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { reconcileElements } from "../../packages/excalidraw";
|
import { reconcileElements } from "../../packages/excalidraw";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import { getSceneVersion } from "../../packages/excalidraw/element";
|
import { getSceneVersion } from "../../packages/excalidraw/element";
|
||||||
import Portal from "../collab/Portal";
|
import type Portal from "../collab/Portal";
|
||||||
import { restoreElements } from "../../packages/excalidraw/data/restore";
|
import { restoreElements } from "../../packages/excalidraw/data/restore";
|
||||||
import {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFileMetadata,
|
BinaryFileMetadata,
|
||||||
@@ -20,8 +20,9 @@ import {
|
|||||||
decryptData,
|
decryptData,
|
||||||
} from "../../packages/excalidraw/data/encryption";
|
} from "../../packages/excalidraw/data/encryption";
|
||||||
import { MIME_TYPES } from "../../packages/excalidraw/constants";
|
import { MIME_TYPES } from "../../packages/excalidraw/constants";
|
||||||
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
import type { SyncableExcalidrawElement } from ".";
|
||||||
import { ResolutionType } from "../../packages/excalidraw/utility-types";
|
import { getSyncableElements } from ".";
|
||||||
|
import type { ResolutionType } from "../../packages/excalidraw/utility-types";
|
||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile";
|
import type { RemoteExcalidrawElement } from "../../packages/excalidraw/data/reconcile";
|
||||||
|
|
||||||
|
|||||||
@@ -9,30 +9,30 @@ import {
|
|||||||
} from "../../packages/excalidraw/data/encryption";
|
} from "../../packages/excalidraw/data/encryption";
|
||||||
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
import { serializeAsJSON } from "../../packages/excalidraw/data/json";
|
||||||
import { restore } from "../../packages/excalidraw/data/restore";
|
import { restore } from "../../packages/excalidraw/data/restore";
|
||||||
import { ImportedDataState } from "../../packages/excalidraw/data/types";
|
import type { ImportedDataState } from "../../packages/excalidraw/data/types";
|
||||||
import { SceneBounds } from "../../packages/excalidraw/element/bounds";
|
import type { SceneBounds } from "../../packages/excalidraw/element/bounds";
|
||||||
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
|
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
|
||||||
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "../../packages/excalidraw/element/types";
|
} from "../../packages/excalidraw/element/types";
|
||||||
import { t } from "../../packages/excalidraw/i18n";
|
import { t } from "../../packages/excalidraw/i18n";
|
||||||
import {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
SocketId,
|
SocketId,
|
||||||
UserIdleState,
|
UserIdleState,
|
||||||
} from "../../packages/excalidraw/types";
|
} from "../../packages/excalidraw/types";
|
||||||
import { MakeBrand } from "../../packages/excalidraw/utility-types";
|
import type { MakeBrand } from "../../packages/excalidraw/utility-types";
|
||||||
import { bytesToHexString } from "../../packages/excalidraw/utils";
|
import { bytesToHexString } from "../../packages/excalidraw/utils";
|
||||||
|
import type { WS_SUBTYPES } from "../app_constants";
|
||||||
import {
|
import {
|
||||||
DELETED_ELEMENT_TIMEOUT,
|
DELETED_ELEMENT_TIMEOUT,
|
||||||
FILE_UPLOAD_MAX_BYTES,
|
FILE_UPLOAD_MAX_BYTES,
|
||||||
ROOM_ID_BYTES,
|
ROOM_ID_BYTES,
|
||||||
WS_SUBTYPES,
|
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
import { encodeFilesForUpload } from "./FileManager";
|
import { encodeFilesForUpload } from "./FileManager";
|
||||||
import { saveFilesToFirebase } from "./firebase";
|
import { saveFilesToFirebase } from "./firebase";
|
||||||
@@ -254,7 +254,7 @@ export const loadScene = async (
|
|||||||
await importFromBackend(id, privateKey),
|
await importFromBackend(id, privateKey),
|
||||||
localDataState?.appState,
|
localDataState?.appState,
|
||||||
localDataState?.elements,
|
localDataState?.elements,
|
||||||
{ repairBindings: true, refreshDimensions: false },
|
{ repairBindings: true },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data = restore(localDataState || null, null, null, {
|
data = restore(localDataState || null, null, null, {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
import type { ExcalidrawElement } from "../../packages/excalidraw/element/types";
|
||||||
import { AppState } from "../../packages/excalidraw/types";
|
import type { AppState } from "../../packages/excalidraw/types";
|
||||||
import {
|
import {
|
||||||
clearAppStateForLocalStorage,
|
clearAppStateForLocalStorage,
|
||||||
getDefaultAppState,
|
getDefaultAppState,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
name="description"
|
name="description"
|
||||||
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
||||||
/>
|
/>
|
||||||
<meta name="image" content="https://excalidraw.com/og-image-2.png" />
|
<meta name="image" content="https://excalidraw.com/og-image-3.png" />
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:site_name" content="Excalidraw" />
|
<meta property="og:site_name" content="Excalidraw" />
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
property="og:description"
|
property="og:description"
|
||||||
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
content="Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
|
||||||
/>
|
/>
|
||||||
<meta property="og:image" content="https://excalidraw.com/og-image-2.png" />
|
<meta property="og:image" content="https://excalidraw.com/og-image-3.png" />
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
property="twitter:image"
|
property="twitter:image"
|
||||||
content="https://excalidraw.com/og-twitter-v2.png"
|
content="https://excalidraw.com/og-image-3.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- General tags -->
|
<!-- General tags -->
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
margin-inline-end: 0.6em;
|
margin-inline-end: 0.6em;
|
||||||
|
z-index: var(--zIndex-layerUI);
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1.2rem;
|
width: 1.2rem;
|
||||||
@@ -40,6 +41,10 @@
|
|||||||
}
|
}
|
||||||
&.highlighted {
|
&.highlighted {
|
||||||
color: var(--color-promo);
|
color: var(--color-promo);
|
||||||
|
font-weight: 700;
|
||||||
|
.dropdown-menu-item__icon g {
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,13 @@
|
|||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-node": "node ./scripts/build-node.js",
|
"build-node": "node ./scripts/build-node.js",
|
||||||
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
|
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true vite build",
|
||||||
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
|
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA cross-env VITE_APP_ENABLE_TRACKING=true vite build",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
} from "../../packages/excalidraw/components/icons";
|
} from "../../packages/excalidraw/components/icons";
|
||||||
import { TextField } from "../../packages/excalidraw/components/TextField";
|
import { TextField } from "../../packages/excalidraw/components/TextField";
|
||||||
import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
|
import { FilledButton } from "../../packages/excalidraw/components/FilledButton";
|
||||||
import { activeRoomLinkAtom, CollabAPI } from "../collab/Collab";
|
import type { CollabAPI } from "../collab/Collab";
|
||||||
|
import { activeRoomLinkAtom } from "../collab/Collab";
|
||||||
import { atom, useAtom, useAtomValue } from "jotai";
|
import { atom, useAtom, useAtomValue } from "jotai";
|
||||||
|
|
||||||
import "./ShareDialog.scss";
|
import "./ShareDialog.scss";
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -216,23 +216,22 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
|||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<g>
|
<g
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 0h24v24H0z"
|
d="M0 0h24v24H0z"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="none"
|
stroke="none"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M10 12l10 0"
|
d="M15 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M10 12l4 4"
|
d="M21 12h-13l3 -3"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M10 12l4 -4"
|
d="M11 15l-3 -3"
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M4 4l0 16"
|
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { atom, useAtom } from "jotai";
|
|||||||
import { useEffect, useLayoutEffect, useState } from "react";
|
import { useEffect, useLayoutEffect, useState } from "react";
|
||||||
import { THEME } from "../packages/excalidraw";
|
import { THEME } from "../packages/excalidraw";
|
||||||
import { EVENT } from "../packages/excalidraw/constants";
|
import { EVENT } from "../packages/excalidraw/constants";
|
||||||
import { Theme } from "../packages/excalidraw/element/types";
|
import type { Theme } from "../packages/excalidraw/element/types";
|
||||||
import { CODES, KEYS } from "../packages/excalidraw/keys";
|
import { CODES, KEYS } from "../packages/excalidraw/keys";
|
||||||
import { STORAGE_KEYS } from "./app_constants";
|
import { STORAGE_KEYS } from "./app_constants";
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "excalidraw-monorepo",
|
"name": "excalidraw-monorepo",
|
||||||
|
"packageManager": "yarn@1.22.22",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"excalidraw-app",
|
"excalidraw-app",
|
||||||
"packages/excalidraw",
|
"packages/excalidraw",
|
||||||
@@ -60,9 +61,9 @@
|
|||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-node": "node ./scripts/build-node.js",
|
"build-node": "node ./scripts/build-node.js",
|
||||||
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true VITE_APP_DISABLE_TRACKING=true vite build",
|
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",
|
||||||
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA vite build",
|
"build:app": "yarn --cwd ./excalidraw-app build:app",
|
||||||
"build:version": "node ./scripts/build-version.js",
|
"build:version": "yarn --cwd ./excalidraw-app build:version",
|
||||||
"build": "yarn --cwd ./excalidraw-app build",
|
"build": "yarn --cwd ./excalidraw-app build",
|
||||||
"fix:code": "yarn test:code --fix",
|
"fix:code": "yarn test:code --fix",
|
||||||
"fix:other": "yarn prettier --write",
|
"fix:other": "yarn prettier --write",
|
||||||
|
|||||||
@@ -15,8 +15,12 @@ Please add the latest change on the top under the correct section.
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
- `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135)
|
||||||
|
|
||||||
- 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)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { alignElements, Alignment } from "../align";
|
import type { Alignment } from "../align";
|
||||||
|
import { alignElements } from "../align";
|
||||||
import {
|
import {
|
||||||
AlignBottomIcon,
|
AlignBottomIcon,
|
||||||
AlignLeftIcon,
|
AlignLeftIcon,
|
||||||
@@ -10,13 +11,13 @@ import {
|
|||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { isFrameLikeElement } from "../element/typeChecks";
|
import { isFrameLikeElement } from "../element/typeChecks";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { AppClassProperties, AppState, UIAppState } from "../types";
|
import type { AppClassProperties, AppState, UIAppState } from "../types";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
VERTICAL_ALIGN,
|
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { isTextElement, newElement } from "../element";
|
import { isTextElement, newElement } from "../element";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
@@ -23,14 +23,14 @@ import {
|
|||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
isUsingAdaptiveRadius,
|
isUsingAdaptiveRadius,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import { arrayToMap, getFontString } from "../utils";
|
import { arrayToMap, getFontString } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { syncMovedIndices } from "../fractionalIndex";
|
import { syncMovedIndices } from "../fractionalIndex";
|
||||||
@@ -142,6 +142,7 @@ export const actionBindText = register({
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
|
autoResize: true,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
mutateElement(container, {
|
||||||
boundElements: (container.boundElements || []).concat({
|
boundElements: (container.boundElements || []).concat({
|
||||||
@@ -296,6 +297,7 @@ export const actionWrapTextInContainer = register({
|
|||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
boundElements: null,
|
boundElements: null,
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
|
autoResize: true,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ import {
|
|||||||
ZOOM_STEP,
|
ZOOM_STEP,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getCommonBounds, getNonDeletedElements } from "../element";
|
import { getCommonBounds, getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getNormalizedZoom } from "../scene";
|
import { getNormalizedZoom } from "../scene";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import { AppState, NormalizedZoomValue } from "../types";
|
import type { AppState, NormalizedZoomValue } from "../types";
|
||||||
import { getShortcutKey, updateActiveTool } from "../utils";
|
import { getShortcutKey, updateActiveTool } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { Tooltip } from "../components/Tooltip";
|
import { Tooltip } from "../components/Tooltip";
|
||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
isHandToolActive,
|
isHandToolActive,
|
||||||
} from "../appState";
|
} from "../appState";
|
||||||
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
||||||
import { SceneBounds } from "../element/bounds";
|
import type { SceneBounds } from "../element/bounds";
|
||||||
import { setCursor } from "../cursor";
|
import { setCursor } from "../cursor";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ export const actionClearCanvas = register({
|
|||||||
exportBackground: appState.exportBackground,
|
exportBackground: appState.exportBackground,
|
||||||
exportEmbedScene: appState.exportEmbedScene,
|
exportEmbedScene: appState.exportEmbedScene,
|
||||||
gridSize: appState.gridSize,
|
gridSize: appState.gridSize,
|
||||||
showStats: appState.showStats,
|
stats: appState.stats,
|
||||||
pasteDialog: appState.pasteDialog,
|
pasteDialog: appState.pasteDialog,
|
||||||
activeTool:
|
activeTool:
|
||||||
appState.activeTool.type === "image"
|
appState.activeTool.type === "image"
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { ToolButton } from "../components/ToolButton";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { getElementsInGroup } from "../groups";
|
import { getElementsInGroup } from "../groups";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ import {
|
|||||||
DistributeVerticallyIcon,
|
DistributeVerticallyIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { distributeElements, Distribution } from "../distribute";
|
import type { Distribution } from "../distribute";
|
||||||
|
import { distributeElements } from "../distribute";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { isFrameLikeElement } from "../element/typeChecks";
|
import { isFrameLikeElement } from "../element/typeChecks";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import type { AppClassProperties, AppState } from "../types";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import { duplicateElement, getNonDeletedElements } from "../element";
|
import { duplicateElement, getNonDeletedElements } from "../element";
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
@@ -12,9 +12,9 @@ import {
|
|||||||
getSelectedGroupForElement,
|
getSelectedGroupForElement,
|
||||||
getElementsInGroup,
|
getElementsInGroup,
|
||||||
} from "../groups";
|
} from "../groups";
|
||||||
import { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { fixBindingsAfterDuplication } from "../element/binding";
|
import { fixBindingsAfterDuplication } from "../element/binding";
|
||||||
import { ActionResult } from "./types";
|
import type { ActionResult } from "./types";
|
||||||
import { GRID_SIZE } from "../constants";
|
import { GRID_SIZE } from "../constants";
|
||||||
import {
|
import {
|
||||||
bindTextToShapeAfterDuplication,
|
bindTextToShapeAfterDuplication,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LockedIcon, UnlockedIcon } from "../components/icons";
|
import { LockedIcon, UnlockedIcon } from "../components/icons";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { isFrameLikeElement } from "../element/typeChecks";
|
import { isFrameLikeElement } from "../element/typeChecks";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { getSelectedElements, isSomeElementSelected } from "../scene";
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { isImageFileHandle } from "../data/blob";
|
import { isImageFileHandle } from "../data/blob";
|
||||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
import { Theme } from "../element/types";
|
import type { Theme } from "../element/types";
|
||||||
|
|
||||||
import "../components/ToolIcon.scss";
|
import "../components/ToolIcon.scss";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import { isBindingElement, isLinearElement } from "../element/typeChecks";
|
import { isBindingElement, isLinearElement } from "../element/typeChecks";
|
||||||
import { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { resetCursor } from "../cursor";
|
import { resetCursor } from "../cursor";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
|
||||||
@@ -131,7 +131,12 @@ export const actionFinalize = register({
|
|||||||
-1,
|
-1,
|
||||||
arrayToMap(elements),
|
arrayToMap(elements),
|
||||||
);
|
);
|
||||||
maybeBindLinearElement(multiPointElement, appState, { x, y }, app);
|
maybeBindLinearElement(
|
||||||
|
multiPointElement,
|
||||||
|
appState,
|
||||||
|
{ x, y },
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { resizeMultipleElements } from "../element/resizeElements";
|
import { resizeMultipleElements } from "../element/resizeElements";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import type { AppClassProperties, AppState } from "../types";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getCommonBoundingBox } from "../element/bounds";
|
import { getCommonBoundingBox } from "../element/bounds";
|
||||||
@@ -124,7 +124,7 @@ const flipElements = (
|
|||||||
|
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
selectedElements.filter(isLinearElement),
|
selectedElements.filter(isLinearElement),
|
||||||
app,
|
elementsMap,
|
||||||
isBindingEnabled(appState),
|
isBindingEnabled(appState),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import { removeAllElementsFromFrame } from "../frame";
|
import { removeAllElementsFromFrame } from "../frame";
|
||||||
import { getFrameChildren } from "../frame";
|
import { getFrameChildren } from "../frame";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { AppClassProperties, AppState, UIAppState } from "../types";
|
import type { AppClassProperties, AppState, UIAppState } from "../types";
|
||||||
import { updateActiveTool } from "../utils";
|
import { updateActiveTool } from "../utils";
|
||||||
import { setCursorForShape } from "../cursor";
|
import { setCursorForShape } from "../cursor";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ import {
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import type { AppClassProperties, AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getElementsInResizingFrame,
|
getElementsInResizingFrame,
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { Action, ActionResult } from "./types";
|
import type { Action, ActionResult } from "./types";
|
||||||
import { UndoIcon, RedoIcon } from "../components/icons";
|
import { UndoIcon, RedoIcon } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { History, HistoryChangedEvent } from "../history";
|
import type { History } from "../history";
|
||||||
import { AppState } from "../types";
|
import { HistoryChangedEvent } from "../history";
|
||||||
|
import type { AppState } from "../types";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import { isWindows } from "../constants";
|
import { isWindows } from "../constants";
|
||||||
import { SceneElementsMap } from "../element/types";
|
import type { SceneElementsMap } from "../element/types";
|
||||||
import { Store, StoreAction } from "../store";
|
import type { Store } from "../store";
|
||||||
|
import { StoreAction } from "../store";
|
||||||
import { useEmitter } from "../hooks/useEmitter";
|
import { useEmitter } from "../hooks/useEmitter";
|
||||||
|
|
||||||
const writeData = (
|
const writeData = (
|
||||||
@@ -63,7 +65,10 @@ export const createUndoAction: ActionCreator = (history, store) => ({
|
|||||||
PanelComponent: ({ updateData, data }) => {
|
PanelComponent: ({ updateData, data }) => {
|
||||||
const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>(
|
const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>(
|
||||||
history.onHistoryChangedEmitter,
|
history.onHistoryChangedEmitter,
|
||||||
new HistoryChangedEvent(),
|
new HistoryChangedEvent(
|
||||||
|
history.isUndoStackEmpty,
|
||||||
|
history.isRedoStackEmpty,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -74,6 +79,7 @@ export const createUndoAction: ActionCreator = (history, store) => ({
|
|||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
size={data?.size || "medium"}
|
size={data?.size || "medium"}
|
||||||
disabled={isUndoStackEmpty}
|
disabled={isUndoStackEmpty}
|
||||||
|
data-testid="button-undo"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -101,7 +107,10 @@ export const createRedoAction: ActionCreator = (history, store) => ({
|
|||||||
PanelComponent: ({ updateData, data }) => {
|
PanelComponent: ({ updateData, data }) => {
|
||||||
const { isRedoStackEmpty } = useEmitter(
|
const { isRedoStackEmpty } = useEmitter(
|
||||||
history.onHistoryChangedEmitter,
|
history.onHistoryChangedEmitter,
|
||||||
new HistoryChangedEvent(),
|
new HistoryChangedEvent(
|
||||||
|
history.isUndoStackEmpty,
|
||||||
|
history.isRedoStackEmpty,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -112,6 +121,7 @@ export const createRedoAction: ActionCreator = (history, store) => ({
|
|||||||
onClick={updateData}
|
onClick={updateData}
|
||||||
size={data?.size || "medium"}
|
size={data?.size || "medium"}
|
||||||
disabled={isRedoStackEmpty}
|
disabled={isRedoStackEmpty}
|
||||||
|
data-testid="button-redo"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+34
-6
@@ -1,9 +1,12 @@
|
|||||||
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { ExcalidrawLinearElement } from "../element/types";
|
import type { ExcalidrawLinearElement } from "../element/types";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
import { lineEditorIcon } from "../components/icons";
|
||||||
|
|
||||||
export const actionToggleLinearEditor = register({
|
export const actionToggleLinearEditor = register({
|
||||||
name: "toggleLinearEditor",
|
name: "toggleLinearEditor",
|
||||||
@@ -11,18 +14,23 @@ export const actionToggleLinearEditor = register({
|
|||||||
label: (elements, appState, app) => {
|
label: (elements, appState, app) => {
|
||||||
const selectedElement = app.scene.getSelectedElements({
|
const selectedElement = app.scene.getSelectedElements({
|
||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: true,
|
})[0] as ExcalidrawLinearElement | undefined;
|
||||||
})[0] as ExcalidrawLinearElement;
|
|
||||||
return appState.editingLinearElement?.elementId === selectedElement?.id
|
return selectedElement?.type === "arrow"
|
||||||
? "labels.lineEditor.exit"
|
? "labels.lineEditor.editArrow"
|
||||||
: "labels.lineEditor.edit";
|
: "labels.lineEditor.edit";
|
||||||
},
|
},
|
||||||
|
keywords: ["line"],
|
||||||
trackEvent: {
|
trackEvent: {
|
||||||
category: "element",
|
category: "element",
|
||||||
},
|
},
|
||||||
predicate: (elements, appState, _, app) => {
|
predicate: (elements, appState, _, app) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
|
if (
|
||||||
|
!appState.editingLinearElement &&
|
||||||
|
selectedElements.length === 1 &&
|
||||||
|
isLinearElement(selectedElements[0])
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -45,4 +53,24 @@ export const actionToggleLinearEditor = register({
|
|||||||
storeAction: StoreAction.CAPTURE,
|
storeAction: StoreAction.CAPTURE,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
PanelComponent: ({ appState, updateData, app }) => {
|
||||||
|
const selectedElement = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
})[0] as ExcalidrawLinearElement;
|
||||||
|
|
||||||
|
const label = t(
|
||||||
|
selectedElement.type === "arrow"
|
||||||
|
? "labels.lineEditor.editArrow"
|
||||||
|
: "labels.lineEditor.edit",
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<ToolButton
|
||||||
|
type="button"
|
||||||
|
icon={lineEditorIcon}
|
||||||
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getClientColor } from "../clients";
|
import { getClientColor } from "../clients";
|
||||||
import { Avatar } from "../components/Avatar";
|
import { Avatar } from "../components/Avatar";
|
||||||
import { GoToCollaboratorComponentProps } from "../components/UserList";
|
import type { GoToCollaboratorComponentProps } from "../components/UserList";
|
||||||
import {
|
import {
|
||||||
eyeIcon,
|
eyeIcon,
|
||||||
microphoneIcon,
|
microphoneIcon,
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { Collaborator } from "../types";
|
import type { Collaborator } from "../types";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
|||||||
@@ -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 { AppClassProperties, AppState, Primitive } from "../types";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
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,16 +65,13 @@ 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,
|
||||||
isUsingAdaptiveRadius,
|
isUsingAdaptiveRadius,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import type {
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -167,7 +165,7 @@ const offsetElementAfterFontResize = (
|
|||||||
prevElement: ExcalidrawTextElement,
|
prevElement: ExcalidrawTextElement,
|
||||||
nextElement: ExcalidrawTextElement,
|
nextElement: ExcalidrawTextElement,
|
||||||
) => {
|
) => {
|
||||||
if (isBoundToContainer(nextElement)) {
|
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
|
||||||
return nextElement;
|
return nextElement;
|
||||||
}
|
}
|
||||||
return mutateElement(
|
return mutateElement(
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { KEYS } from "../keys";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { selectGroupsForSelectedElements } from "../groups";
|
import { selectGroupsForSelectedElements } from "../groups";
|
||||||
import { getNonDeletedElements, isTextElement } from "../element";
|
import { getNonDeletedElements, isTextElement } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { excludeElementsInFramesFromSelection } from "../scene/selection";
|
import { excludeElementsInFramesFromSelection } from "../scene/selection";
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -24,9 +21,10 @@ import {
|
|||||||
isArrowElement,
|
isArrowElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { 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) {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { isTextElement } from "../element";
|
||||||
|
import { newElementWith } from "../element/mutateElement";
|
||||||
|
import { measureText } from "../element/textElement";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { StoreAction } from "../store";
|
||||||
|
import type { AppClassProperties } from "../types";
|
||||||
|
import { getFontString } from "../utils";
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
export const actionTextAutoResize = register({
|
||||||
|
name: "autoResize",
|
||||||
|
label: "labels.autoResize",
|
||||||
|
icon: null,
|
||||||
|
trackEvent: { category: "element" },
|
||||||
|
predicate: (elements, appState, _: unknown, app: AppClassProperties) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
return (
|
||||||
|
selectedElements.length === 1 &&
|
||||||
|
isTextElement(selectedElements[0]) &&
|
||||||
|
!selectedElements[0].autoResize
|
||||||
|
);
|
||||||
|
},
|
||||||
|
perform: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
|
return {
|
||||||
|
appState,
|
||||||
|
elements: elements.map((element) => {
|
||||||
|
if (element.id === selectedElements[0].id && isTextElement(element)) {
|
||||||
|
const metrics = measureText(
|
||||||
|
element.originalText,
|
||||||
|
getFontString(element),
|
||||||
|
element.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
return newElementWith(element, {
|
||||||
|
autoResize: true,
|
||||||
|
width: metrics.width,
|
||||||
|
height: metrics.height,
|
||||||
|
text: element.originalText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}),
|
||||||
|
storeAction: StoreAction.CAPTURE,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { GRID_SIZE } from "../constants";
|
import { GRID_SIZE } from "../constants";
|
||||||
import { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import { gridIcon } from "../components/icons";
|
import { gridIcon } from "../components/icons";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,22 @@ import { StoreAction } from "../store";
|
|||||||
|
|
||||||
export const actionToggleStats = register({
|
export const actionToggleStats = register({
|
||||||
name: "stats",
|
name: "stats",
|
||||||
label: "stats.title",
|
label: "stats.fullTitle",
|
||||||
icon: abacusIcon,
|
icon: abacusIcon,
|
||||||
paletteName: "Toggle stats",
|
paletteName: "Toggle stats",
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
trackEvent: { category: "menu" },
|
trackEvent: { category: "menu" },
|
||||||
|
keywords: ["edit", "attributes", "customize"],
|
||||||
perform(elements, appState) {
|
perform(elements, appState) {
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
showStats: !this.checked!(appState),
|
stats: { ...appState.stats, open: !this.checked!(appState) },
|
||||||
},
|
},
|
||||||
storeAction: StoreAction.NONE,
|
storeAction: StoreAction.NONE,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
checked: (appState) => appState.showStats,
|
checked: (appState) => appState.stats.open,
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.SLASH,
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.SLASH,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { StoreAction } from "../store";
|
|||||||
export const actionSendBackward = register({
|
export const actionSendBackward = register({
|
||||||
name: "sendBackward",
|
name: "sendBackward",
|
||||||
label: "labels.sendBackward",
|
label: "labels.sendBackward",
|
||||||
|
keywords: ["move down", "zindex", "layer"],
|
||||||
icon: SendBackwardIcon,
|
icon: SendBackwardIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
@@ -49,6 +50,7 @@ export const actionSendBackward = register({
|
|||||||
export const actionBringForward = register({
|
export const actionBringForward = register({
|
||||||
name: "bringForward",
|
name: "bringForward",
|
||||||
label: "labels.bringForward",
|
label: "labels.bringForward",
|
||||||
|
keywords: ["move up", "zindex", "layer"],
|
||||||
icon: BringForwardIcon,
|
icon: BringForwardIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
@@ -78,6 +80,7 @@ export const actionBringForward = register({
|
|||||||
export const actionSendToBack = register({
|
export const actionSendToBack = register({
|
||||||
name: "sendToBack",
|
name: "sendToBack",
|
||||||
label: "labels.sendToBack",
|
label: "labels.sendToBack",
|
||||||
|
keywords: ["move down", "zindex", "layer"],
|
||||||
icon: SendToBackIcon,
|
icon: SendToBackIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
@@ -114,6 +117,7 @@ export const actionSendToBack = register({
|
|||||||
export const actionBringToFront = register({
|
export const actionBringToFront = register({
|
||||||
name: "bringToFront",
|
name: "bringToFront",
|
||||||
label: "labels.bringToFront",
|
label: "labels.bringToFront",
|
||||||
|
keywords: ["move up", "zindex", "layer"],
|
||||||
icon: BringToFrontIcon,
|
icon: BringToFrontIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import type {
|
||||||
Action,
|
Action,
|
||||||
UpdaterFn,
|
UpdaterFn,
|
||||||
ActionName,
|
ActionName,
|
||||||
@@ -7,8 +7,11 @@ import {
|
|||||||
PanelComponentProps,
|
PanelComponentProps,
|
||||||
ActionSource,
|
ActionSource,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ExcalidrawElement, OrderedExcalidrawElement } from "../element/types";
|
import type {
|
||||||
import { AppClassProperties, AppState } from "../types";
|
ExcalidrawElement,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
|
import type { AppClassProperties, AppState } from "../types";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { isPromiseLike } from "../utils";
|
import { isPromiseLike } from "../utils";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Action } from "./types";
|
import type { Action } from "./types";
|
||||||
|
|
||||||
export let actions: readonly Action[] = [];
|
export let actions: readonly Action[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { isDarwin } from "../constants";
|
import { isDarwin } from "../constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { SubtypeOf } from "../utility-types";
|
import type { SubtypeOf } from "../utility-types";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { ActionName } from "./types";
|
import type { ActionName } from "./types";
|
||||||
|
|
||||||
export type ShortcutName =
|
export type ShortcutName =
|
||||||
| SubtypeOf<
|
| SubtypeOf<
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import React from "react";
|
import type React from "react";
|
||||||
import { ExcalidrawElement, OrderedExcalidrawElement } from "../element/types";
|
import type {
|
||||||
import {
|
ExcalidrawElement,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
|
import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppState,
|
AppState,
|
||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
UIAppState,
|
UIAppState,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { MarkOptional } from "../utility-types";
|
import type { MarkOptional } from "../utility-types";
|
||||||
import { StoreActionType } from "../store";
|
import type { StoreActionType } from "../store";
|
||||||
|
|
||||||
export type ActionSource =
|
export type ActionSource =
|
||||||
| "ui"
|
| "ui"
|
||||||
@@ -131,7 +134,9 @@ export type ActionName =
|
|||||||
| "setEmbeddableAsActiveTool"
|
| "setEmbeddableAsActiveTool"
|
||||||
| "createContainerFromText"
|
| "createContainerFromText"
|
||||||
| "wrapTextInContainer"
|
| "wrapTextInContainer"
|
||||||
| "commandPalette";
|
| "commandPalette"
|
||||||
|
| "autoResize"
|
||||||
|
| "elementStats";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ElementsMap, ExcalidrawElement } from "./element/types";
|
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
||||||
import { newElementWith } from "./element/mutateElement";
|
import { newElementWith } from "./element/mutateElement";
|
||||||
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
|
import type { BoundingBox } from "./element/bounds";
|
||||||
|
import { getCommonBoundingBox } from "./element/bounds";
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
|
|
||||||
export interface Alignment {
|
export interface Alignment {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// place here categories that you want to track. We want to track just a
|
// place here categories that you want to track. We want to track just a
|
||||||
// small subset of categories at a given time.
|
// small subset of categories at a given time.
|
||||||
const ALLOWED_CATEGORIES_TO_TRACK = ["ai", "command_palette"] as string[];
|
const ALLOWED_CATEGORIES_TO_TRACK = new Set(["command_palette"]);
|
||||||
|
|
||||||
export const trackEvent = (
|
export const trackEvent = (
|
||||||
category: string,
|
category: string,
|
||||||
@@ -9,17 +9,20 @@ export const trackEvent = (
|
|||||||
value?: number,
|
value?: number,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// prettier-ignore
|
|
||||||
if (
|
if (
|
||||||
typeof window === "undefined"
|
typeof window === "undefined" ||
|
||||||
|| import.meta.env.VITE_WORKER_ID
|
import.meta.env.VITE_WORKER_ID ||
|
||||||
// comment out to debug locally
|
import.meta.env.VITE_APP_ENABLE_TRACKING !== "true"
|
||||||
|| import.meta.env.PROD
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ALLOWED_CATEGORIES_TO_TRACK.includes(category)) {
|
if (!ALLOWED_CATEGORIES_TO_TRACK.has(category)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// comment out to debug in dev
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { LaserPointer, LaserPointerOptions } from "@excalidraw/laser-pointer";
|
import type { LaserPointerOptions } from "@excalidraw/laser-pointer";
|
||||||
import { AnimationFrameHandler } from "./animation-frame-handler";
|
import { LaserPointer } from "@excalidraw/laser-pointer";
|
||||||
import { AppState } from "./types";
|
import type { AnimationFrameHandler } from "./animation-frame-handler";
|
||||||
|
import type { AppState } from "./types";
|
||||||
import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils";
|
import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils";
|
||||||
import type App from "./components/App";
|
import type App from "./components/App";
|
||||||
import { SVG_NS } from "./constants";
|
import { SVG_NS } from "./constants";
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import {
|
|||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
EXPORT_SCALES,
|
EXPORT_SCALES,
|
||||||
|
STATS_PANELS,
|
||||||
THEME,
|
THEME,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { AppState, NormalizedZoomValue } from "./types";
|
import type { AppState, NormalizedZoomValue } from "./types";
|
||||||
|
|
||||||
const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
|
const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
|
||||||
? devicePixelRatio
|
? devicePixelRatio
|
||||||
@@ -35,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,
|
||||||
@@ -80,7 +82,10 @@ export const getDefaultAppState = (): Omit<
|
|||||||
selectedElementsAreBeingDragged: false,
|
selectedElementsAreBeingDragged: false,
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
showStats: false,
|
stats: {
|
||||||
|
open: false,
|
||||||
|
panels: STATS_PANELS.generalStats | STATS_PANELS.elementProperties,
|
||||||
|
},
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
||||||
@@ -145,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 },
|
||||||
@@ -196,7 +202,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
},
|
},
|
||||||
selectionElement: { browser: false, export: false, server: false },
|
selectionElement: { browser: false, export: false, server: false },
|
||||||
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
||||||
showStats: { browser: true, export: false, server: false },
|
stats: { browser: true, export: false, server: false },
|
||||||
startBoundElement: { browser: false, export: false, server: false },
|
startBoundElement: { browser: false, export: false, server: false },
|
||||||
suggestedBindings: { browser: false, export: false, server: false },
|
suggestedBindings: { browser: false, export: false, server: false },
|
||||||
frameRendering: { browser: false, export: false, server: false },
|
frameRendering: { browser: false, export: false, server: false },
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import { ENV } from "./constants";
|
import { ENV } from "./constants";
|
||||||
|
import type { BindableProp, BindingProp } from "./element/binding";
|
||||||
import {
|
import {
|
||||||
BoundElement,
|
BoundElement,
|
||||||
BindableElement,
|
BindableElement,
|
||||||
BindableProp,
|
|
||||||
BindingProp,
|
|
||||||
bindingProperties,
|
bindingProperties,
|
||||||
updateBoundElements,
|
updateBoundElements,
|
||||||
} from "./element/binding";
|
} from "./element/binding";
|
||||||
import { LinearElementEditor } from "./element/linearElementEditor";
|
import { LinearElementEditor } from "./element/linearElementEditor";
|
||||||
import {
|
import type { ElementUpdate } from "./element/mutateElement";
|
||||||
ElementUpdate,
|
import { mutateElement, newElementWith } from "./element/mutateElement";
|
||||||
mutateElement,
|
|
||||||
newElementWith,
|
|
||||||
} from "./element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@@ -23,7 +19,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./element/typeChecks";
|
} from "./element/typeChecks";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -34,13 +30,13 @@ import {
|
|||||||
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
|
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
|
||||||
import { getNonDeletedGroupIds } from "./groups";
|
import { getNonDeletedGroupIds } from "./groups";
|
||||||
import { getObservedAppState } from "./store";
|
import { getObservedAppState } from "./store";
|
||||||
import {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
ObservedAppState,
|
ObservedAppState,
|
||||||
ObservedElementsAppState,
|
ObservedElementsAppState,
|
||||||
ObservedStandaloneAppState,
|
ObservedStandaloneAppState,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { SubtypeOf, ValueOf } from "./utility-types";
|
import type { SubtypeOf, ValueOf } from "./utility-types";
|
||||||
import {
|
import {
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
arrayToObject,
|
arrayToObject,
|
||||||
@@ -1481,19 +1477,28 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
|||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previous = Array.from(elements.values());
|
const unordered = Array.from(elements.values());
|
||||||
const reordered = orderByFractionalIndex([...previous]);
|
const ordered = orderByFractionalIndex([...unordered]);
|
||||||
|
const moved = Delta.getRightDifferences(unordered, ordered, true).reduce(
|
||||||
|
(acc, arrayIndex) => {
|
||||||
|
const candidate = unordered[Number(arrayIndex)];
|
||||||
|
if (candidate && changed.has(candidate.id)) {
|
||||||
|
acc.set(candidate.id, candidate);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
return acc;
|
||||||
!flags.containsVisibleDifference &&
|
},
|
||||||
Delta.isRightDifferent(previous, reordered, true)
|
new Map(),
|
||||||
) {
|
);
|
||||||
|
|
||||||
|
if (!flags.containsVisibleDifference && moved.size) {
|
||||||
// we found a difference in order!
|
// we found a difference in order!
|
||||||
flags.containsVisibleDifference = true;
|
flags.containsVisibleDifference = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// let's synchronize all invalid indices of moved elements
|
// synchronize all elements that were actually moved
|
||||||
return arrayToMap(syncMovedIndices(reordered, changed)) as typeof elements;
|
// could fallback to synchronizing all invalid indices
|
||||||
|
return arrayToMap(syncMovedIndices(ordered, moved)) as typeof elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import type { Spreadsheet } from "./charts";
|
||||||
Spreadsheet,
|
import { tryParseCells, tryParseNumber, VALID_SPREADSHEET } from "./charts";
|
||||||
tryParseCells,
|
|
||||||
tryParseNumber,
|
|
||||||
VALID_SPREADSHEET,
|
|
||||||
} from "./charts";
|
|
||||||
|
|
||||||
describe("charts", () => {
|
describe("charts", () => {
|
||||||
describe("tryParseNumber", () => {
|
describe("tryParseNumber", () => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { newElement, newLinearElement, newTextElement } from "./element";
|
import { newElement, newLinearElement, newTextElement } from "./element";
|
||||||
import { NonDeletedExcalidrawElement } from "./element/types";
|
import type { NonDeletedExcalidrawElement } from "./element/types";
|
||||||
import { randomId } from "./random";
|
import { randomId } from "./random";
|
||||||
|
|
||||||
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
export type ChartElements = readonly NonDeletedExcalidrawElement[];
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import {
|
|||||||
THEME,
|
THEME,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { roundRect } from "./renderer/roundRect";
|
import { roundRect } from "./renderer/roundRect";
|
||||||
import { InteractiveCanvasRenderConfig } from "./scene/types";
|
import type { InteractiveCanvasRenderConfig } from "./scene/types";
|
||||||
import {
|
import type {
|
||||||
Collaborator,
|
Collaborator,
|
||||||
InteractiveCanvasAppState,
|
InteractiveCanvasAppState,
|
||||||
SocketId,
|
SocketId,
|
||||||
UserIdleState,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { UserIdleState } from "./types";
|
||||||
|
|
||||||
function hashToInteger(id: string) {
|
function hashToInteger(id: string) {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { BinaryFiles } from "./types";
|
import type { BinaryFiles } from "./types";
|
||||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
import type { Spreadsheet } from "./charts";
|
||||||
|
import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import {
|
import {
|
||||||
ALLOWED_PASTE_MIME_TYPES,
|
ALLOWED_PASTE_MIME_TYPES,
|
||||||
EXPORT_DATA_TYPES,
|
EXPORT_DATA_TYPES,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import { Merge } from "./utility-types";
|
import type { Merge } from "./utility-types";
|
||||||
|
|
||||||
// FIXME can't put to utils.ts rn because of circular dependency
|
// FIXME can't put to utils.ts rn because of circular dependency
|
||||||
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import type { ActionManager } from "../actions/manager";
|
||||||
import {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawElementType,
|
ExcalidrawElementType,
|
||||||
NonDeletedElementsMap,
|
NonDeletedElementsMap,
|
||||||
@@ -17,13 +17,17 @@ import {
|
|||||||
hasStrokeWidth,
|
hasStrokeWidth,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
import { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
||||||
import { capitalizeString, isTransparent } from "../utils";
|
import { capitalizeString, isTransparent } from "../utils";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { hasBoundTextElement, isTextElement } from "../element/typeChecks";
|
import {
|
||||||
|
hasBoundTextElement,
|
||||||
|
isLinearElement,
|
||||||
|
isTextElement,
|
||||||
|
} from "../element/typeChecks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { actionToggleZenMode } from "../actions";
|
import { actionToggleZenMode } from "../actions";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
@@ -114,6 +118,11 @@ export const SelectedShapeActions = ({
|
|||||||
const showLinkIcon =
|
const showLinkIcon =
|
||||||
targetElements.length === 1 || isSingleElementBoundContainer;
|
targetElements.length === 1 || isSingleElementBoundContainer;
|
||||||
|
|
||||||
|
const showLineEditorAction =
|
||||||
|
!appState.editingLinearElement &&
|
||||||
|
targetElements.length === 1 &&
|
||||||
|
isLinearElement(targetElements[0]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
<div>
|
<div>
|
||||||
@@ -149,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")}
|
||||||
@@ -173,8 +180,8 @@ export const SelectedShapeActions = ({
|
|||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
{renderAction("sendToBack")}
|
{renderAction("sendToBack")}
|
||||||
{renderAction("sendBackward")}
|
{renderAction("sendBackward")}
|
||||||
{renderAction("bringToFront")}
|
|
||||||
{renderAction("bringForward")}
|
{renderAction("bringForward")}
|
||||||
|
{renderAction("bringToFront")}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@@ -229,6 +236,7 @@ export const SelectedShapeActions = ({
|
|||||||
{renderAction("group")}
|
{renderAction("group")}
|
||||||
{renderAction("ungroup")}
|
{renderAction("ungroup")}
|
||||||
{showLinkIcon && renderAction("hyperlink")}
|
{showLinkIcon && renderAction("hyperlink")}
|
||||||
|
{showLineEditorAction && renderAction("toggleLinearEditor")}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
)}
|
)}
|
||||||
@@ -333,8 +341,8 @@ export const ShapesSwitcher = ({
|
|||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
fontFamily: "Cascadia, monospace",
|
fontFamily: "Cascadia, monospace",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
background: "pink",
|
background: "var(--color-promo)",
|
||||||
color: "black",
|
color: "var(--color-surface-lowest)",
|
||||||
bottom: 3,
|
bottom: 3,
|
||||||
right: 4,
|
right: 4,
|
||||||
}}
|
}}
|
||||||
@@ -458,6 +466,7 @@ export const ExitZenModeAction = ({
|
|||||||
showExitZenModeBtn: boolean;
|
showExitZenModeBtn: boolean;
|
||||||
}) => (
|
}) => (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className={clsx("disable-zen-mode", {
|
className={clsx("disable-zen-mode", {
|
||||||
"disable-zen-mode--visible": showExitZenModeBtn,
|
"disable-zen-mode--visible": showExitZenModeBtn,
|
||||||
})}
|
})}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,20 +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
|
||||||
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}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export const ButtonSeparator = () => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 1,
|
||||||
|
height: "1rem",
|
||||||
|
backgroundColor: "var(--default-border-color)",
|
||||||
|
margin: "0 auto",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
@@ -22,7 +22,12 @@ export const CheckboxItem: React.FC<{
|
|||||||
).focus();
|
).focus();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button className="Checkbox-box" role="checkbox" aria-checked={checked}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="Checkbox-box"
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={checked}
|
||||||
|
>
|
||||||
{checkIcon}
|
{checkIcon}
|
||||||
</button>
|
</button>
|
||||||
<div className="Checkbox-label">{children}</div>
|
<div className="Checkbox-label">{children}</div>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { getColor } from "./ColorPicker";
|
import { getColor } from "./ColorPicker";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import {
|
import type { ColorPickerType } from "./colorPickerUtils";
|
||||||
ColorPickerType,
|
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
||||||
activeColorPickerSectionAtom,
|
|
||||||
} from "./colorPickerUtils";
|
|
||||||
import { eyeDropperIcon } from "../icons";
|
import { eyeDropperIcon } from "../icons";
|
||||||
import { jotaiScope } from "../../jotai";
|
import { jotaiScope } from "../../jotai";
|
||||||
import { KEYS } from "../../keys";
|
import { KEYS } from "../../keys";
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@include isMobile {
|
@include isMobile {
|
||||||
max-width: 175px;
|
max-width: 11rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import { isInteractive, isTransparent, isWritableElement } from "../../utils";
|
import { isTransparent } from "../../utils";
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
import { 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 {
|
import type { ColorPickerType } from "./colorPickerUtils";
|
||||||
activeColorPickerSectionAtom,
|
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
||||||
ColorPickerType,
|
import { useExcalidrawContainer } from "../App";
|
||||||
} from "./colorPickerUtils";
|
import type { ColorTuple, ColorPaletteCustom } from "../../colors";
|
||||||
import { useDevice, useExcalidrawContainer } from "../App";
|
import { COLOR_PALETTE } from "../../colors";
|
||||||
import { ColorTuple, COLOR_PALETTE, ColorPaletteCustom } 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";
|
||||||
|
|
||||||
@@ -72,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(
|
||||||
@@ -79,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>
|
||||||
@@ -95,6 +94,7 @@ const ColorPickerPopupContent = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const popoverRef = useRef<HTMLDivElement>(null);
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const focusPickerContent = () => {
|
const focusPickerContent = () => {
|
||||||
@@ -104,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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,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}
|
||||||
@@ -269,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) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
|
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
import { ShadeList } from "./ShadeList";
|
import { ShadeList } from "./ShadeList";
|
||||||
|
|
||||||
import PickerColorList from "./PickerColorList";
|
import PickerColorList from "./PickerColorList";
|
||||||
@@ -9,15 +9,15 @@ import { useAtom } from "jotai";
|
|||||||
import { CustomColorList } from "./CustomColorList";
|
import { CustomColorList } from "./CustomColorList";
|
||||||
import { colorPickerKeyNavHandler } from "./keyboardNavHandlers";
|
import { colorPickerKeyNavHandler } from "./keyboardNavHandlers";
|
||||||
import PickerHeading from "./PickerHeading";
|
import PickerHeading from "./PickerHeading";
|
||||||
|
import type { ColorPickerType } from "./colorPickerUtils";
|
||||||
import {
|
import {
|
||||||
ColorPickerType,
|
|
||||||
activeColorPickerSectionAtom,
|
activeColorPickerSectionAtom,
|
||||||
getColorNameAndShadeFromColor,
|
getColorNameAndShadeFromColor,
|
||||||
getMostUsedCustomColors,
|
getMostUsedCustomColors,
|
||||||
isCustomColor,
|
isCustomColor,
|
||||||
} from "./colorPickerUtils";
|
} from "./colorPickerUtils";
|
||||||
|
import type { ColorPaletteCustom } from "../../colors";
|
||||||
import {
|
import {
|
||||||
ColorPaletteCustom,
|
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
|
||||||
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
|
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
|
||||||
} from "../../colors";
|
} from "../../colors";
|
||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import {
|
|||||||
getColorNameAndShadeFromColor,
|
getColorNameAndShadeFromColor,
|
||||||
} from "./colorPickerUtils";
|
} from "./colorPickerUtils";
|
||||||
import HotkeyLabel from "./HotkeyLabel";
|
import HotkeyLabel from "./HotkeyLabel";
|
||||||
import { ColorPaletteCustom } from "../../colors";
|
import type { ColorPaletteCustom } from "../../colors";
|
||||||
import { TranslationKeys, t } from "../../i18n";
|
import type { TranslationKeys } from "../../i18n";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
|
||||||
interface PickerColorListProps {
|
interface PickerColorListProps {
|
||||||
palette: ColorPaletteCustom;
|
palette: ColorPaletteCustom;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
const PickerHeading = ({ children }: { children: ReactNode }) => (
|
const PickerHeading = ({ children }: { children: ReactNode }) => (
|
||||||
<div className="color-picker__heading">{children}</div>
|
<div className="color-picker__heading">{children}</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from "./colorPickerUtils";
|
} from "./colorPickerUtils";
|
||||||
import HotkeyLabel from "./HotkeyLabel";
|
import HotkeyLabel from "./HotkeyLabel";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { ColorPaletteCustom } from "../../colors";
|
import type { ColorPaletteCustom } from "../../colors";
|
||||||
|
|
||||||
interface ShadeListProps {
|
interface ShadeListProps {
|
||||||
hex: string;
|
hex: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ColorPickerType } from "./colorPickerUtils";
|
import type { ColorPickerType } from "./colorPickerUtils";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CANVAS_BACKGROUND_PICKS,
|
DEFAULT_CANVAS_BACKGROUND_PICKS,
|
||||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import {
|
import type { ColorPickerColor, ColorPaletteCustom } from "../../colors";
|
||||||
ColorPickerColor,
|
import { MAX_CUSTOM_COLORS_USED_IN_CANVAS } from "../../colors";
|
||||||
ColorPaletteCustom,
|
|
||||||
MAX_CUSTOM_COLORS_USED_IN_CANVAS,
|
|
||||||
} from "../../colors";
|
|
||||||
|
|
||||||
export const getColorNameAndShadeFromColor = ({
|
export const getColorNameAndShadeFromColor = ({
|
||||||
palette,
|
palette,
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { KEYS } from "../../keys";
|
import { KEYS } from "../../keys";
|
||||||
import {
|
import type {
|
||||||
ColorPickerColor,
|
ColorPickerColor,
|
||||||
ColorPalette,
|
ColorPalette,
|
||||||
ColorPaletteCustom,
|
ColorPaletteCustom,
|
||||||
COLORS_PER_ROW,
|
|
||||||
COLOR_PALETTE,
|
|
||||||
} from "../../colors";
|
} from "../../colors";
|
||||||
import { ValueOf } from "../../utility-types";
|
import { COLORS_PER_ROW, COLOR_PALETTE } from "../../colors";
|
||||||
|
import type { ValueOf } from "../../utility-types";
|
||||||
|
import type { ActiveColorPickerSectionAtomType } from "./colorPickerUtils";
|
||||||
import {
|
import {
|
||||||
ActiveColorPickerSectionAtomType,
|
|
||||||
colorPickerHotkeyBindings,
|
colorPickerHotkeyBindings,
|
||||||
getColorNameAndShadeFromColor,
|
getColorNameAndShadeFromColor,
|
||||||
} from "./colorPickerUtils";
|
} from "./colorPickerUtils";
|
||||||
|
|||||||
@@ -10,12 +10,11 @@ import { Dialog } from "../Dialog";
|
|||||||
import { TextField } from "../TextField";
|
import { TextField } from "../TextField";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { getSelectedElements } from "../../scene";
|
import { getSelectedElements } from "../../scene";
|
||||||
import { Action } from "../../actions/types";
|
import type { Action } from "../../actions/types";
|
||||||
import { TranslationKeys, t } from "../../i18n";
|
import type { TranslationKeys } from "../../i18n";
|
||||||
import {
|
import { t } from "../../i18n";
|
||||||
ShortcutName,
|
import type { ShortcutName } from "../../actions/shortcuts";
|
||||||
getShortcutFromShortcutName,
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
} from "../../actions/shortcuts";
|
|
||||||
import { DEFAULT_SIDEBAR, EVENT } from "../../constants";
|
import { DEFAULT_SIDEBAR, EVENT } from "../../constants";
|
||||||
import {
|
import {
|
||||||
LockedIcon,
|
LockedIcon,
|
||||||
@@ -31,7 +30,7 @@ import {
|
|||||||
} from "../icons";
|
} from "../icons";
|
||||||
import fuzzy from "fuzzy";
|
import fuzzy from "fuzzy";
|
||||||
import { useUIAppState } from "../../context/ui-appState";
|
import { useUIAppState } from "../../context/ui-appState";
|
||||||
import { AppProps, AppState, UIAppState } from "../../types";
|
import type { AppProps, AppState, UIAppState } from "../../types";
|
||||||
import {
|
import {
|
||||||
capitalizeString,
|
capitalizeString,
|
||||||
getShortcutKey,
|
getShortcutKey,
|
||||||
@@ -39,7 +38,7 @@ import {
|
|||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { deburr } from "../../deburr";
|
import { deburr } from "../../deburr";
|
||||||
import { MarkRequired } from "../../utility-types";
|
import type { MarkRequired } from "../../utility-types";
|
||||||
import { InlineIcon } from "../InlineIcon";
|
import { InlineIcon } from "../InlineIcon";
|
||||||
import { SHAPES } from "../../shapes";
|
import { SHAPES } from "../../shapes";
|
||||||
import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
|
import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
|
||||||
@@ -47,7 +46,7 @@ import { useStableCallback } from "../../hooks/useStableCallback";
|
|||||||
import { actionClearCanvas, actionLink } from "../../actions";
|
import { actionClearCanvas, actionLink } from "../../actions";
|
||||||
import { jotaiStore } from "../../jotai";
|
import { jotaiStore } from "../../jotai";
|
||||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||||
import { CommandPaletteItem } from "./types";
|
import type { CommandPaletteItem } from "./types";
|
||||||
import * as defaultItems from "./defaultCommandPaletteItems";
|
import * as defaultItems from "./defaultCommandPaletteItems";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
import { useStable } from "../../hooks/useStable";
|
import { useStable } from "../../hooks/useStable";
|
||||||
@@ -258,10 +257,10 @@ function CommandPaletteInner({
|
|||||||
actionManager.actions.deleteSelectedElements,
|
actionManager.actions.deleteSelectedElements,
|
||||||
actionManager.actions.copyStyles,
|
actionManager.actions.copyStyles,
|
||||||
actionManager.actions.pasteStyles,
|
actionManager.actions.pasteStyles,
|
||||||
|
actionManager.actions.bringToFront,
|
||||||
|
actionManager.actions.bringForward,
|
||||||
actionManager.actions.sendBackward,
|
actionManager.actions.sendBackward,
|
||||||
actionManager.actions.sendToBack,
|
actionManager.actions.sendToBack,
|
||||||
actionManager.actions.bringForward,
|
|
||||||
actionManager.actions.bringToFront,
|
|
||||||
actionManager.actions.alignTop,
|
actionManager.actions.alignTop,
|
||||||
actionManager.actions.alignBottom,
|
actionManager.actions.alignBottom,
|
||||||
actionManager.actions.alignLeft,
|
actionManager.actions.alignLeft,
|
||||||
@@ -541,7 +540,7 @@ function CommandPaletteInner({
|
|||||||
...command,
|
...command,
|
||||||
icon: command.icon || boltIcon,
|
icon: command.icon || boltIcon,
|
||||||
order: command.order ?? getCategoryOrder(command.category),
|
order: command.order ?? getCategoryOrder(command.category),
|
||||||
haystack: `${deburr(command.label)} ${
|
haystack: `${deburr(command.label.toLocaleLowerCase())} ${
|
||||||
command.keywords?.join(" ") || ""
|
command.keywords?.join(" ") || ""
|
||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
@@ -778,7 +777,9 @@ function CommandPaletteInner({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _query = deburr(commandSearch.replace(/[<>-_| ]/g, ""));
|
const _query = deburr(
|
||||||
|
commandSearch.toLocaleLowerCase().replace(/[<>_| -]/g, ""),
|
||||||
|
);
|
||||||
matchingCommands = fuzzy
|
matchingCommands = fuzzy
|
||||||
.filter(_query, matchingCommands, {
|
.filter(_query, matchingCommands, {
|
||||||
extract: (command) => command.haystack,
|
extract: (command) => command.haystack,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user