Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
5639bb8e87 | |||
![]() |
712f267519 | ||
![]() |
41a7613dff | ||
![]() |
95d89a751a | ||
![]() |
6b5fb30d69 | ||
![]() |
d92a849038 | ||
![]() |
0a534f1bc6 | ||
![]() |
4ca5f53b1f | ||
![]() |
f7dcc893ea | ||
![]() |
4dfb8a3f8e | ||
![]() |
298812e1d0 | ||
![]() |
35bb449a4b | ||
![]() |
c4c064982f | ||
![]() |
51dbd4831b | ||
![]() |
7e41026812 | ||
![]() |
a8ebe514da | ||
![]() |
a30e1b25c6 | ||
![]() |
ff2ed5d26a | ||
![]() |
e058a08b33 | ||
![]() |
a306a909a0 | ||
![]() |
3dc54a724a | ||
![]() |
a7c61319dd | ||
![]() |
cec5232a7a | ||
![]() |
d4f70e9f31 | ||
![]() |
e19fd1332a | ||
![]() |
6e655cdb24 | ||
![]() |
192c4e7658 | ||
![]() |
195a743874 | ||
![]() |
4a60fe3d22 | ||
![]() |
2a0d15799c | ||
![]() |
a18b139a60 | ||
![]() |
1913599594 | ||
![]() |
debf2ad608 | ||
![]() |
8fb2f70414 | ||
![]() |
5fc13e4309 | ||
![]() |
b5d60973b7 | ||
![]() |
a5d6939826 | ||
![]() |
0cf36d6b30 | ||
![]() |
58f7d33d80 | ||
![]() |
6fe7de8020 | ||
![]() |
01304aac49 | ||
![]() |
dff69e9191 | ||
![]() |
6fc85022ae | ||
![]() |
e48b63a0ae | ||
![]() |
c2caf78e95 | ||
![]() |
ce267aa0d3 | ||
![]() |
6e47fadb59 | ||
![]() |
b3d5ba0567 | ||
![]() |
c79e892e55 | ||
![]() |
57a9e301d4 | ||
![]() |
7c58477382 | ||
![]() |
83fac6d0db | ||
![]() |
f2e8404c7b | ||
![]() |
d797c2e210 | ||
![]() |
0cd5a259ae | ||
![]() |
432a46ef9e | ||
![]() |
a18f059188 | ||
![]() |
ab89d4c16f | ||
![]() |
6c3a434f2a | ||
![]() |
e1bb59fb8f | ||
![]() |
77aca48c84 | ||
![]() |
58990b41ae | ||
![]() |
99d8bff175 | ||
![]() |
30983d801a | ||
![]() |
21ffaf4d76 | ||
![]() |
82b9a6b464 |
@ -48,3 +48,6 @@ UNWEjuqNMi/lwAErS9fFa2oJlWyT8U7zzv/5kQREkxZI6y9v0AF3qcbsy2731FnD
|
||||
s9ChJvOUW9toIab2gsIdrKW8ZNpu084ZFVKb6LNjvIXI1Se4oMTHeszXzNptzlot
|
||||
kdxxjOoaQMAyfljFSot1F1FlU6MQlag7UnFGvFjRHN1JI5q4K+n3a67DX+TMyRqS
|
||||
HQIDAQAB'
|
||||
|
||||
# set to true in .env.development.local to disable the prevent unload dialog
|
||||
VITE_APP_DISABLE_PREVENT_UNLOAD=
|
||||
|
@ -1,5 +1,5 @@
|
||||
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
||||
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||
VITE_APP_BACKEND_V2_GET_URL=https://ex.dylanbanta.com/api/v2/scenes/
|
||||
VITE_APP_BACKEND_V2_POST_URL=https://ex.dylanbanta.com/api/v2/scenes/
|
||||
|
||||
VITE_APP_LIBRARY_URL=https://libraries.excalidraw.com
|
||||
VITE_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
|
||||
|
@ -1,6 +1,21 @@
|
||||
{
|
||||
"extends": ["@excalidraw/eslint-config", "react-app"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
"groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@excalidraw/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
}
|
||||
],
|
||||
"newlines-between": "always-and-inside-groups",
|
||||
"warnOnUnassignedImports": true
|
||||
}
|
||||
],
|
||||
"import/no-anonymous-default-export": "off",
|
||||
"no-restricted-globals": "off",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
@ -17,6 +32,12 @@
|
||||
"name": "jotai",
|
||||
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
||||
}
|
||||
],
|
||||
"react/jsx-no-target-blank": [
|
||||
"error",
|
||||
{
|
||||
"allowReferrer": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ The Excalidraw editor (npm package) supports:
|
||||
- 🏗️ Customizable.
|
||||
- 📷 Image support.
|
||||
- 😀 Shape libraries support.
|
||||
- 👅 Localization (i18n) support.
|
||||
- 🌐 Localization (i18n) support.
|
||||
- 🖼️ Export to PNG, SVG & clipboard.
|
||||
- 💾 Open format - export drawings as an `.excalidraw` json file.
|
||||
- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
||||
|
||||
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should a valid React Node.
|
||||
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should be a valid React Node.
|
||||
|
||||
**Usage**
|
||||
|
||||
@ -25,7 +25,7 @@ function App() {
|
||||
}
|
||||
```
|
||||
|
||||
This will only for `Desktop` devices.
|
||||
This will only work for `Desktop` devices.
|
||||
|
||||
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
||||
|
||||
@ -65,4 +65,4 @@ const App = () => (
|
||||
// Need to render when code is span across multiple components
|
||||
// in Live Code blocks editor
|
||||
render(<App />);
|
||||
```
|
||||
```
|
||||
|
@ -3,7 +3,7 @@
|
||||
All `props` are _optional_.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| --- | --- | --- | --- |
|
||||
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
||||
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
|
||||
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
|
||||
@ -13,7 +13,7 @@ All `props` are _optional_.
|
||||
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
|
||||
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene |
|
||||
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||
| [`generateLinkForSelection`](#generateLinkForSelection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
|
||||
| [`generateLinkForSelection`](#generatelinkforselection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
|
||||
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
|
||||
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
||||
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
|
||||
@ -29,8 +29,9 @@ All `props` are _optional_.
|
||||
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
|
||||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
||||
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||
| [`renderScrollbars`] | `boolean`| | `false` | Indicates whether scrollbars will be shown
|
||||
|
||||
### Storing custom data on Excalidraw elements
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
const FeatureList = [
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
type FeatureItem = {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import Layout from "@theme/Layout";
|
||||
import Link from "@docusaurus/Link";
|
||||
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
||||
import styles from "./index.module.css";
|
||||
import HomepageFeatures from "@site/src/components/Homepage";
|
||||
import Layout from "@theme/Layout";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import styles from "./index.module.css";
|
||||
|
||||
function HomepageHeader() {
|
||||
const { siteConfig } = useDocusaurusContext();
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Import the original mapper
|
||||
import MDXComponents from "@theme-original/MDXComponents";
|
||||
import Highlight from "@site/src/components/Highlight";
|
||||
import MDXComponents from "@theme-original/MDXComponents";
|
||||
|
||||
export default {
|
||||
// Re-use the default mapping
|
||||
|
@ -19,7 +19,7 @@ services:
|
||||
- ./:/opt/node_app/app:delegated
|
||||
- ./package.json:/opt/node_app/package.json
|
||||
- ./yarn.lock:/opt/node_app/yarn.lock
|
||||
- notused:/opt/node_app/app/node_modules
|
||||
# - notused:/opt/node_app/app/node_modules
|
||||
|
||||
volumes:
|
||||
notused:
|
||||
# volumes:
|
||||
# notused:
|
||||
|
@ -1,5 +1,6 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import Script from "next/script";
|
||||
|
||||
import "../common.scss";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||
|
@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
import * as excalidrawLib from "@excalidraw/excalidraw";
|
||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
import App from "../../with-script-in-browser/components/ExampleApp";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
import App from "../../with-script-in-browser/components/ExampleApp";
|
||||
|
||||
const ExcalidrawWrapper: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import "../common.scss";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||
|
||||
|
@ -52,7 +52,7 @@
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.excalidraw .panelColumn {
|
||||
.excalidraw .selected-shape-actions {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
@ -6,13 +7,24 @@ import React, {
|
||||
Children,
|
||||
cloneElement,
|
||||
} from "react";
|
||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
||||
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
|
||||
import type {
|
||||
NonDeletedExcalidrawElement,
|
||||
Theme,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
ExcalidrawImperativeAPI,
|
||||
ExcalidrawInitialDataState,
|
||||
Gesture,
|
||||
LibraryItems,
|
||||
PointerDownState as ExcalidrawPointerDownState,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import type { ResolvablePromise } from "../utils";
|
||||
import initialData from "../initialData";
|
||||
import {
|
||||
resolvablePromise,
|
||||
distance2d,
|
||||
@ -23,25 +35,12 @@ import {
|
||||
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import MobileFooter from "./MobileFooter";
|
||||
import initialData from "../initialData";
|
||||
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
ExcalidrawImperativeAPI,
|
||||
ExcalidrawInitialDataState,
|
||||
Gesture,
|
||||
LibraryItems,
|
||||
PointerDownState as ExcalidrawPointerDownState,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import type {
|
||||
NonDeletedExcalidrawElement,
|
||||
Theme,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
|
||||
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
||||
|
||||
import "./ExampleApp.scss";
|
||||
|
||||
import type { ResolvablePromise } from "../utils";
|
||||
|
||||
type Comment = {
|
||||
x: number;
|
||||
y: number;
|
||||
@ -105,6 +104,7 @@ export default function ExampleApp({
|
||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||
const [renderScrollbars, setRenderScrollbars] = useState(false);
|
||||
const [blobUrl, setBlobUrl] = useState<string>("");
|
||||
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||
@ -193,6 +193,7 @@ export default function ExampleApp({
|
||||
}) => setPointerData(payload),
|
||||
viewModeEnabled,
|
||||
zenModeEnabled,
|
||||
renderScrollbars,
|
||||
gridModeEnabled,
|
||||
theme,
|
||||
name: "Custom name of drawing",
|
||||
@ -711,6 +712,14 @@ export default function ExampleApp({
|
||||
/>
|
||||
Grid mode
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={renderScrollbars}
|
||||
onChange={() => setRenderScrollbars(!renderScrollbars)}
|
||||
/>
|
||||
Render scrollbars
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React from "react";
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||
import CustomFooter from "./CustomFooter";
|
||||
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import CustomFooter from "./CustomFooter";
|
||||
|
||||
const MobileFooter = ({
|
||||
excalidrawAPI,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import "./ExampleSidebar.scss";
|
||||
|
||||
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import App from "./components/ExampleApp";
|
||||
import React, { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
import type * as TExcalidraw from "@excalidraw/excalidraw";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
import App from "./components/ExampleApp";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -15,7 +15,8 @@
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"build:preview": "yarn build && vite preview --port 5002",
|
||||
"preview": "vite preview --port 5002",
|
||||
"build:preview": "yarn build && yarn preview",
|
||||
"build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { unstable_batchedUpdates } from "react-dom";
|
||||
import { fileOpen as _fileOpen } from "browser-fs-access";
|
||||
import { MIME_TYPES } from "@excalidraw/excalidraw";
|
||||
import { fileOpen as _fileOpen } from "browser-fs-access";
|
||||
import { unstable_batchedUpdates } from "react-dom";
|
||||
|
||||
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||
|
||||
|
@ -1,24 +1,3 @@
|
||||
import polyfill from "@excalidraw/excalidraw/polyfill";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
|
||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
||||
import { TopErrorBoundary } from "./components/TopErrorBoundary";
|
||||
import {
|
||||
APP_NAME,
|
||||
EVENT,
|
||||
THEME,
|
||||
TITLE_TIMEOUT,
|
||||
VERSION_TIMEOUT,
|
||||
} from "@excalidraw/excalidraw/constants";
|
||||
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
|
||||
import type {
|
||||
FileId,
|
||||
NonDeletedExcalidrawElement,
|
||||
OrderedExcalidrawElement,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
import {
|
||||
Excalidraw,
|
||||
LiveCollaborationTrigger,
|
||||
@ -26,15 +5,23 @@ import {
|
||||
CaptureUpdateAction,
|
||||
reconcileElements,
|
||||
} from "@excalidraw/excalidraw";
|
||||
import type {
|
||||
AppState,
|
||||
ExcalidrawImperativeAPI,
|
||||
BinaryFiles,
|
||||
ExcalidrawInitialDataState,
|
||||
UIAppState,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import type { ResolvablePromise } from "@excalidraw/excalidraw/utils";
|
||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
|
||||
import {
|
||||
CommandPalette,
|
||||
DEFAULT_CATEGORIES,
|
||||
} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";
|
||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
||||
import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
|
||||
import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
|
||||
import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";
|
||||
import Trans from "@excalidraw/excalidraw/components/Trans";
|
||||
import {
|
||||
APP_NAME,
|
||||
EVENT,
|
||||
THEME,
|
||||
TITLE_TIMEOUT,
|
||||
VERSION_TIMEOUT,
|
||||
debounce,
|
||||
getVersion,
|
||||
getFrame,
|
||||
@ -42,75 +29,14 @@ import {
|
||||
preventUnload,
|
||||
resolvablePromise,
|
||||
isRunningInIframe,
|
||||
} from "@excalidraw/excalidraw/utils";
|
||||
import {
|
||||
FIREBASE_STORAGE_PREFIXES,
|
||||
isExcalidrawPlusSignedUser,
|
||||
STORAGE_KEYS,
|
||||
SYNC_BROWSER_TABS_TIMEOUT,
|
||||
} from "./app_constants";
|
||||
import type { CollabAPI } from "./collab/Collab";
|
||||
import Collab, {
|
||||
collabAPIAtom,
|
||||
isCollaboratingAtom,
|
||||
isOfflineAtom,
|
||||
} from "./collab/Collab";
|
||||
import {
|
||||
exportToBackend,
|
||||
getCollaborationLinkData,
|
||||
isCollaborationLink,
|
||||
loadScene,
|
||||
} from "./data";
|
||||
import {
|
||||
importFromLocalStorage,
|
||||
importUsernameFromLocalStorage,
|
||||
} from "./data/localStorage";
|
||||
import CustomStats from "./CustomStats";
|
||||
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
|
||||
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
||||
import {
|
||||
ExportToExcalidrawPlus,
|
||||
exportToExcalidrawPlus,
|
||||
} from "./components/ExportToExcalidrawPlus";
|
||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
||||
import { loadFilesFromFirebase } from "./data/firebase";
|
||||
import {
|
||||
LibraryIndexedDBAdapter,
|
||||
LibraryLocalStorageMigrationAdapter,
|
||||
LocalData,
|
||||
} from "./data/LocalData";
|
||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
parseLibraryTokensFromUrl,
|
||||
useHandleLibrary,
|
||||
} from "@excalidraw/excalidraw/data/library";
|
||||
import { AppMainMenu } from "./components/AppMainMenu";
|
||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||
import { AppFooter } from "./components/AppFooter";
|
||||
import {
|
||||
Provider,
|
||||
useAtom,
|
||||
useAtomValue,
|
||||
useAtomWithInitialValue,
|
||||
appJotaiStore,
|
||||
} from "./app-jotai";
|
||||
isDevEnv,
|
||||
} from "@excalidraw/common";
|
||||
import polyfill from "@excalidraw/excalidraw/polyfill";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
|
||||
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
|
||||
import "./index.scss";
|
||||
import type { ResolutionType } from "@excalidraw/excalidraw/utility-types";
|
||||
import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";
|
||||
import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
|
||||
import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
|
||||
import Trans from "@excalidraw/excalidraw/components/Trans";
|
||||
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
|
||||
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
|
||||
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
||||
import {
|
||||
CommandPalette,
|
||||
DEFAULT_CATEGORIES,
|
||||
} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";
|
||||
import {
|
||||
GithubIcon,
|
||||
XBrandIcon,
|
||||
@ -121,6 +47,83 @@ import {
|
||||
share,
|
||||
youtubeIcon,
|
||||
} from "@excalidraw/excalidraw/components/icons";
|
||||
import { isElementLink } from "@excalidraw/element";
|
||||
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
||||
import { newElementWith } from "@excalidraw/element";
|
||||
import { isInitializedImageElement } from "@excalidraw/element";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
parseLibraryTokensFromUrl,
|
||||
useHandleLibrary,
|
||||
} from "@excalidraw/excalidraw/data/library";
|
||||
|
||||
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
||||
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
|
||||
import type {
|
||||
FileId,
|
||||
NonDeletedExcalidrawElement,
|
||||
OrderedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
import type {
|
||||
AppState,
|
||||
ExcalidrawImperativeAPI,
|
||||
BinaryFiles,
|
||||
ExcalidrawInitialDataState,
|
||||
UIAppState,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import type { ResolutionType } from "@excalidraw/common/utility-types";
|
||||
import type { ResolvablePromise } from "@excalidraw/common/utils";
|
||||
|
||||
import CustomStats from "./CustomStats";
|
||||
import {
|
||||
Provider,
|
||||
useAtom,
|
||||
useAtomValue,
|
||||
useAtomWithInitialValue,
|
||||
appJotaiStore,
|
||||
} from "./app-jotai";
|
||||
import {
|
||||
FIREBASE_STORAGE_PREFIXES,
|
||||
isExcalidrawPlusSignedUser,
|
||||
STORAGE_KEYS,
|
||||
SYNC_BROWSER_TABS_TIMEOUT,
|
||||
} from "./app_constants";
|
||||
import Collab, {
|
||||
collabAPIAtom,
|
||||
isCollaboratingAtom,
|
||||
isOfflineAtom,
|
||||
} from "./collab/Collab";
|
||||
import { AppFooter } from "./components/AppFooter";
|
||||
import { AppMainMenu } from "./components/AppMainMenu";
|
||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||
import {
|
||||
ExportToExcalidrawPlus,
|
||||
exportToExcalidrawPlus,
|
||||
} from "./components/ExportToExcalidrawPlus";
|
||||
import { TopErrorBoundary } from "./components/TopErrorBoundary";
|
||||
|
||||
import {
|
||||
exportToBackend,
|
||||
getCollaborationLinkData,
|
||||
isCollaborationLink,
|
||||
loadScene,
|
||||
} from "./data";
|
||||
|
||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||
import {
|
||||
importFromLocalStorage,
|
||||
importUsernameFromLocalStorage,
|
||||
} from "./data/localStorage";
|
||||
|
||||
import { loadFilesFromFirebase } from "./data/firebase";
|
||||
import {
|
||||
LibraryIndexedDBAdapter,
|
||||
LibraryLocalStorageMigrationAdapter,
|
||||
LocalData,
|
||||
} from "./data/LocalData";
|
||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
|
||||
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
|
||||
import { useHandleAppTheme } from "./useHandleAppTheme";
|
||||
import { getPreferredLanguage } from "./app-language/language-detector";
|
||||
import { useAppLangCode } from "./app-language/language-state";
|
||||
@ -131,7 +134,10 @@ import DebugCanvas, {
|
||||
} from "./components/DebugCanvas";
|
||||
import { AIComponents } from "./components/AI";
|
||||
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
|
||||
import { isElementLink } from "@excalidraw/excalidraw/element/elementLink";
|
||||
|
||||
import "./index.scss";
|
||||
|
||||
import type { CollabAPI } from "./collab/Collab";
|
||||
|
||||
polyfill();
|
||||
|
||||
@ -377,7 +383,7 @@ const ExcalidrawWrapper = () => {
|
||||
const [, forceRefresh] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (import.meta.env.DEV) {
|
||||
if (isDevEnv()) {
|
||||
const debugState = loadSavedDebugState();
|
||||
|
||||
if (debugState.enabled && !window.visualDebug) {
|
||||
@ -602,7 +608,13 @@ const ExcalidrawWrapper = () => {
|
||||
excalidrawAPI.getSceneElements(),
|
||||
)
|
||||
) {
|
||||
preventUnload(event);
|
||||
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
|
||||
preventUnload(event);
|
||||
} else {
|
||||
console.warn(
|
||||
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
||||
@ -914,16 +926,21 @@ const ExcalidrawWrapper = () => {
|
||||
<ShareDialog
|
||||
collabAPI={collabAPI}
|
||||
onExportToBackend={async () => {
|
||||
if (excalidrawAPI) {
|
||||
try {
|
||||
await onExportToBackend(
|
||||
excalidrawAPI.getSceneElements(),
|
||||
excalidrawAPI.getAppState(),
|
||||
excalidrawAPI.getFiles(),
|
||||
);
|
||||
} catch (error: any) {
|
||||
setErrorMessage(error.message);
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { url, errorMessage } = await exportToBackend(
|
||||
excalidrawAPI.getSceneElements(),
|
||||
excalidrawAPI.getAppState(),
|
||||
excalidrawAPI.getFiles(),
|
||||
);
|
||||
if (errorMessage) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
setLatestShareableLink(url);
|
||||
} catch (error: any) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -1,15 +1,21 @@
|
||||
import { Stats } from "@excalidraw/excalidraw";
|
||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
||||
import {
|
||||
DEFAULT_VERSION,
|
||||
debounce,
|
||||
getVersion,
|
||||
nFormatter,
|
||||
} from "@excalidraw/common";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
import { useEffect, useState } from "react";
|
||||
import { debounce, getVersion, nFormatter } from "@excalidraw/excalidraw/utils";
|
||||
|
||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||
import type { UIAppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import {
|
||||
getElementsStorageSize,
|
||||
getTotalStorageSize,
|
||||
} from "./data/localStorage";
|
||||
import { DEFAULT_VERSION } from "@excalidraw/excalidraw/constants";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
||||
import type { UIAppState } from "@excalidraw/excalidraw/types";
|
||||
import { Stats } from "@excalidraw/excalidraw";
|
||||
|
||||
type StorageSizes = { scene: number; total: number };
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { base64urlToString } from "@excalidraw/excalidraw/data/encode";
|
||||
import { ExcalidrawError } from "@excalidraw/excalidraw/errors";
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import { STORAGE_KEYS } from "./app_constants";
|
||||
import { LocalData } from "./data/LocalData";
|
||||
|
||||
import type {
|
||||
FileId,
|
||||
OrderedExcalidrawElement,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
} from "@excalidraw/element/types";
|
||||
import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types";
|
||||
import { ExcalidrawError } from "@excalidraw/excalidraw/errors";
|
||||
import { base64urlToString } from "@excalidraw/excalidraw/data/encode";
|
||||
|
||||
import { STORAGE_KEYS } from "./app_constants";
|
||||
import { LocalData } from "./data/LocalData";
|
||||
|
||||
const EVENT_REQUEST_SCENE = "REQUEST_SCENE";
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React from "react";
|
||||
import { useI18n, languages } from "@excalidraw/excalidraw/i18n";
|
||||
import React from "react";
|
||||
|
||||
import { useSetAtom } from "../app-jotai";
|
||||
|
||||
import { appLangCodeAtom } from "./language-state";
|
||||
|
||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import { defaultLang, languages } from "@excalidraw/excalidraw";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
export const languageDetector = new LanguageDetector();
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { atom, useAtom } from "../app-jotai";
|
||||
|
||||
import { getPreferredLanguage, languageDetector } from "./language-detector";
|
||||
|
||||
export const appLangCodeAtom = atom(getPreferredLanguage());
|
||||
|
@ -1,21 +1,3 @@
|
||||
import throttle from "lodash.throttle";
|
||||
import { PureComponent } from "react";
|
||||
import type {
|
||||
BinaryFileData,
|
||||
ExcalidrawImperativeAPI,
|
||||
SocketId,
|
||||
Collaborator,
|
||||
Gesture,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
||||
import { APP_NAME, ENV, EVENT } from "@excalidraw/excalidraw/constants";
|
||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
InitializedExcalidrawImageElement,
|
||||
OrderedExcalidrawElement,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
import {
|
||||
CaptureUpdateAction,
|
||||
getSceneVersion,
|
||||
@ -23,12 +5,51 @@ import {
|
||||
zoomToFitBounds,
|
||||
reconcileElements,
|
||||
} from "@excalidraw/excalidraw";
|
||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
||||
import { APP_NAME, EVENT } from "@excalidraw/common";
|
||||
import {
|
||||
IDLE_THRESHOLD,
|
||||
ACTIVE_THRESHOLD,
|
||||
UserIdleState,
|
||||
assertNever,
|
||||
isDevEnv,
|
||||
isTestEnv,
|
||||
preventUnload,
|
||||
resolvablePromise,
|
||||
throttleRAF,
|
||||
} from "@excalidraw/excalidraw/utils";
|
||||
} from "@excalidraw/common";
|
||||
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||
import { getVisibleSceneBounds } from "@excalidraw/element";
|
||||
import { newElementWith } from "@excalidraw/element";
|
||||
import { isImageElement, isInitializedImageElement } from "@excalidraw/element";
|
||||
import { AbortError } from "@excalidraw/excalidraw/errors";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
|
||||
|
||||
import throttle from "lodash.throttle";
|
||||
import { PureComponent } from "react";
|
||||
|
||||
import type {
|
||||
ReconciledExcalidrawElement,
|
||||
RemoteExcalidrawElement,
|
||||
} from "@excalidraw/excalidraw/data/reconcile";
|
||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
InitializedExcalidrawImageElement,
|
||||
OrderedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
import type {
|
||||
BinaryFileData,
|
||||
ExcalidrawImperativeAPI,
|
||||
SocketId,
|
||||
Collaborator,
|
||||
Gesture,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { appJotaiStore, atom } from "../app-jotai";
|
||||
import {
|
||||
CURSOR_SYNC_TIMEOUT,
|
||||
FILE_UPLOAD_MAX_BYTES,
|
||||
@ -39,15 +60,17 @@ import {
|
||||
SYNC_FULL_SCENE_INTERVAL_MS,
|
||||
WS_EVENTS,
|
||||
} from "../app_constants";
|
||||
import type {
|
||||
SocketUpdateDataSource,
|
||||
SyncableExcalidrawElement,
|
||||
} from "../data";
|
||||
import {
|
||||
generateCollaborationLinkData,
|
||||
getCollaborationLink,
|
||||
getSyncableElements,
|
||||
} from "../data";
|
||||
import {
|
||||
encodeFilesForUpload,
|
||||
FileManager,
|
||||
updateStaleImageStatuses,
|
||||
} from "../data/FileManager";
|
||||
import { LocalData } from "../data/LocalData";
|
||||
import {
|
||||
isSavedToFirebase,
|
||||
loadFilesFromFirebase,
|
||||
@ -59,36 +82,15 @@ import {
|
||||
importUsernameFromLocalStorage,
|
||||
saveUsernameToLocalStorage,
|
||||
} from "../data/localStorage";
|
||||
import Portal from "./Portal";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
import {
|
||||
IDLE_THRESHOLD,
|
||||
ACTIVE_THRESHOLD,
|
||||
UserIdleState,
|
||||
} from "@excalidraw/excalidraw/constants";
|
||||
import {
|
||||
encodeFilesForUpload,
|
||||
FileManager,
|
||||
updateStaleImageStatuses,
|
||||
} from "../data/FileManager";
|
||||
import { AbortError } from "@excalidraw/excalidraw/errors";
|
||||
import {
|
||||
isImageElement,
|
||||
isInitializedImageElement,
|
||||
} from "@excalidraw/excalidraw/element/typeChecks";
|
||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
||||
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||
import { LocalData } from "../data/LocalData";
|
||||
import { appJotaiStore, atom } from "../app-jotai";
|
||||
import type { Mutable, ValueOf } from "@excalidraw/excalidraw/utility-types";
|
||||
import { getVisibleSceneBounds } from "@excalidraw/excalidraw/element/bounds";
|
||||
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
|
||||
|
||||
import { collabErrorIndicatorAtom } from "./CollabError";
|
||||
import Portal from "./Portal";
|
||||
|
||||
import type {
|
||||
ReconciledExcalidrawElement,
|
||||
RemoteExcalidrawElement,
|
||||
} from "@excalidraw/excalidraw/data/reconcile";
|
||||
SocketUpdateDataSource,
|
||||
SyncableExcalidrawElement,
|
||||
} from "../data";
|
||||
|
||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||
export const isCollaboratingAtom = atom(false);
|
||||
@ -236,7 +238,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
||||
|
||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||
|
||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
||||
if (isTestEnv() || isDevEnv()) {
|
||||
window.collab = window.collab || ({} as Window["collab"]);
|
||||
Object.defineProperties(window, {
|
||||
collab: {
|
||||
@ -296,7 +298,13 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
||||
// the purpose is to run in immediately after user decides to stay
|
||||
this.saveCollabRoomToFirebase(syncableElements);
|
||||
|
||||
preventUnload(event);
|
||||
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
|
||||
preventUnload(event);
|
||||
} else {
|
||||
console.warn(
|
||||
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1009,7 +1017,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
||||
if (isTestEnv() || isDevEnv()) {
|
||||
window.collab = window.collab || ({} as Window["collab"]);
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
|
||||
import { warning } from "@excalidraw/excalidraw/components/icons";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { atom } from "../app-jotai";
|
||||
|
||||
import "./CollabError.scss";
|
||||
|
@ -1,25 +1,26 @@
|
||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||
import { newElementWith } from "@excalidraw/element";
|
||||
import throttle from "lodash.throttle";
|
||||
|
||||
import type { UserIdleState } from "@excalidraw/common";
|
||||
import type { OrderedExcalidrawElement } from "@excalidraw/element/types";
|
||||
import type {
|
||||
OnUserFollowedPayload,
|
||||
SocketId,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
||||
import { isSyncableElement } from "../data";
|
||||
|
||||
import type {
|
||||
SocketUpdateData,
|
||||
SocketUpdateDataSource,
|
||||
SyncableExcalidrawElement,
|
||||
} from "../data";
|
||||
import { isSyncableElement } from "../data";
|
||||
|
||||
import type { TCollabClass } from "./Collab";
|
||||
|
||||
import type { OrderedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
||||
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
|
||||
import type {
|
||||
OnUserFollowedPayload,
|
||||
SocketId,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import type { UserIdleState } from "@excalidraw/excalidraw/constants";
|
||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||
import throttle from "lodash.throttle";
|
||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
||||
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||
import type { Socket } from "socket.io-client";
|
||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||
|
||||
class Portal {
|
||||
collab: TCollabClass;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||
import {
|
||||
DiagramToCodePlugin,
|
||||
exportToBlob,
|
||||
@ -7,7 +6,9 @@ import {
|
||||
TTDDialog,
|
||||
} from "@excalidraw/excalidraw";
|
||||
import { getDataURL } from "@excalidraw/excalidraw/data/blob";
|
||||
import { safelyParseJSON } from "@excalidraw/excalidraw/utils";
|
||||
import { safelyParseJSON } from "@excalidraw/common";
|
||||
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||
|
||||
export const AIComponents = ({
|
||||
excalidrawAPI,
|
||||
@ -72,7 +73,7 @@ export const AIComponents = ({
|
||||
</br>
|
||||
<div>You can also try <a href="${
|
||||
import.meta.env.VITE_APP_PLUS_LP
|
||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noreferrer noopener">Excalidraw+</a> to get more requests.</div>
|
||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noopener">Excalidraw+</a> to get more requests.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
import { Footer } from "@excalidraw/excalidraw/index";
|
||||
import React from "react";
|
||||
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
|
||||
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
|
||||
import { EncryptedIcon } from "./EncryptedIcon";
|
||||
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
|
||||
|
||||
export const AppFooter = React.memo(
|
||||
({ onChange }: { onChange: () => void }) => {
|
||||
|
@ -1,13 +1,18 @@
|
||||
import React from "react";
|
||||
import {
|
||||
loginIcon,
|
||||
ExcalLogo,
|
||||
eyeIcon,
|
||||
} from "@excalidraw/excalidraw/components/icons";
|
||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
||||
import { MainMenu } from "@excalidraw/excalidraw/index";
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
import React from "react";
|
||||
|
||||
import { isDevEnv } from "@excalidraw/common";
|
||||
|
||||
import type { Theme } from "@excalidraw/element/types";
|
||||
|
||||
import { LanguageList } from "../app-language/LanguageList";
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
|
||||
import { saveDebugState } from "./DebugCanvas";
|
||||
|
||||
export const AppMainMenu: React.FC<{
|
||||
@ -54,7 +59,7 @@ export const AppMainMenu: React.FC<{
|
||||
>
|
||||
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
||||
</MainMenu.ItemLink>
|
||||
{import.meta.env.DEV && (
|
||||
{isDevEnv() && (
|
||||
<MainMenu.Item
|
||||
icon={eyeIcon}
|
||||
onClick={() => {
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React from "react";
|
||||
import { loginIcon } from "@excalidraw/excalidraw/components/icons";
|
||||
import { POINTER_EVENTS } from "@excalidraw/common";
|
||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||
import { WelcomeScreen } from "@excalidraw/excalidraw/index";
|
||||
import React from "react";
|
||||
|
||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||
import { POINTER_EVENTS } from "@excalidraw/excalidraw/constants";
|
||||
|
||||
export const AppWelcomeScreen: React.FC<{
|
||||
onCollabDialogOpen: () => any;
|
||||
|
@ -1,24 +1,28 @@
|
||||
import { useCallback, useImperativeHandle, useRef } from "react";
|
||||
import { type AppState } from "@excalidraw/excalidraw/types";
|
||||
import { throttleRAF } from "@excalidraw/excalidraw/utils";
|
||||
import {
|
||||
bootstrapCanvas,
|
||||
getNormalizedCanvasDimensions,
|
||||
} from "@excalidraw/excalidraw/renderer/helpers";
|
||||
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
|
||||
import {
|
||||
ArrowheadArrowIcon,
|
||||
CloseIcon,
|
||||
TrashIcon,
|
||||
} from "@excalidraw/excalidraw/components/icons";
|
||||
import { STORAGE_KEYS } from "../app_constants";
|
||||
import type { Curve } from "../../packages/math";
|
||||
import {
|
||||
bootstrapCanvas,
|
||||
getNormalizedCanvasDimensions,
|
||||
} from "@excalidraw/excalidraw/renderer/helpers";
|
||||
import { type AppState } from "@excalidraw/excalidraw/types";
|
||||
import { throttleRAF } from "@excalidraw/common";
|
||||
import { useCallback, useImperativeHandle, useRef } from "react";
|
||||
|
||||
import {
|
||||
isLineSegment,
|
||||
type GlobalPoint,
|
||||
type LineSegment,
|
||||
} from "../../packages/math";
|
||||
import { isCurve } from "../../packages/math/curve";
|
||||
} from "@excalidraw/math";
|
||||
import { isCurve } from "@excalidraw/math/curve";
|
||||
|
||||
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
|
||||
|
||||
import type { Curve } from "@excalidraw/math";
|
||||
|
||||
import { STORAGE_KEYS } from "../app_constants";
|
||||
|
||||
const renderLine = (
|
||||
context: CanvasRenderingContext2D,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { shield } from "@excalidraw/excalidraw/components/icons";
|
||||
import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
|
||||
import { shield } from "@excalidraw/excalidraw/components/icons";
|
||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||
|
||||
export const EncryptedIcon = () => {
|
||||
@ -10,7 +10,7 @@ export const EncryptedIcon = () => {
|
||||
className="encrypted-icon tooltip"
|
||||
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
aria-label={t("encrypted.link")}
|
||||
>
|
||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||
|
@ -10,7 +10,7 @@ export const ExcalidrawPlusAppLink = () => {
|
||||
import.meta.env.VITE_APP_PLUS_APP
|
||||
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
className="plus-button"
|
||||
>
|
||||
Go to Excalidraw+
|
||||
|
@ -1,31 +1,33 @@
|
||||
import React from "react";
|
||||
import { uploadBytes, ref } from "firebase/storage";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||
import { Card } from "@excalidraw/excalidraw/components/Card";
|
||||
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
|
||||
import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
|
||||
import { MIME_TYPES, getFrame } from "@excalidraw/common";
|
||||
import {
|
||||
encryptData,
|
||||
generateEncryptionKey,
|
||||
} from "@excalidraw/excalidraw/data/encryption";
|
||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
||||
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
||||
import { isInitializedImageElement } from "@excalidraw/element";
|
||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||
|
||||
import type {
|
||||
FileId,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
} from "@excalidraw/element/types";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFiles,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||
import {
|
||||
encryptData,
|
||||
generateEncryptionKey,
|
||||
} from "@excalidraw/excalidraw/data/encryption";
|
||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
||||
|
||||
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
|
||||
import { encodeFilesForUpload } from "../data/FileManager";
|
||||
import { uploadBytes, ref } from "firebase/storage";
|
||||
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
|
||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||
import { getFrame } from "@excalidraw/excalidraw/utils";
|
||||
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
|
||||
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
||||
|
||||
export const exportToExcalidrawPlus = async (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { THEME } from "@excalidraw/common";
|
||||
import oc from "open-color";
|
||||
import React from "react";
|
||||
import { THEME } from "@excalidraw/excalidraw/constants";
|
||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
||||
|
||||
import type { Theme } from "@excalidraw/element/types";
|
||||
|
||||
// https://github.com/tholman/github-corners
|
||||
export const GitHubCorner = React.memo(
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
import Trans from "@excalidraw/excalidraw/components/Trans";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
import React from "react";
|
||||
|
||||
interface TopErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||
import { compressData } from "@excalidraw/excalidraw/data/encode";
|
||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
||||
import { newElementWith } from "@excalidraw/element";
|
||||
import { isInitializedImageElement } from "@excalidraw/element";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawImageElement,
|
||||
FileId,
|
||||
InitializedExcalidrawImageElement,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
} from "@excalidraw/element/types";
|
||||
import type {
|
||||
BinaryFileData,
|
||||
BinaryFileMetadata,
|
||||
|
@ -10,6 +10,13 @@
|
||||
* (localStorage, indexedDB).
|
||||
*/
|
||||
|
||||
import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
|
||||
import {
|
||||
CANVAS_SEARCH_TAB,
|
||||
DEFAULT_SIDEBAR,
|
||||
debounce,
|
||||
} from "@excalidraw/common";
|
||||
import { clearElementsForLocalStorage } from "@excalidraw/element";
|
||||
import {
|
||||
createStore,
|
||||
entries,
|
||||
@ -19,26 +26,19 @@ import {
|
||||
setMany,
|
||||
get,
|
||||
} from "idb-keyval";
|
||||
import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
|
||||
import {
|
||||
CANVAS_SEARCH_TAB,
|
||||
DEFAULT_SIDEBAR,
|
||||
} from "@excalidraw/excalidraw/constants";
|
||||
|
||||
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
|
||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFiles,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import type { MaybePromise } from "@excalidraw/excalidraw/utility-types";
|
||||
import { debounce } from "@excalidraw/excalidraw/utils";
|
||||
import type { MaybePromise } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
||||
|
||||
import { FileManager } from "./FileManager";
|
||||
import { Locker } from "./Locker";
|
||||
import { updateBrowserStateVersion } from "./tabSync";
|
||||
|
@ -1,27 +1,12 @@
|
||||
import { reconcileElements } from "@excalidraw/excalidraw";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
OrderedExcalidrawElement,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
import { getSceneVersion } from "@excalidraw/excalidraw/element";
|
||||
import type Portal from "../collab/Portal";
|
||||
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFileMetadata,
|
||||
DataURL,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
|
||||
import { MIME_TYPES } from "@excalidraw/common";
|
||||
import { decompressData } from "@excalidraw/excalidraw/data/encode";
|
||||
import {
|
||||
encryptData,
|
||||
decryptData,
|
||||
} from "@excalidraw/excalidraw/data/encryption";
|
||||
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
|
||||
import type { SyncableExcalidrawElement } from ".";
|
||||
import { getSyncableElements } from ".";
|
||||
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
|
||||
import { getSceneVersion } from "@excalidraw/element";
|
||||
import { initializeApp } from "firebase/app";
|
||||
import {
|
||||
getFirestore,
|
||||
@ -31,8 +16,27 @@ import {
|
||||
Bytes,
|
||||
} from "firebase/firestore";
|
||||
import { getStorage, ref, uploadBytes } from "firebase/storage";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
||||
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
OrderedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFileMetadata,
|
||||
DataURL,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
|
||||
|
||||
import { getSyncableElements } from ".";
|
||||
|
||||
import type { SyncableExcalidrawElement } from ".";
|
||||
import type Portal from "../collab/Portal";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
||||
// private
|
||||
// -----------------------------------------------------------------------------
|
||||
|
@ -9,34 +9,38 @@ import {
|
||||
} from "@excalidraw/excalidraw/data/encryption";
|
||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
||||
import { restore } from "@excalidraw/excalidraw/data/restore";
|
||||
import { isInvisiblySmallElement } from "@excalidraw/element";
|
||||
import { isInitializedImageElement } from "@excalidraw/element";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
import { bytesToHexString } from "@excalidraw/common";
|
||||
|
||||
import type { UserIdleState } from "@excalidraw/common";
|
||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||
import type { SceneBounds } from "@excalidraw/excalidraw/element/bounds";
|
||||
import { isInvisiblySmallElement } from "@excalidraw/excalidraw/element/sizeHelpers";
|
||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
||||
import type { SceneBounds } from "@excalidraw/element";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FileId,
|
||||
OrderedExcalidrawElement,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
import { t } from "@excalidraw/excalidraw/i18n";
|
||||
} from "@excalidraw/element/types";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
BinaryFiles,
|
||||
SocketId,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import type { UserIdleState } from "@excalidraw/excalidraw/constants";
|
||||
import type { MakeBrand } from "@excalidraw/excalidraw/utility-types";
|
||||
import { bytesToHexString } from "@excalidraw/excalidraw/utils";
|
||||
import type { WS_SUBTYPES } from "../app_constants";
|
||||
import type { MakeBrand } from "@excalidraw/common/utility-types";
|
||||
|
||||
import {
|
||||
DELETED_ELEMENT_TIMEOUT,
|
||||
FILE_UPLOAD_MAX_BYTES,
|
||||
ROOM_ID_BYTES,
|
||||
} from "../app_constants";
|
||||
|
||||
import { encodeFilesForUpload } from "./FileManager";
|
||||
import { saveFilesToFirebase } from "./firebase";
|
||||
|
||||
import type { WS_SUBTYPES } from "../app_constants";
|
||||
|
||||
export type SyncableExcalidrawElement = OrderedExcalidrawElement &
|
||||
MakeBrand<"SyncableExcalidrawElement">;
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
import type { ExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
import {
|
||||
clearAppStateForLocalStorage,
|
||||
getDefaultAppState,
|
||||
} from "@excalidraw/excalidraw/appState";
|
||||
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element";
|
||||
import { clearElementsForLocalStorage } from "@excalidraw/element";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { STORAGE_KEYS } from "../app_constants";
|
||||
|
||||
export const saveUsernameToLocalStorage = (username: string) => {
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import ExcalidrawApp from "./App";
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
|
||||
import "../excalidraw-app/sentry";
|
||||
|
||||
import ExcalidrawApp from "./App";
|
||||
|
||||
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
|
||||
const rootElement = document.getElementById("root")!;
|
||||
const root = createRoot(rootElement);
|
||||
|
@ -41,8 +41,8 @@
|
||||
"prettier": "@excalidraw/prettier-config",
|
||||
"scripts": {
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true vite build",
|
||||
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA cross-env VITE_APP_ENABLE_TRACKING=true vite build",
|
||||
"build:app:docker": "vite build",
|
||||
"build:app": "vite build",
|
||||
"build:version": "node ../scripts/build-version.js",
|
||||
"build": "yarn build:app && yarn build:version",
|
||||
"start": "yarn && vite",
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||
import { getFrame } from "@excalidraw/excalidraw/utils";
|
||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||
import { KEYS } from "@excalidraw/excalidraw/keys";
|
||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
||||
import { Dialog } from "@excalidraw/excalidraw/components/Dialog";
|
||||
import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
|
||||
import { TextField } from "@excalidraw/excalidraw/components/TextField";
|
||||
import {
|
||||
copyIcon,
|
||||
LinkIcon,
|
||||
@ -14,16 +12,19 @@ import {
|
||||
shareIOS,
|
||||
shareWindows,
|
||||
} from "@excalidraw/excalidraw/components/icons";
|
||||
import { TextField } from "@excalidraw/excalidraw/components/TextField";
|
||||
import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
|
||||
import type { CollabAPI } from "../collab/Collab";
|
||||
import { activeRoomLinkAtom } from "../collab/Collab";
|
||||
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
|
||||
import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
|
||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||
import { KEYS, getFrame } from "@excalidraw/common";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { atom, useAtom, useAtomValue } from "../app-jotai";
|
||||
import { activeRoomLinkAtom } from "../collab/Collab";
|
||||
|
||||
import "./ShareDialog.scss";
|
||||
|
||||
import type { CollabAPI } from "../collab/Collab";
|
||||
|
||||
type OnExportToBackend = () => void;
|
||||
type ShareDialogType = "share" | "collaborationOnly";
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import ExcalidrawApp from "../App";
|
||||
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||
import {
|
||||
mockBoundingClientRect,
|
||||
render,
|
||||
restoreOriginalGetBoundingClientRect,
|
||||
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||
|
||||
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||
import ExcalidrawApp from "../App";
|
||||
|
||||
describe("Test MobileMenu", () => {
|
||||
const { h } = window;
|
||||
|
@ -198,7 +198,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
||||
<a
|
||||
class="welcome-screen-menu-item "
|
||||
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { vi } from "vitest";
|
||||
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
|
||||
import ExcalidrawApp from "../App";
|
||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||
import { syncInvalidIndices } from "@excalidraw/excalidraw/fractionalIndex";
|
||||
import { CaptureUpdateAction, newElementWith } from "@excalidraw/excalidraw";
|
||||
import {
|
||||
createRedoAction,
|
||||
createUndoAction,
|
||||
} from "@excalidraw/excalidraw/actions/actionHistory";
|
||||
import { CaptureUpdateAction, newElementWith } from "@excalidraw/excalidraw";
|
||||
import { syncInvalidIndices } from "@excalidraw/element";
|
||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { StoreIncrement } from "@excalidraw/element";
|
||||
|
||||
import type { DurableIncrement, EphemeralIncrement } from "@excalidraw/element";
|
||||
|
||||
import ExcalidrawApp from "../App";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
@ -64,6 +69,79 @@ vi.mock("socket.io-client", () => {
|
||||
* i.e. multiplayer history tests could be a good first candidate, as we could test both history stacks simultaneously.
|
||||
*/
|
||||
describe("collaboration", () => {
|
||||
it("should emit two ephemeral increments even though updates get batched", async () => {
|
||||
const durableIncrements: DurableIncrement[] = [];
|
||||
const ephemeralIncrements: EphemeralIncrement[] = [];
|
||||
|
||||
await render(<ExcalidrawApp />);
|
||||
|
||||
h.store.onStoreIncrementEmitter.on((increment) => {
|
||||
if (StoreIncrement.isDurable(increment)) {
|
||||
durableIncrements.push(increment);
|
||||
} else {
|
||||
ephemeralIncrements.push(increment);
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(h.store["scheduledMicroActions"].length).toBe(0);
|
||||
expect(durableIncrements.length).toBe(0);
|
||||
expect(ephemeralIncrements.length).toBe(0);
|
||||
|
||||
const rectProps = {
|
||||
type: "rectangle",
|
||||
id: "A",
|
||||
height: 200,
|
||||
width: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
} as const;
|
||||
|
||||
const rect = API.createElement({ ...rectProps });
|
||||
|
||||
API.updateScene({
|
||||
elements: [rect],
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// expect(commitSpy).toHaveBeenCalledTimes(1);
|
||||
expect(durableIncrements.length).toBe(1);
|
||||
});
|
||||
|
||||
// simulate two batched remote updates
|
||||
act(() => {
|
||||
h.app.updateScene({
|
||||
elements: [newElementWith(h.elements[0], { x: 100 })],
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
h.app.updateScene({
|
||||
elements: [newElementWith(h.elements[0], { x: 200 })],
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
});
|
||||
|
||||
// we scheduled two micro actions,
|
||||
// which confirms they are going to be executed as part of one batched component update
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(h.store["scheduledMicroActions"].length).toBe(2);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// altough the updates get batched,
|
||||
// we expect two ephemeral increments for each update,
|
||||
// and each such update should have the expected change
|
||||
expect(ephemeralIncrements.length).toBe(2);
|
||||
expect(ephemeralIncrements[0].change.elements.A).toEqual(
|
||||
expect.objectContaining({ x: 100 }),
|
||||
);
|
||||
expect(ephemeralIncrements[1].change.elements.A).toEqual(
|
||||
expect.objectContaining({ x: 200 }),
|
||||
);
|
||||
// eslint-disable-next-line dot-notation
|
||||
expect(h.store["scheduledMicroActions"].length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow to undo / redo even on force-deleted elements", async () => {
|
||||
await render(<ExcalidrawApp />);
|
||||
const rect1Props = {
|
||||
@ -121,7 +199,7 @@ describe("collaboration", () => {
|
||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||
});
|
||||
|
||||
const undoAction = createUndoAction(h.history, h.store);
|
||||
const undoAction = createUndoAction(h.history);
|
||||
act(() => h.app.actionManager.executeAction(undoAction));
|
||||
|
||||
// with explicit undo (as addition) we expect our item to be restored from the snapshot!
|
||||
@ -153,7 +231,7 @@ describe("collaboration", () => {
|
||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||
});
|
||||
|
||||
const redoAction = createRedoAction(h.history, h.store);
|
||||
const redoAction = createRedoAction(h.history);
|
||||
act(() => h.app.actionManager.executeAction(redoAction));
|
||||
|
||||
// with explicit redo (as removal) we again restore the element from the snapshot!
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { useEffect, useLayoutEffect, useState } from "react";
|
||||
import { THEME } from "@excalidraw/excalidraw";
|
||||
import { EVENT } from "@excalidraw/excalidraw/constants";
|
||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
||||
import { CODES, KEYS } from "@excalidraw/excalidraw/keys";
|
||||
import { EVENT, CODES, KEYS } from "@excalidraw/common";
|
||||
import { useEffect, useLayoutEffect, useState } from "react";
|
||||
|
||||
import type { Theme } from "@excalidraw/element/types";
|
||||
|
||||
import { STORAGE_KEYS } from "./app_constants";
|
||||
|
||||
const getDarkThemeMediaQuery = (): MediaQueryList | undefined =>
|
||||
|
@ -23,29 +23,57 @@ export default defineConfig(({ mode }) => {
|
||||
envDir: "../",
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^@excalidraw\/common$/,
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
"../packages/common/src/index.ts",
|
||||
),
|
||||
},
|
||||
{
|
||||
find: /^@excalidraw\/common\/(.*?)/,
|
||||
replacement: path.resolve(__dirname, "../packages/common/src/$1"),
|
||||
},
|
||||
{
|
||||
find: /^@excalidraw\/element$/,
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
"../packages/element/src/index.ts",
|
||||
),
|
||||
},
|
||||
{
|
||||
find: /^@excalidraw\/element\/(.*?)/,
|
||||
replacement: path.resolve(__dirname, "../packages/element/src/$1"),
|
||||
},
|
||||
{
|
||||
find: /^@excalidraw\/excalidraw$/,
|
||||
replacement: path.resolve(__dirname, "../packages/excalidraw/index.tsx"),
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
"../packages/excalidraw/index.tsx",
|
||||
),
|
||||
},
|
||||
{
|
||||
find: /^@excalidraw\/excalidraw\/(.*?)/,
|
||||
replacement: path.resolve(__dirname, "../packages/excalidraw/$1"),
|
||||
},
|
||||
{
|
||||
find: /^@excalidraw\/utils$/,
|
||||
replacement: path.resolve(__dirname, "../packages/utils/index.ts"),
|
||||
},
|
||||
{
|
||||
find: /^@excalidraw\/utils\/(.*?)/,
|
||||
replacement: path.resolve(__dirname, "../packages/utils/$1"),
|
||||
},
|
||||
{
|
||||
find: /^@excalidraw\/math$/,
|
||||
replacement: path.resolve(__dirname, "../packages/math/index.ts"),
|
||||
replacement: path.resolve(__dirname, "../packages/math/src/index.ts"),
|
||||
},
|
||||
{
|
||||
find: /^@excalidraw\/math\/(.*?)/,
|
||||
replacement: path.resolve(__dirname, "../packages/math/$1"),
|
||||
replacement: path.resolve(__dirname, "../packages/math/src/$1"),
|
||||
},
|
||||
{
|
||||
find: /^@excalidraw\/utils$/,
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
"../packages/utils/src/index.ts",
|
||||
),
|
||||
},
|
||||
{
|
||||
find: /^@excalidraw\/utils\/(.*?)/,
|
||||
replacement: path.resolve(__dirname, "../packages/utils/src/$1"),
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -197,7 +225,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
],
|
||||
start_url: "/",
|
||||
id:"excalidraw",
|
||||
id: "excalidraw",
|
||||
display: "standalone",
|
||||
theme_color: "#121212",
|
||||
background_color: "#ffffff",
|
||||
|
10
package.json
10
package.json
@ -4,9 +4,7 @@
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"workspaces": [
|
||||
"excalidraw-app",
|
||||
"packages/excalidraw",
|
||||
"packages/utils",
|
||||
"packages/math",
|
||||
"packages/*",
|
||||
"examples/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
@ -26,6 +24,7 @@
|
||||
"dotenv": "16.0.1",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-config-react-app": "7.0.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"http-server": "14.1.1",
|
||||
"husky": "7.0.4",
|
||||
@ -34,6 +33,7 @@
|
||||
"pepjs": "0.5.3",
|
||||
"prettier": "2.6.2",
|
||||
"rewire": "6.0.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "5.0.12",
|
||||
"vite-plugin-checker": "0.7.2",
|
||||
@ -79,8 +79,8 @@
|
||||
"autorelease": "node scripts/autorelease.js",
|
||||
"prerelease:excalidraw": "node scripts/prerelease.js",
|
||||
"release:excalidraw": "node scripts/release.js",
|
||||
"rm:build": "rm -rf excalidraw-app/{build,dist,dev-dist} && rm -rf packages/*/{dist,build} && rm -rf examples/*/{build,dist}",
|
||||
"rm:node_modules": "rm -rf node_modules && rm -rf excalidraw-app/node_modules && rm -rf packages/*/node_modules",
|
||||
"rm:build": "rimraf --glob excalidraw-app/build excalidraw-app/dist excalidraw-app/dev-dist packages/*/dist packages/*/build examples/*/build examples/*/dist",
|
||||
"rm:node_modules": "rimraf --glob node_modules excalidraw-app/node_modules packages/*/node_modules",
|
||||
"clean-install": "yarn rm:node_modules && yarn install"
|
||||
},
|
||||
"resolutions": {
|
||||
|
3
packages/common/.eslintrc.json
Normal file
3
packages/common/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["../eslintrc.base.json"]
|
||||
}
|
19
packages/common/README.md
Normal file
19
packages/common/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# @excalidraw/common
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @excalidraw/common
|
||||
```
|
||||
|
||||
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
|
||||
|
||||
```bash
|
||||
yarn add @excalidraw/common
|
||||
```
|
||||
|
||||
With PNPM, similarly install the package with this command:
|
||||
|
||||
```bash
|
||||
pnpm add @excalidraw/common
|
||||
```
|
3
packages/common/global.d.ts
vendored
Normal file
3
packages/common/global.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
import "@excalidraw/excalidraw/global";
|
||||
import "@excalidraw/excalidraw/css";
|
56
packages/common/package.json
Normal file
56
packages/common/package.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@excalidraw/common",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"types": "./dist/types/common/src/index.d.ts",
|
||||
"main": "./dist/prod/index.js",
|
||||
"module": "./dist/prod/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/common/src/index.d.ts",
|
||||
"development": "./dist/dev/index.js",
|
||||
"production": "./dist/prod/index.js",
|
||||
"default": "./dist/prod/index.js"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./dist/types/common/src/*.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"description": "Excalidraw common functions, constants, etc.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"excalidraw",
|
||||
"excalidraw-utils"
|
||||
],
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all",
|
||||
"not safari < 12",
|
||||
"not kaios <= 2.5",
|
||||
"not edge < 79",
|
||||
"not chrome < 70",
|
||||
"not and_uc < 13",
|
||||
"not samsung < 10"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
"scripts": {
|
||||
"gen:types": "rimraf types && tsc",
|
||||
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export default class BinaryHeap<T> {
|
||||
export class BinaryHeap<T> {
|
||||
private content: T[] = [];
|
||||
|
||||
constructor(private scoreFunction: (node: T) => number) {}
|
@ -1,6 +1,9 @@
|
||||
import oc from "open-color";
|
||||
|
||||
import type { Merge } from "./utility-types";
|
||||
|
||||
export const COLOR_OUTLINE_CONTRAST_THRESHOLD = 240;
|
||||
|
||||
// FIXME can't put to utils.ts rn because of circular dependency
|
||||
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
||||
source: R,
|
@ -1,11 +1,16 @@
|
||||
import type { AppProps, AppState } from "./types";
|
||||
import type { ExcalidrawElement, FontFamilyValues } from "./element/types";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
FontFamilyValues,
|
||||
} from "@excalidraw/element/types";
|
||||
import type { AppProps, AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { COLOR_PALETTE } from "./colors";
|
||||
|
||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||
export const isWindows = /^Win/.test(navigator.platform);
|
||||
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
||||
export const isFirefox =
|
||||
typeof window !== "undefined" &&
|
||||
"netscape" in window &&
|
||||
navigator.userAgent.indexOf("rv:") > 1 &&
|
||||
navigator.userAgent.indexOf("Gecko") > 1;
|
||||
@ -108,12 +113,14 @@ export const YOUTUBE_STATES = {
|
||||
export const ENV = {
|
||||
TEST: "test",
|
||||
DEVELOPMENT: "development",
|
||||
PRODUCTION: "production",
|
||||
};
|
||||
|
||||
export const CLASSES = {
|
||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||
ZOOM_ACTIONS: "zoom-actions",
|
||||
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
||||
CONVERT_ELEMENT_TYPE_POPUP: "ConvertElementTypePopup",
|
||||
};
|
||||
|
||||
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
||||
@ -137,6 +144,7 @@ export const FONT_FAMILY = {
|
||||
"Lilita One": 7,
|
||||
"Comic Shanns": 8,
|
||||
"Liberation Sans": 9,
|
||||
Assistant: 10,
|
||||
};
|
||||
|
||||
export const FONT_FAMILY_FALLBACKS = {
|
||||
@ -248,7 +256,7 @@ export const EXPORT_DATA_TYPES = {
|
||||
excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
|
||||
} as const;
|
||||
|
||||
export const EXPORT_SOURCE =
|
||||
export const getExportSource = () =>
|
||||
window.EXCALIDRAW_EXPORT_SOURCE || window.location.origin;
|
||||
|
||||
// time in milliseconds
|
||||
@ -314,6 +322,9 @@ export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
||||
export const MAX_ALLOWED_FILE_BYTES = 4 * 1024 * 1024;
|
||||
|
||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
export const SVG_DOCUMENT_PREAMBLE = `<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
`;
|
||||
|
||||
export const ENCRYPTION_KEY_BITS = 128;
|
||||
|
||||
@ -415,6 +426,7 @@ export const LIBRARY_DISABLED_TYPES = new Set([
|
||||
// use these constants to easily identify reference sites
|
||||
export const TOOL_TYPE = {
|
||||
selection: "selection",
|
||||
lasso: "lasso",
|
||||
rectangle: "rectangle",
|
||||
diamond: "diamond",
|
||||
ellipse: "ellipse",
|
@ -1,4 +1,4 @@
|
||||
import type { UnsubscribeCallback } from "./types";
|
||||
import type { UnsubscribeCallback } from "@excalidraw/excalidraw/types";
|
||||
|
||||
type Subscriber<T extends any[]> = (...payload: T) => void;
|
||||
|
@ -1,11 +1,9 @@
|
||||
import type { JSX } from "react";
|
||||
import {
|
||||
FreedrawIcon,
|
||||
FontFamilyNormalIcon,
|
||||
FontFamilyHeadingIcon,
|
||||
FontFamilyCodeIcon,
|
||||
} from "../components/icons";
|
||||
import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "../constants";
|
||||
import type {
|
||||
ExcalidrawTextElement,
|
||||
FontFamilyValues,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "./constants";
|
||||
|
||||
/**
|
||||
* Encapsulates font metrics with additional font metadata.
|
||||
@ -22,12 +20,12 @@ export interface FontMetadata {
|
||||
/** harcoded unitless line-height, https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971 */
|
||||
lineHeight: number;
|
||||
};
|
||||
/** element to be displayed as an icon */
|
||||
icon?: JSX.Element;
|
||||
/** flag to indicate a deprecated font */
|
||||
deprecated?: true;
|
||||
/** flag to indicate a server-side only font */
|
||||
serverSide?: true;
|
||||
/**
|
||||
* whether this is a font that users can use (= shown in font picker)
|
||||
*/
|
||||
private?: true;
|
||||
/** flag to indiccate a local-only font */
|
||||
local?: true;
|
||||
/** flag to indicate a fallback font */
|
||||
@ -42,16 +40,14 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||
descender: -374,
|
||||
lineHeight: 1.25,
|
||||
},
|
||||
icon: FreedrawIcon,
|
||||
},
|
||||
[FONT_FAMILY.Nunito]: {
|
||||
metrics: {
|
||||
unitsPerEm: 1000,
|
||||
ascender: 1011,
|
||||
descender: -353,
|
||||
lineHeight: 1.35,
|
||||
lineHeight: 1.25,
|
||||
},
|
||||
icon: FontFamilyNormalIcon,
|
||||
},
|
||||
[FONT_FAMILY["Lilita One"]]: {
|
||||
metrics: {
|
||||
@ -60,7 +56,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||
descender: -220,
|
||||
lineHeight: 1.15,
|
||||
},
|
||||
icon: FontFamilyHeadingIcon,
|
||||
},
|
||||
[FONT_FAMILY["Comic Shanns"]]: {
|
||||
metrics: {
|
||||
@ -69,7 +64,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||
descender: -250,
|
||||
lineHeight: 1.25,
|
||||
},
|
||||
icon: FontFamilyCodeIcon,
|
||||
},
|
||||
[FONT_FAMILY.Virgil]: {
|
||||
metrics: {
|
||||
@ -78,7 +72,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||
descender: -374,
|
||||
lineHeight: 1.25,
|
||||
},
|
||||
icon: FreedrawIcon,
|
||||
deprecated: true,
|
||||
},
|
||||
[FONT_FAMILY.Helvetica]: {
|
||||
@ -88,7 +81,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||
descender: -471,
|
||||
lineHeight: 1.15,
|
||||
},
|
||||
icon: FontFamilyNormalIcon,
|
||||
deprecated: true,
|
||||
local: true,
|
||||
},
|
||||
@ -99,7 +91,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||
descender: -480,
|
||||
lineHeight: 1.2,
|
||||
},
|
||||
icon: FontFamilyCodeIcon,
|
||||
deprecated: true,
|
||||
},
|
||||
[FONT_FAMILY["Liberation Sans"]]: {
|
||||
@ -109,14 +100,23 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||
descender: -434,
|
||||
lineHeight: 1.15,
|
||||
},
|
||||
serverSide: true,
|
||||
private: true,
|
||||
},
|
||||
[FONT_FAMILY.Assistant]: {
|
||||
metrics: {
|
||||
unitsPerEm: 2048,
|
||||
ascender: 1021,
|
||||
descender: -287,
|
||||
lineHeight: 1.25,
|
||||
},
|
||||
private: true,
|
||||
},
|
||||
[FONT_FAMILY_FALLBACKS.Xiaolai]: {
|
||||
metrics: {
|
||||
unitsPerEm: 1000,
|
||||
ascender: 880,
|
||||
descender: -144,
|
||||
lineHeight: 1.15,
|
||||
lineHeight: 1.25,
|
||||
},
|
||||
fallback: true,
|
||||
},
|
||||
@ -148,3 +148,34 @@ export const GOOGLE_FONTS_RANGES = {
|
||||
|
||||
/** local protocol to skip the local font from registering or inlining */
|
||||
export const LOCAL_FONT_PROTOCOL = "local:";
|
||||
|
||||
/**
|
||||
* Calculates vertical offset for a text with alphabetic baseline.
|
||||
*/
|
||||
export const getVerticalOffset = (
|
||||
fontFamily: ExcalidrawTextElement["fontFamily"],
|
||||
fontSize: ExcalidrawTextElement["fontSize"],
|
||||
lineHeightPx: number,
|
||||
) => {
|
||||
const { unitsPerEm, ascender, descender } =
|
||||
FONT_METADATA[fontFamily]?.metrics ||
|
||||
FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
|
||||
|
||||
const fontSizeEm = fontSize / unitsPerEm;
|
||||
const lineGap =
|
||||
(lineHeightPx - fontSizeEm * ascender + fontSizeEm * descender) / 2;
|
||||
|
||||
const verticalOffset = fontSizeEm * ascender + lineGap;
|
||||
return verticalOffset;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets line height for a selected family.
|
||||
*/
|
||||
export const getLineHeight = (fontFamily: FontFamilyValues) => {
|
||||
const { lineHeight } =
|
||||
FONT_METADATA[fontFamily]?.metrics ||
|
||||
FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
|
||||
|
||||
return lineHeight as ExcalidrawTextElement["lineHeight"];
|
||||
};
|
12
packages/common/src/index.ts
Normal file
12
packages/common/src/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export * from "./binary-heap";
|
||||
export * from "./colors";
|
||||
export * from "./constants";
|
||||
export * from "./font-metadata";
|
||||
export * from "./queue";
|
||||
export * from "./keys";
|
||||
export * from "./points";
|
||||
export * from "./promise-pool";
|
||||
export * from "./random";
|
||||
export * from "./url";
|
||||
export * from "./utils";
|
||||
export * from "./emitter";
|
@ -1,4 +1,5 @@
|
||||
import { isDarwin } from "./constants";
|
||||
|
||||
import type { ValueOf } from "./utility-types";
|
||||
|
||||
export const CODES = {
|
@ -4,6 +4,8 @@ import {
|
||||
type LocalPoint,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { NullableGridSize } from "@excalidraw/excalidraw/types";
|
||||
|
||||
export const getSizeFromPoints = (
|
||||
points: readonly (GlobalPoint | LocalPoint)[],
|
||||
) => {
|
||||
@ -61,3 +63,18 @@ export const rescalePoints = <Point extends GlobalPoint | LocalPoint>(
|
||||
|
||||
return nextPoints;
|
||||
};
|
||||
|
||||
// TODO: Rounding this point causes some shake when free drawing
|
||||
export const getGridPoint = (
|
||||
x: number,
|
||||
y: number,
|
||||
gridSize: NullableGridSize,
|
||||
): [number, number] => {
|
||||
if (gridSize) {
|
||||
return [
|
||||
Math.round(x / gridSize) * gridSize,
|
||||
Math.round(y / gridSize) * gridSize,
|
||||
];
|
||||
}
|
||||
return [x, y];
|
||||
};
|
50
packages/common/src/promise-pool.ts
Normal file
50
packages/common/src/promise-pool.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import Pool from "es6-promise-pool";
|
||||
|
||||
// extending the missing types
|
||||
// relying on the [Index, T] to keep a correct order
|
||||
type TPromisePool<T, Index = number> = Pool<[Index, T][]> & {
|
||||
addEventListener: (
|
||||
type: "fulfilled",
|
||||
listener: (event: { data: { result: [Index, T] } }) => void,
|
||||
) => (event: { data: { result: [Index, T] } }) => void;
|
||||
removeEventListener: (
|
||||
type: "fulfilled",
|
||||
listener: (event: { data: { result: [Index, T] } }) => void,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export class PromisePool<T> {
|
||||
private readonly pool: TPromisePool<T>;
|
||||
private readonly entries: Record<number, T> = {};
|
||||
|
||||
constructor(
|
||||
source: IterableIterator<Promise<void | readonly [number, T]>>,
|
||||
concurrency: number,
|
||||
) {
|
||||
this.pool = new Pool(
|
||||
source as unknown as () => void | PromiseLike<[number, T][]>,
|
||||
concurrency,
|
||||
) as TPromisePool<T>;
|
||||
}
|
||||
|
||||
public all() {
|
||||
const listener = (event: { data: { result: void | [number, T] } }) => {
|
||||
if (event.data.result) {
|
||||
// by default pool does not return the results, so we are gathering them manually
|
||||
// with the correct call order (represented by the index in the tuple)
|
||||
const [index, value] = event.data.result;
|
||||
this.entries[index] = value;
|
||||
}
|
||||
};
|
||||
|
||||
this.pool.addEventListener("fulfilled", listener);
|
||||
|
||||
return this.pool.start().then(() => {
|
||||
setTimeout(() => {
|
||||
this.pool.removeEventListener("fulfilled", listener);
|
||||
});
|
||||
|
||||
return Object.values(this.entries);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import { promiseTry, resolvablePromise } from ".";
|
||||
|
||||
import type { ResolvablePromise } from ".";
|
||||
|
||||
import type { MaybePromise } from "./utility-types";
|
||||
import type { ResolvablePromise } from "./utils";
|
||||
import { promiseTry, resolvablePromise } from "./utils";
|
||||
|
||||
type Job<T, TArgs extends unknown[]> = (...args: TArgs) => MaybePromise<T>;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Random } from "roughjs/bin/math";
|
||||
import { nanoid } from "nanoid";
|
||||
import { Random } from "roughjs/bin/math";
|
||||
|
||||
import { isTestEnv } from "./utils";
|
||||
|
||||
let random = new Random(Date.now());
|
@ -1,5 +1,6 @@
|
||||
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||
import { escapeDoubleQuotes } from "../utils";
|
||||
|
||||
import { escapeDoubleQuotes } from "./utils";
|
||||
|
||||
export const normalizeLink = (link: string) => {
|
||||
link = link.trim();
|
@ -68,3 +68,12 @@ export type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
// get union of all keys from the union of types
|
||||
export type AllPossibleKeys<T> = T extends any ? keyof T : never;
|
||||
|
||||
/** Strip all the methods or functions from a type */
|
||||
export type DTO<T> = {
|
||||
[K in keyof T as T[K] extends Function ? never : K]: T[K];
|
||||
};
|
||||
|
||||
export type MapEntry<M extends Map<any, any>> = M extends Map<infer K, infer V>
|
||||
? [K, V]
|
||||
: never;
|
82
packages/common/src/utils.test.ts
Normal file
82
packages/common/src/utils.test.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import {
|
||||
isTransparent,
|
||||
mapFind,
|
||||
reduceToCommonValue,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
describe("@excalidraw/common/utils", () => {
|
||||
describe("isTransparent()", () => {
|
||||
it("should return true when color is rgb transparent", () => {
|
||||
expect(isTransparent("#ff00")).toEqual(true);
|
||||
expect(isTransparent("#fff00000")).toEqual(true);
|
||||
expect(isTransparent("transparent")).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false when color is not transparent", () => {
|
||||
expect(isTransparent("#ced4da")).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reduceToCommonValue()", () => {
|
||||
it("should return the common value when all values are the same", () => {
|
||||
expect(reduceToCommonValue([1, 1])).toEqual(1);
|
||||
expect(reduceToCommonValue([0, 0])).toEqual(0);
|
||||
expect(reduceToCommonValue(["a", "a"])).toEqual("a");
|
||||
expect(reduceToCommonValue(new Set([1]))).toEqual(1);
|
||||
expect(reduceToCommonValue([""])).toEqual("");
|
||||
expect(reduceToCommonValue([0])).toEqual(0);
|
||||
|
||||
const o = {};
|
||||
expect(reduceToCommonValue([o, o])).toEqual(o);
|
||||
|
||||
expect(
|
||||
reduceToCommonValue([{ a: 1 }, { a: 1, b: 2 }], (o) => o.a),
|
||||
).toEqual(1);
|
||||
expect(
|
||||
reduceToCommonValue(new Set([{ a: 1 }, { a: 1, b: 2 }]), (o) => o.a),
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("should return `null` when values are different", () => {
|
||||
expect(reduceToCommonValue([1, 2, 3])).toEqual(null);
|
||||
expect(reduceToCommonValue(new Set([1, 2]))).toEqual(null);
|
||||
expect(reduceToCommonValue([{ a: 1 }, { a: 2 }], (o) => o.a)).toEqual(
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return `null` when some values are nullable", () => {
|
||||
expect(reduceToCommonValue([1, null, 1])).toEqual(null);
|
||||
expect(reduceToCommonValue([null, 1])).toEqual(null);
|
||||
expect(reduceToCommonValue([1, undefined])).toEqual(null);
|
||||
expect(reduceToCommonValue([undefined, 1])).toEqual(null);
|
||||
expect(reduceToCommonValue([null])).toEqual(null);
|
||||
expect(reduceToCommonValue([undefined])).toEqual(null);
|
||||
expect(reduceToCommonValue([])).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapFind()", () => {
|
||||
it("should return the first mapped non-null element", () => {
|
||||
{
|
||||
let counter = 0;
|
||||
|
||||
const result = mapFind(["a", "b", "c"], (value) => {
|
||||
counter++;
|
||||
return value === "b" ? 42 : null;
|
||||
});
|
||||
expect(result).toEqual(42);
|
||||
expect(counter).toBe(2);
|
||||
}
|
||||
|
||||
expect(mapFind([1, 2], (value) => value * 0)).toBe(0);
|
||||
expect(mapFind([1, 2], () => false)).toBe(false);
|
||||
expect(mapFind([1, 2], () => "")).toBe("");
|
||||
});
|
||||
|
||||
it("should return undefined if no mapped element is found", () => {
|
||||
expect(mapFind([1, 2], () => undefined)).toBe(undefined);
|
||||
expect(mapFind([1, 2], () => null)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,28 +1,34 @@
|
||||
import Pool from "es6-promise-pool";
|
||||
import { average } from "@excalidraw/math";
|
||||
import { COLOR_PALETTE } from "./colors";
|
||||
import type { EVENT } from "./constants";
|
||||
import {
|
||||
DEFAULT_VERSION,
|
||||
FONT_FAMILY,
|
||||
getFontFamilyFallbacks,
|
||||
isDarwin,
|
||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||
} from "./constants";
|
||||
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawBindableElement,
|
||||
FontFamilyValues,
|
||||
FontString,
|
||||
} from "./element/types";
|
||||
ExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type {
|
||||
ActiveTool,
|
||||
AppState,
|
||||
ToolType,
|
||||
UnsubscribeCallback,
|
||||
Zoom,
|
||||
} from "./types";
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { COLOR_PALETTE } from "./colors";
|
||||
import {
|
||||
DEFAULT_VERSION,
|
||||
ENV,
|
||||
FONT_FAMILY,
|
||||
getFontFamilyFallbacks,
|
||||
isDarwin,
|
||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||
} from "./constants";
|
||||
|
||||
import type { MaybePromise, ResolutionType } from "./utility-types";
|
||||
|
||||
import type { EVENT } from "./constants";
|
||||
|
||||
let mockDateTime: string | null = null;
|
||||
|
||||
export const setDateTimeForTests = (dateTime: string) => {
|
||||
@ -167,7 +173,7 @@ export const throttleRAF = <T extends any[]>(
|
||||
};
|
||||
|
||||
const ret = (...args: T) => {
|
||||
if (import.meta.env.MODE === "test") {
|
||||
if (isTestEnv()) {
|
||||
fn(...args);
|
||||
return;
|
||||
}
|
||||
@ -380,7 +386,7 @@ export const updateActiveTool = (
|
||||
type: ToolType;
|
||||
}
|
||||
| { type: "custom"; customType: string }
|
||||
) & { locked?: boolean }) & {
|
||||
) & { locked?: boolean; fromSelection?: boolean }) & {
|
||||
lastActiveToolBeforeEraser?: ActiveTool | null;
|
||||
},
|
||||
): AppState["activeTool"] => {
|
||||
@ -402,6 +408,7 @@ export const updateActiveTool = (
|
||||
type: data.type,
|
||||
customType: null,
|
||||
locked: data.locked ?? appState.activeTool.locked,
|
||||
fromSelection: data.fromSelection ?? false,
|
||||
};
|
||||
};
|
||||
|
||||
@ -537,6 +544,20 @@ export const findLastIndex = <T>(
|
||||
return -1;
|
||||
};
|
||||
|
||||
/** returns the first non-null mapped value */
|
||||
export const mapFind = <T, K>(
|
||||
collection: readonly T[],
|
||||
iteratee: (value: T, index: number) => K | undefined | null,
|
||||
): K | undefined => {
|
||||
for (let idx = 0; idx < collection.length; idx++) {
|
||||
const result = iteratee(collection[idx], idx);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const isTransparent = (color: string) => {
|
||||
const isRGBTransparent = color.length === 5 && color.substr(4, 1) === "0";
|
||||
const isRRGGBBTransparent = color.length === 9 && color.substr(7, 2) === "00";
|
||||
@ -673,7 +694,7 @@ export const arrayToMap = <T extends { id: string } | string>(
|
||||
return items.reduce((acc: Map<string, T>, element) => {
|
||||
acc.set(typeof element === "string" ? element : element.id, element);
|
||||
return acc;
|
||||
}, new Map());
|
||||
}, new Map() as Map<string, T>);
|
||||
};
|
||||
|
||||
export const arrayToMapWithIndex = <T extends { id: string }>(
|
||||
@ -728,9 +749,30 @@ export const arrayToList = <T>(array: readonly T[]): Node<T>[] =>
|
||||
return acc;
|
||||
}, [] as Node<T>[]);
|
||||
|
||||
export const isTestEnv = () => import.meta.env.MODE === "test";
|
||||
/**
|
||||
* Converts a readonly array or map into an iterable.
|
||||
* Useful for avoiding entry allocations when iterating object / map on each iteration.
|
||||
*/
|
||||
export const toIterable = <T>(
|
||||
values: readonly T[] | ReadonlyMap<string, T>,
|
||||
): Iterable<T> => {
|
||||
return Array.isArray(values) ? values : values.values();
|
||||
};
|
||||
|
||||
export const isDevEnv = () => import.meta.env.MODE === "development";
|
||||
/**
|
||||
* Converts a readonly array or map into an array.
|
||||
*/
|
||||
export const toArray = <T>(
|
||||
values: readonly T[] | ReadonlyMap<string, T>,
|
||||
): T[] => {
|
||||
return Array.isArray(values) ? values : Array.from(toIterable(values));
|
||||
};
|
||||
|
||||
export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
|
||||
|
||||
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
||||
|
||||
export const isProdEnv = () => import.meta.env.MODE === ENV.PRODUCTION;
|
||||
|
||||
export const isServerEnv = () =>
|
||||
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
|
||||
@ -1184,54 +1226,6 @@ export const safelyParseJSON = (json: string): Record<string, any> | null => {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
// extending the missing types
|
||||
// relying on the [Index, T] to keep a correct order
|
||||
type TPromisePool<T, Index = number> = Pool<[Index, T][]> & {
|
||||
addEventListener: (
|
||||
type: "fulfilled",
|
||||
listener: (event: { data: { result: [Index, T] } }) => void,
|
||||
) => (event: { data: { result: [Index, T] } }) => void;
|
||||
removeEventListener: (
|
||||
type: "fulfilled",
|
||||
listener: (event: { data: { result: [Index, T] } }) => void,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export class PromisePool<T> {
|
||||
private readonly pool: TPromisePool<T>;
|
||||
private readonly entries: Record<number, T> = {};
|
||||
|
||||
constructor(
|
||||
source: IterableIterator<Promise<void | readonly [number, T]>>,
|
||||
concurrency: number,
|
||||
) {
|
||||
this.pool = new Pool(
|
||||
source as unknown as () => void | PromiseLike<[number, T][]>,
|
||||
concurrency,
|
||||
) as TPromisePool<T>;
|
||||
}
|
||||
|
||||
public all() {
|
||||
const listener = (event: { data: { result: void | [number, T] } }) => {
|
||||
if (event.data.result) {
|
||||
// by default pool does not return the results, so we are gathering them manually
|
||||
// with the correct call order (represented by the index in the tuple)
|
||||
const [index, value] = event.data.result;
|
||||
this.entries[index] = value;
|
||||
}
|
||||
};
|
||||
|
||||
this.pool.addEventListener("fulfilled", listener);
|
||||
|
||||
return this.pool.start().then(() => {
|
||||
setTimeout(() => {
|
||||
this.pool.removeEventListener("fulfilled", listener);
|
||||
});
|
||||
|
||||
return Object.values(this.entries);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* use when you need to render unsafe string as HTML attribute, but MAKE SURE
|
||||
@ -1243,3 +1237,60 @@ export const escapeDoubleQuotes = (str: string) => {
|
||||
|
||||
export const castArray = <T>(value: T | T[]): T[] =>
|
||||
Array.isArray(value) ? value : [value];
|
||||
|
||||
export const elementCenterPoint = (
|
||||
element: ExcalidrawElement,
|
||||
xOffset: number = 0,
|
||||
yOffset: number = 0,
|
||||
) => {
|
||||
const { x, y, width, height } = element;
|
||||
|
||||
const centerXPoint = x + width / 2 + xOffset;
|
||||
|
||||
const centerYPoint = y + height / 2 + yOffset;
|
||||
|
||||
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
|
||||
};
|
||||
|
||||
/** hack for Array.isArray type guard not working with readonly value[] */
|
||||
export const isReadonlyArray = (value?: any): value is readonly any[] => {
|
||||
return Array.isArray(value);
|
||||
};
|
||||
|
||||
export const sizeOf = (
|
||||
value:
|
||||
| readonly unknown[]
|
||||
| Readonly<Map<string, unknown>>
|
||||
| Readonly<Record<string, unknown>>
|
||||
| ReadonlySet<unknown>,
|
||||
): number => {
|
||||
return isReadonlyArray(value)
|
||||
? value.length
|
||||
: value instanceof Map || value instanceof Set
|
||||
? value.size
|
||||
: Object.keys(value).length;
|
||||
};
|
||||
|
||||
export const reduceToCommonValue = <T, R = T>(
|
||||
collection: readonly T[] | ReadonlySet<T>,
|
||||
getValue?: (item: T) => R,
|
||||
): R | null => {
|
||||
if (sizeOf(collection) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const valueExtractor = getValue || ((item: T) => item as unknown as R);
|
||||
|
||||
let commonValue: R | null = null;
|
||||
|
||||
for (const item of collection) {
|
||||
const value = valueExtractor(item);
|
||||
if ((commonValue === null || commonValue === value) && value != null) {
|
||||
commonValue = value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return commonValue;
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { KEYS, matchKey } from "./keys";
|
||||
import { KEYS, matchKey } from "../src/keys";
|
||||
|
||||
describe("key matcher", async () => {
|
||||
it("should not match unexpected key", async () => {
|
@ -1,4 +1,4 @@
|
||||
import { Queue } from "./queue";
|
||||
import { Queue } from "../src/queue";
|
||||
|
||||
describe("Queue", () => {
|
||||
const calls: any[] = [];
|
@ -1,4 +1,4 @@
|
||||
import { normalizeLink } from "./url";
|
||||
import { normalizeLink } from "../src/url";
|
||||
|
||||
describe("normalizeLink", () => {
|
||||
// NOTE not an extensive XSS test suite, just to check if we're not
|
8
packages/common/tsconfig.json
Normal file
8
packages/common/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/types"
|
||||
},
|
||||
"include": ["src/**/*", "global.d.ts"],
|
||||
"exclude": ["**/*.test.*", "tests", "types", "examples", "dist"]
|
||||
}
|
3
packages/element/.eslintrc.json
Normal file
3
packages/element/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["../eslintrc.base.json"]
|
||||
}
|
19
packages/element/README.md
Normal file
19
packages/element/README.md
Normal file
@ -0,0 +1,19 @@
|
||||
# @excalidraw/element
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @excalidraw/element
|
||||
```
|
||||
|
||||
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
|
||||
|
||||
```bash
|
||||
yarn add @excalidraw/element
|
||||
```
|
||||
|
||||
With PNPM, similarly install the package with this command:
|
||||
|
||||
```bash
|
||||
pnpm add @excalidraw/element
|
||||
```
|
3
packages/element/global.d.ts
vendored
Normal file
3
packages/element/global.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
import "@excalidraw/excalidraw/global";
|
||||
import "@excalidraw/excalidraw/css";
|
56
packages/element/package.json
Normal file
56
packages/element/package.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@excalidraw/element",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"types": "./dist/types/element/src/index.d.ts",
|
||||
"main": "./dist/prod/index.js",
|
||||
"module": "./dist/prod/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/element/src/index.d.ts",
|
||||
"development": "./dist/dev/index.js",
|
||||
"production": "./dist/prod/index.js",
|
||||
"default": "./dist/prod/index.js"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./dist/types/element/src/*.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"description": "Excalidraw elements-related logic",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"excalidraw",
|
||||
"excalidraw-utils"
|
||||
],
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all",
|
||||
"not safari < 12",
|
||||
"not kaios <= 2.5",
|
||||
"not edge < 79",
|
||||
"not chrome < 70",
|
||||
"not and_uc < 13",
|
||||
"not samsung < 10"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
"scripts": {
|
||||
"gen:types": "rimraf types && tsc",
|
||||
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||
}
|
||||
}
|
@ -1,4 +1,27 @@
|
||||
import throttle from "lodash.throttle";
|
||||
|
||||
import {
|
||||
randomInteger,
|
||||
arrayToMap,
|
||||
toBrandedType,
|
||||
isDevEnv,
|
||||
isTestEnv,
|
||||
toArray,
|
||||
} from "@excalidraw/common";
|
||||
import { isNonDeletedElement } from "@excalidraw/element";
|
||||
import { isFrameLikeElement } from "@excalidraw/element";
|
||||
import { getElementsInGroup } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
syncInvalidIndices,
|
||||
syncMovedIndices,
|
||||
validateFractionalIndices,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { getSelectedElements } from "@excalidraw/element";
|
||||
|
||||
import { mutateElement, type ElementUpdate } from "@excalidraw/element";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
@ -9,26 +32,15 @@ import type {
|
||||
NonDeletedSceneElementsMap,
|
||||
OrderedExcalidrawElement,
|
||||
Ordered,
|
||||
} from "../element/types";
|
||||
import { isNonDeletedElement } from "../element";
|
||||
import type { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { isFrameLikeElement } from "../element/typeChecks";
|
||||
import { getSelectedElements } from "./selection";
|
||||
import type { AppState } from "../types";
|
||||
import type { Assert, SameType } from "../utility-types";
|
||||
import { randomInteger } from "../random";
|
||||
import {
|
||||
syncInvalidIndices,
|
||||
syncMovedIndices,
|
||||
validateFractionalIndices,
|
||||
} from "../fractionalIndex";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { toBrandedType } from "../utils";
|
||||
import { ENV } from "../constants";
|
||||
import { getElementsInGroup } from "../groups";
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||
import type {
|
||||
Assert,
|
||||
Mutable,
|
||||
SameType,
|
||||
} from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { AppState } from "../../excalidraw/types";
|
||||
|
||||
type SceneStateCallback = () => void;
|
||||
type SceneStateCallbackRemover = () => void;
|
||||
@ -54,14 +66,10 @@ const getNonDeletedElements = <T extends ExcalidrawElement>(
|
||||
|
||||
const validateIndicesThrottled = throttle(
|
||||
(elements: readonly ExcalidrawElement[]) => {
|
||||
if (
|
||||
import.meta.env.DEV ||
|
||||
import.meta.env.MODE === ENV.TEST ||
|
||||
window?.DEBUG_FRACTIONAL_INDICES
|
||||
) {
|
||||
if (isDevEnv() || isTestEnv() || window?.DEBUG_FRACTIONAL_INDICES) {
|
||||
validateFractionalIndices(elements, {
|
||||
// throw only in dev & test, to remain functional on `DEBUG_FRACTIONAL_INDICES`
|
||||
shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST,
|
||||
shouldThrow: isDevEnv() || isTestEnv(),
|
||||
includeBoundTextValidation: true,
|
||||
});
|
||||
}
|
||||
@ -97,44 +105,7 @@ const hashSelectionOpts = (
|
||||
// in our codebase
|
||||
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
||||
|
||||
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
|
||||
if (typeof elementKey === "string") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
class Scene {
|
||||
// ---------------------------------------------------------------------------
|
||||
// static methods/props
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
|
||||
private static sceneMapById = new Map<string, Scene>();
|
||||
|
||||
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
|
||||
if (isIdKey(elementKey)) {
|
||||
// for cases where we don't have access to the element object
|
||||
// (e.g. restore serialized appState with id references)
|
||||
this.sceneMapById.set(elementKey, scene);
|
||||
} else {
|
||||
this.sceneMapByElement.set(elementKey, scene);
|
||||
// if mapping element objects, also cache the id string when later
|
||||
// looking up by id alone
|
||||
this.sceneMapById.set(elementKey.id, scene);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated pass down `app.scene` and use it directly
|
||||
*/
|
||||
static getScene(elementKey: ElementKey): Scene | null {
|
||||
if (isIdKey(elementKey)) {
|
||||
return this.sceneMapById.get(elementKey) || null;
|
||||
}
|
||||
return this.sceneMapByElement.get(elementKey) || null;
|
||||
}
|
||||
|
||||
export class Scene {
|
||||
// ---------------------------------------------------------------------------
|
||||
// instance methods/props
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -193,6 +164,12 @@ class Scene {
|
||||
return this.frames;
|
||||
}
|
||||
|
||||
constructor(elements: ElementsMapOrArray | null = null) {
|
||||
if (elements) {
|
||||
this.replaceAllElements(elements);
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedElements(opts: {
|
||||
// NOTE can be ommitted by making Scene constructor require App instance
|
||||
selectedElementIds: AppState["selectedElementIds"];
|
||||
@ -287,11 +264,8 @@ class Scene {
|
||||
}
|
||||
|
||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||
const _nextElements =
|
||||
// ts doesn't like `Array.isArray` of `instanceof Map`
|
||||
nextElements instanceof Array
|
||||
? nextElements
|
||||
: Array.from(nextElements.values());
|
||||
// we do trust the insertion order on the map, though maybe we shouldn't and should prefer order defined by fractional indices
|
||||
const _nextElements = toArray(nextElements);
|
||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||
|
||||
validateIndicesThrottled(_nextElements);
|
||||
@ -303,7 +277,6 @@ class Scene {
|
||||
nextFrameLikes.push(element);
|
||||
}
|
||||
this.elementsMap.set(element.id, element);
|
||||
Scene.mapElementToScene(element, this);
|
||||
});
|
||||
const nonDeletedElements = getNonDeletedElements(this.elements);
|
||||
this.nonDeletedElements = nonDeletedElements.elements;
|
||||
@ -348,12 +321,6 @@ class Scene {
|
||||
this.selectedElementsCache.elements = null;
|
||||
this.selectedElementsCache.cache.clear();
|
||||
|
||||
Scene.sceneMapById.forEach((scene, elementKey) => {
|
||||
if (scene === this) {
|
||||
Scene.sceneMapById.delete(elementKey);
|
||||
}
|
||||
});
|
||||
|
||||
// done not for memory leaks, but to guard against possible late fires
|
||||
// (I guess?)
|
||||
this.callbacks.clear();
|
||||
@ -450,6 +417,40 @@ class Scene {
|
||||
// then, check if the id is a group
|
||||
return getElementsInGroup(elementsMap, id);
|
||||
};
|
||||
}
|
||||
|
||||
export default Scene;
|
||||
// Mutate an element with passed updates and trigger the component to update. Make sure you
|
||||
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
||||
mutateElement<TElement extends Mutable<ExcalidrawElement>>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
options: {
|
||||
informMutation: boolean;
|
||||
isDragging: boolean;
|
||||
} = {
|
||||
informMutation: true,
|
||||
isDragging: false,
|
||||
},
|
||||
) {
|
||||
const elementsMap = this.getNonDeletedElementsMap();
|
||||
|
||||
const { version: prevVersion } = element;
|
||||
const { version: nextVersion } = mutateElement(
|
||||
element,
|
||||
elementsMap,
|
||||
updates,
|
||||
options,
|
||||
);
|
||||
|
||||
if (
|
||||
// skip if the element is not in the scene (i.e. selection)
|
||||
this.elementsMap.has(element.id) &&
|
||||
// skip if the element's version hasn't changed, as mutateElement returned the same element
|
||||
prevVersion !== nextVersion &&
|
||||
options.informMutation
|
||||
) {
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
@ -1,31 +1,38 @@
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
import type { Drawable, Options } from "roughjs/bin/core";
|
||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
||||
import { getDiamondPoints, getArrowheadPoints } from "../element";
|
||||
import type { ElementShapes } from "./types";
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
ExcalidrawSelectionElement,
|
||||
ExcalidrawLinearElement,
|
||||
Arrowhead,
|
||||
} from "../element/types";
|
||||
import { generateFreeDrawShape } from "../renderer/renderElement";
|
||||
import { isTransparent, assertNever } from "../utils";
|
||||
import { simplify } from "points-on-curve";
|
||||
import { ROUGHNESS } from "../constants";
|
||||
|
||||
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
|
||||
import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
||||
import type { ElementShapes } from "@excalidraw/excalidraw/scene/types";
|
||||
|
||||
import {
|
||||
isElbowArrow,
|
||||
isEmbeddableElement,
|
||||
isIframeElement,
|
||||
isIframeLikeElement,
|
||||
isLinearElement,
|
||||
} from "../element/typeChecks";
|
||||
} from "./typeChecks";
|
||||
import { getCornerRadius, isPathALoop } from "./shapes";
|
||||
import { headingForPointIsHorizontal } from "./heading";
|
||||
|
||||
import { canChangeRoundness } from "./comparisons";
|
||||
import type { EmbedsValidationStatus } from "../types";
|
||||
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
|
||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||
import { headingForPointIsHorizontal } from "../element/heading";
|
||||
import { generateFreeDrawShape } from "./renderElement";
|
||||
import { getArrowheadPoints, getDiamondPoints } from "./bounds";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
ExcalidrawSelectionElement,
|
||||
ExcalidrawLinearElement,
|
||||
Arrowhead,
|
||||
} from "./types";
|
||||
|
||||
import type { Drawable, Options } from "roughjs/bin/core";
|
||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
|
||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||
|
||||
@ -508,7 +515,10 @@ export const _generateElementShape = (
|
||||
|
||||
if (isPathALoop(element.points)) {
|
||||
// generate rough polygon to fill freedraw shape
|
||||
const simplifiedPoints = simplify(element.points, 0.75);
|
||||
const simplifiedPoints = simplify(
|
||||
element.points as Mutable<LocalPoint[]>,
|
||||
0.75,
|
||||
);
|
||||
shape = generator.curve(simplifiedPoints as [number, number][], {
|
||||
...generateRoughOptions(element),
|
||||
stroke: "none",
|
@ -1,14 +1,23 @@
|
||||
import type { Drawable } from "roughjs/bin/core";
|
||||
import { RoughGenerator } from "roughjs/bin/generator";
|
||||
|
||||
import { COLOR_PALETTE } from "@excalidraw/common";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawSelectionElement,
|
||||
} from "../element/types";
|
||||
import { elementWithCanvasCache } from "../renderer/renderElement";
|
||||
AppState,
|
||||
EmbedsValidationStatus,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import type {
|
||||
ElementShape,
|
||||
ElementShapes,
|
||||
} from "@excalidraw/excalidraw/scene/types";
|
||||
|
||||
import { _generateElementShape } from "./Shape";
|
||||
import type { ElementShape, ElementShapes } from "./types";
|
||||
import { COLOR_PALETTE } from "../colors";
|
||||
import type { AppState, EmbedsValidationStatus } from "../types";
|
||||
|
||||
import { elementWithCanvasCache } from "./renderElement";
|
||||
|
||||
import type { ExcalidrawElement, ExcalidrawSelectionElement } from "./types";
|
||||
|
||||
import type { Drawable } from "roughjs/bin/core";
|
||||
|
||||
export class ShapeCache {
|
||||
private static rg = new RoughGenerator();
|
@ -1,10 +1,11 @@
|
||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
||||
import { mutateElement } from "./element/mutateElement";
|
||||
import type { BoundingBox } from "./element/bounds";
|
||||
import { getCommonBoundingBox } from "./element/bounds";
|
||||
import { updateBoundElements } from "./binding";
|
||||
import { getCommonBoundingBox } from "./bounds";
|
||||
import { getMaximumGroups } from "./groups";
|
||||
import { updateBoundElements } from "./element/binding";
|
||||
import type Scene from "./scene/Scene";
|
||||
|
||||
import type { Scene } from "./Scene";
|
||||
|
||||
import type { BoundingBox } from "./bounds";
|
||||
import type { ExcalidrawElement } from "./types";
|
||||
|
||||
export interface Alignment {
|
||||
position: "start" | "center" | "end";
|
||||
@ -13,10 +14,10 @@ export interface Alignment {
|
||||
|
||||
export const alignElements = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
alignment: Alignment,
|
||||
scene: Scene,
|
||||
): ExcalidrawElement[] => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
@ -31,12 +32,13 @@ export const alignElements = (
|
||||
);
|
||||
return group.map((element) => {
|
||||
// update element
|
||||
const updatedEle = mutateElement(element, {
|
||||
const updatedEle = scene.mutateElement(element, {
|
||||
x: element.x + translation.x,
|
||||
y: element.y + translation.y,
|
||||
});
|
||||
|
||||
// update bound elements
|
||||
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: group,
|
||||
});
|
||||
return updatedEle;
|
File diff suppressed because it is too large
Load Diff
@ -1,17 +1,42 @@
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
Arrowhead,
|
||||
ExcalidrawFreeDrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMap,
|
||||
} from "./types";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
import type { Drawable, Op } from "roughjs/bin/core";
|
||||
import type { AppState } from "../types";
|
||||
import { generateRoughOptions } from "../scene/Shape";
|
||||
|
||||
import {
|
||||
arrayToMap,
|
||||
invariant,
|
||||
rescalePoints,
|
||||
sizeOf,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
degreesToRadians,
|
||||
lineSegment,
|
||||
pointDistance,
|
||||
pointFrom,
|
||||
pointFromArray,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||
|
||||
import { pointsOnBezierCurves } from "points-on-curve";
|
||||
|
||||
import type {
|
||||
Curve,
|
||||
Degrees,
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
LocalPoint,
|
||||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { generateRoughOptions } from "./Shape";
|
||||
import { ShapeCache } from "./ShapeCache";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBoundToContainer,
|
||||
@ -19,28 +44,28 @@ import {
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import { rescalePoints } from "../points";
|
||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import { arrayToMap, invariant } from "../utils";
|
||||
import type {
|
||||
Degrees,
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
LocalPoint,
|
||||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { getElementShape } from "./shapes";
|
||||
|
||||
import {
|
||||
degreesToRadians,
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
pointDistance,
|
||||
pointFromArray,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
import type { Mutable } from "../utility-types";
|
||||
import { getCurvePathOps } from "@excalidraw/utils/geometry/shape";
|
||||
deconstructDiamondElement,
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
import type { Drawable, Op } from "roughjs/bin/core";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
import type {
|
||||
Arrowhead,
|
||||
ElementsMap,
|
||||
ElementsMapOrArray,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
NonDeleted,
|
||||
} from "./types";
|
||||
|
||||
export type RectangleBox = {
|
||||
x: number;
|
||||
@ -247,50 +272,82 @@ export const getElementAbsoluteCoords = (
|
||||
* that can be used for visual collision detection (useful for frames)
|
||||
* as opposed to bounding box collision detection
|
||||
*/
|
||||
/**
|
||||
* Given an element, return the line segments that make up the element.
|
||||
*
|
||||
* Uses helpers from /math
|
||||
*/
|
||||
export const getElementLineSegments = (
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): LineSegment<GlobalPoint>[] => {
|
||||
const shape = getElementShape(element, elementsMap);
|
||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
elementsMap,
|
||||
);
|
||||
const center = pointFrom<GlobalPoint>(cx, cy);
|
||||
|
||||
const center: GlobalPoint = pointFrom(cx, cy);
|
||||
|
||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
|
||||
if (shape.type === "polycurve") {
|
||||
const curves = shape.data;
|
||||
const points = curves
|
||||
.map((curve) => pointsOnBezierCurves(curve, 10))
|
||||
.flat();
|
||||
let i = 0;
|
||||
|
||||
while (i < element.points.length - 1) {
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
while (i < points.length - 1) {
|
||||
segments.push(
|
||||
lineSegment(
|
||||
pointRotateRads(
|
||||
pointFrom(
|
||||
element.points[i][0] + element.x,
|
||||
element.points[i][1] + element.y,
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointRotateRads(
|
||||
pointFrom(
|
||||
element.points[i + 1][0] + element.x,
|
||||
element.points[i + 1][1] + element.y,
|
||||
),
|
||||
center,
|
||||
element.angle,
|
||||
),
|
||||
pointFrom(points[i][0], points[i][1]),
|
||||
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||
),
|
||||
);
|
||||
i++;
|
||||
}
|
||||
|
||||
return segments;
|
||||
} else if (shape.type === "polyline") {
|
||||
return shape.data as LineSegment<GlobalPoint>[];
|
||||
} else if (_isRectanguloidElement(element)) {
|
||||
const [sides, corners] = deconstructRectanguloidElement(element);
|
||||
const cornerSegments: LineSegment<GlobalPoint>[] = corners
|
||||
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||
.flat();
|
||||
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||
return [...rotatedSides, ...cornerSegments];
|
||||
} else if (element.type === "diamond") {
|
||||
const [sides, corners] = deconstructDiamondElement(element);
|
||||
const cornerSegments = corners
|
||||
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||
.flat();
|
||||
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||
|
||||
return [...rotatedSides, ...cornerSegments];
|
||||
} else if (shape.type === "polygon") {
|
||||
if (isTextElement(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (container && isLinearElement(container)) {
|
||||
const segments: LineSegment<GlobalPoint>[] = [
|
||||
lineSegment(pointFrom(x1, y1), pointFrom(x2, y1)),
|
||||
lineSegment(pointFrom(x2, y1), pointFrom(x2, y2)),
|
||||
lineSegment(pointFrom(x2, y2), pointFrom(x1, y2)),
|
||||
lineSegment(pointFrom(x1, y2), pointFrom(x1, y1)),
|
||||
];
|
||||
return segments;
|
||||
}
|
||||
}
|
||||
|
||||
const points = shape.data as GlobalPoint[];
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
segments.push(lineSegment(points[i], points[i + 1]));
|
||||
}
|
||||
return segments;
|
||||
} else if (shape.type === "ellipse") {
|
||||
return getSegmentsOnEllipse(element as ExcalidrawEllipseElement);
|
||||
}
|
||||
|
||||
const [nw, ne, sw, se, n, s, w, e] = (
|
||||
const [nw, ne, sw, se, , , w, e] = (
|
||||
[
|
||||
[x1, y1],
|
||||
[x2, y1],
|
||||
@ -303,28 +360,6 @@ export const getElementLineSegments = (
|
||||
] as GlobalPoint[]
|
||||
).map((point) => pointRotateRads(point, center, element.angle));
|
||||
|
||||
if (element.type === "diamond") {
|
||||
return [
|
||||
lineSegment(n, w),
|
||||
lineSegment(n, e),
|
||||
lineSegment(s, w),
|
||||
lineSegment(s, e),
|
||||
];
|
||||
}
|
||||
|
||||
if (element.type === "ellipse") {
|
||||
return [
|
||||
lineSegment(n, w),
|
||||
lineSegment(n, e),
|
||||
lineSegment(s, w),
|
||||
lineSegment(s, e),
|
||||
lineSegment(n, w),
|
||||
lineSegment(n, e),
|
||||
lineSegment(s, w),
|
||||
lineSegment(s, e),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
lineSegment(nw, ne),
|
||||
lineSegment(sw, se),
|
||||
@ -337,6 +372,94 @@ export const getElementLineSegments = (
|
||||
];
|
||||
};
|
||||
|
||||
const _isRectanguloidElement = (
|
||||
element: ExcalidrawElement,
|
||||
): element is ExcalidrawRectanguloidElement => {
|
||||
return (
|
||||
element != null &&
|
||||
(element.type === "rectangle" ||
|
||||
element.type === "image" ||
|
||||
element.type === "iframe" ||
|
||||
element.type === "embeddable" ||
|
||||
element.type === "frame" ||
|
||||
element.type === "magicframe" ||
|
||||
(element.type === "text" && !element.containerId))
|
||||
);
|
||||
};
|
||||
|
||||
const getRotatedSides = (
|
||||
sides: LineSegment<GlobalPoint>[],
|
||||
center: GlobalPoint,
|
||||
angle: Radians,
|
||||
) => {
|
||||
return sides.map((side) => {
|
||||
return lineSegment(
|
||||
pointRotateRads<GlobalPoint>(side[0], center, angle),
|
||||
pointRotateRads<GlobalPoint>(side[1], center, angle),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getSegmentsOnCurve = (
|
||||
curve: Curve<GlobalPoint>,
|
||||
center: GlobalPoint,
|
||||
angle: Radians,
|
||||
): LineSegment<GlobalPoint>[] => {
|
||||
const points = pointsOnBezierCurves(curve, 10);
|
||||
let i = 0;
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
while (i < points.length - 1) {
|
||||
segments.push(
|
||||
lineSegment(
|
||||
pointRotateRads<GlobalPoint>(
|
||||
pointFrom(points[i][0], points[i][1]),
|
||||
center,
|
||||
angle,
|
||||
),
|
||||
pointRotateRads<GlobalPoint>(
|
||||
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||
center,
|
||||
angle,
|
||||
),
|
||||
),
|
||||
);
|
||||
i++;
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
|
||||
const getSegmentsOnEllipse = (
|
||||
ellipse: ExcalidrawEllipseElement,
|
||||
): LineSegment<GlobalPoint>[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
ellipse.x + ellipse.width / 2,
|
||||
ellipse.y + ellipse.height / 2,
|
||||
);
|
||||
|
||||
const a = ellipse.width / 2;
|
||||
const b = ellipse.height / 2;
|
||||
|
||||
const segments: LineSegment<GlobalPoint>[] = [];
|
||||
const points: GlobalPoint[] = [];
|
||||
const n = 90;
|
||||
const deltaT = (Math.PI * 2) / n;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = i * deltaT;
|
||||
const x = center[0] + a * Math.cos(t);
|
||||
const y = center[1] + b * Math.sin(t);
|
||||
points.push(pointRotateRads(pointFrom(x, y), center, ellipse.angle));
|
||||
}
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
segments.push(lineSegment(points[i], points[i + 1]));
|
||||
}
|
||||
|
||||
segments.push(lineSegment(points[points.length - 1], points[0]));
|
||||
return segments;
|
||||
};
|
||||
|
||||
/**
|
||||
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
||||
*
|
||||
@ -821,10 +944,10 @@ export const getElementBounds = (
|
||||
};
|
||||
|
||||
export const getCommonBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: ElementsMapOrArray,
|
||||
elementsMap?: ElementsMap,
|
||||
): Bounds => {
|
||||
if (!elements.length) {
|
||||
if (!sizeOf(elements)) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
@ -1,31 +1,4 @@
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
} from "./types";
|
||||
import { getElementBounds } from "./bounds";
|
||||
import type { FrameNameBounds } from "../types";
|
||||
import type { GeometricShape } from "@excalidraw/utils/geometry/shape";
|
||||
import { getPolygonShape } from "@excalidraw/utils/geometry/shape";
|
||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||
import { isTransparent } from "../utils";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isIframeLikeElement,
|
||||
isImageElement,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import { getBoundTextShape, isPathALoop } from "../shapes";
|
||||
import type {
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
LocalPoint,
|
||||
Polygon,
|
||||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
import { isTransparent, elementCenterPoint } from "@excalidraw/common";
|
||||
import {
|
||||
curveIntersectLineSegment,
|
||||
isPointWithinBounds,
|
||||
@ -36,15 +9,47 @@ import {
|
||||
pointRotateRads,
|
||||
pointsEqual,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
ellipse,
|
||||
ellipseLineIntersectionPoints,
|
||||
} from "@excalidraw/math/ellipse";
|
||||
|
||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
||||
|
||||
import type {
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
LocalPoint,
|
||||
Polygon,
|
||||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { getBoundTextShape, isPathALoop } from "./shapes";
|
||||
import { getElementBounds } from "./bounds";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isIframeLikeElement,
|
||||
isImageElement,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import {
|
||||
deconstructDiamondElement,
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
} from "./types";
|
||||
|
||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||
if (element.type === "arrow") {
|
||||
return false;
|
||||
@ -184,10 +189,7 @@ const intersectRectanguloidWithLineSegment = (
|
||||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||
// instead. It's all the same distance-wise.
|
||||
const rotatedA = pointRotateRads<GlobalPoint>(
|
||||
@ -205,28 +207,28 @@ const intersectRectanguloidWithLineSegment = (
|
||||
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
||||
|
||||
return (
|
||||
[
|
||||
// Test intersection against the sides, keep only the valid
|
||||
// intersection points and rotate them back to scene space
|
||||
...sides
|
||||
.map((s) =>
|
||||
lineSegmentIntersectionPoints(
|
||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
||||
s,
|
||||
),
|
||||
)
|
||||
.filter((x) => x != null)
|
||||
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle)),
|
||||
// Test intersection against the sides, keep only the valid
|
||||
// intersection points and rotate them back to scene space
|
||||
sides
|
||||
.map((s) =>
|
||||
lineSegmentIntersectionPoints(
|
||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
||||
s,
|
||||
),
|
||||
)
|
||||
.filter((x) => x != null)
|
||||
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle))
|
||||
// Test intersection against the corners which are cubic bezier curves,
|
||||
// keep only the valid intersection points and rotate them back to scene
|
||||
// space
|
||||
...corners
|
||||
.flatMap((t) =>
|
||||
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
|
||||
)
|
||||
.filter((i) => i != null)
|
||||
.map((j) => pointRotateRads(j, center, element.angle)),
|
||||
]
|
||||
.concat(
|
||||
corners
|
||||
.flatMap((t) =>
|
||||
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
|
||||
)
|
||||
.filter((i) => i != null)
|
||||
.map((j) => pointRotateRads(j, center, element.angle)),
|
||||
)
|
||||
// Remove duplicates
|
||||
.filter(
|
||||
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
||||
@ -246,10 +248,7 @@ const intersectDiamondWithLineSegment = (
|
||||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||
// points. It's all the same distance-wise.
|
||||
@ -259,25 +258,25 @@ const intersectDiamondWithLineSegment = (
|
||||
const [sides, curves] = deconstructDiamondElement(element, offset);
|
||||
|
||||
return (
|
||||
[
|
||||
...sides
|
||||
.map((s) =>
|
||||
lineSegmentIntersectionPoints(
|
||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
||||
s,
|
||||
),
|
||||
)
|
||||
.filter((p): p is GlobalPoint => p != null)
|
||||
// Rotate back intersection points
|
||||
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle)),
|
||||
...curves
|
||||
.flatMap((p) =>
|
||||
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
|
||||
)
|
||||
.filter((p) => p != null)
|
||||
// Rotate back intersection points
|
||||
.map((p) => pointRotateRads(p, center, element.angle)),
|
||||
]
|
||||
sides
|
||||
.map((s) =>
|
||||
lineSegmentIntersectionPoints(
|
||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
||||
s,
|
||||
),
|
||||
)
|
||||
.filter((p): p is GlobalPoint => p != null)
|
||||
// Rotate back intersection points
|
||||
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle))
|
||||
.concat(
|
||||
curves
|
||||
.flatMap((p) =>
|
||||
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
|
||||
)
|
||||
.filter((p) => p != null)
|
||||
// Rotate back intersection points
|
||||
.map((p) => pointRotateRads(p, center, element.angle)),
|
||||
)
|
||||
// Remove duplicates
|
||||
.filter(
|
||||
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
||||
@ -297,10 +296,7 @@ const intersectEllipseWithLineSegment = (
|
||||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
@ -1,4 +1,4 @@
|
||||
import type { ElementOrToolType } from "../types";
|
||||
import type { ElementOrToolType } from "@excalidraw/excalidraw/types";
|
||||
|
||||
export const hasBackground = (type: ElementOrToolType) =>
|
||||
type === "rectangle" ||
|
@ -1,4 +1,3 @@
|
||||
import { type Point } from "points-on-curve";
|
||||
import {
|
||||
type Radians,
|
||||
pointFrom,
|
||||
@ -13,6 +12,15 @@ import {
|
||||
clamp,
|
||||
isCloseTo,
|
||||
} from "@excalidraw/math";
|
||||
import { type Point } from "points-on-curve";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
getResizedElementAbsoluteCoords,
|
||||
} from "./bounds";
|
||||
|
||||
import type { TransformHandleType } from "./transformHandles";
|
||||
import type {
|
||||
ElementsMap,
|
||||
@ -21,10 +29,6 @@ import type {
|
||||
ImageCrop,
|
||||
NonDeleted,
|
||||
} from "./types";
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
getResizedElementAbsoluteCoords,
|
||||
} from "./bounds";
|
||||
|
||||
export const MINIMAL_CROP_SIZE = 10;
|
||||
|
||||
@ -59,7 +63,7 @@ export const cropElement = (
|
||||
|
||||
const rotatedPointer = pointRotateRads(
|
||||
pointFrom(pointerX, pointerY),
|
||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
||||
elementCenterPoint(element),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,26 @@
|
||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||
import {
|
||||
curvePointDistance,
|
||||
distanceToLineSegment,
|
||||
pointFrom,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
deconstructDiamondElement,
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
import type {
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
} from "./types";
|
||||
import {
|
||||
deconstructDiamondElement,
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
export const distanceToBindableElement = (
|
||||
element: ExcalidrawBindableElement,
|
||||
@ -49,10 +54,7 @@ const distanceToRectanguloidElement = (
|
||||
element: ExcalidrawRectanguloidElement,
|
||||
p: GlobalPoint,
|
||||
) => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||
// instead. It's all the same distance-wise.
|
||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||
@ -80,10 +82,7 @@ const distanceToDiamondElement = (
|
||||
element: ExcalidrawDiamondElement,
|
||||
p: GlobalPoint,
|
||||
): number => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||
// points. It's all the same distance-wise.
|
||||
@ -111,10 +110,7 @@ const distanceToEllipseElement = (
|
||||
element: ExcalidrawEllipseElement,
|
||||
p: GlobalPoint,
|
||||
): number => {
|
||||
const center = pointFrom(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
return ellipseDistanceFromPoint(
|
||||
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
||||
pointRotateRads(p, center, -element.angle as Radians),
|
@ -1,7 +1,9 @@
|
||||
import { newElementWith } from "./element/mutateElement";
|
||||
import { getCommonBoundingBox } from "./bounds";
|
||||
import { newElementWith } from "./mutateElement";
|
||||
|
||||
import { getMaximumGroups } from "./groups";
|
||||
import { getCommonBoundingBox } from "./element/bounds";
|
||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
||||
|
||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||
|
||||
export interface Distribution {
|
||||
space: "between";
|
@ -1,17 +1,23 @@
|
||||
import { updateBoundElements } from "./binding";
|
||||
import type { Bounds } from "./bounds";
|
||||
import { getCommonBounds } from "./bounds";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getPerfectElementSize } from "./sizeHelpers";
|
||||
import type { NonDeletedExcalidrawElement } from "./types";
|
||||
import {
|
||||
TEXT_AUTOWRAP_THRESHOLD,
|
||||
getGridPoint,
|
||||
getFontString,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type {
|
||||
AppState,
|
||||
NormalizedZoomValue,
|
||||
NullableGridSize,
|
||||
PointerDownState,
|
||||
} from "../types";
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { updateBoundElements } from "./binding";
|
||||
import { getCommonBounds } from "./bounds";
|
||||
import { getPerfectElementSize } from "./sizeHelpers";
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
import type Scene from "../scene/Scene";
|
||||
import { getMinTextElementWidth } from "./textMeasurements";
|
||||
import {
|
||||
isArrowElement,
|
||||
isElbowArrow,
|
||||
@ -19,10 +25,11 @@ import {
|
||||
isImageElement,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import { getFontString } from "../utils";
|
||||
import { TEXT_AUTOWRAP_THRESHOLD } from "../constants";
|
||||
import { getGridPoint } from "../snapping";
|
||||
import { getMinTextElementWidth } from "./textMeasurements";
|
||||
|
||||
import type { Scene } from "./Scene";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
import type { ExcalidrawElement } from "./types";
|
||||
|
||||
export const dragSelectedElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
@ -76,20 +83,27 @@ export const dragSelectedElements = (
|
||||
}
|
||||
}
|
||||
|
||||
const commonBounds = getCommonBounds(
|
||||
Array.from(elementsToUpdate).map(
|
||||
(el) => pointerDownState.originalElements.get(el.id) ?? el,
|
||||
),
|
||||
);
|
||||
const origElements: ExcalidrawElement[] = [];
|
||||
|
||||
for (const element of elementsToUpdate) {
|
||||
const origElement = pointerDownState.originalElements.get(element.id);
|
||||
// if original element is not set (e.g. when you duplicate during a drag
|
||||
// operation), exit to avoid undefined behavior
|
||||
if (!origElement) {
|
||||
return;
|
||||
}
|
||||
origElements.push(origElement);
|
||||
}
|
||||
|
||||
const adjustedOffset = calculateOffset(
|
||||
commonBounds,
|
||||
getCommonBounds(origElements),
|
||||
offset,
|
||||
snapOffset,
|
||||
gridSize,
|
||||
);
|
||||
|
||||
elementsToUpdate.forEach((element) => {
|
||||
updateElementCoords(pointerDownState, element, adjustedOffset);
|
||||
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||
if (!isArrowElement(element)) {
|
||||
// skip arrow labels since we calculate its position during render
|
||||
const textElement = getBoundTextElement(
|
||||
@ -97,9 +111,14 @@ export const dragSelectedElements = (
|
||||
scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (textElement) {
|
||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||
updateElementCoords(
|
||||
pointerDownState,
|
||||
textElement,
|
||||
scene,
|
||||
adjustedOffset,
|
||||
);
|
||||
}
|
||||
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||
});
|
||||
}
|
||||
@ -140,6 +159,7 @@ const calculateOffset = (
|
||||
const updateElementCoords = (
|
||||
pointerDownState: PointerDownState,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
scene: Scene,
|
||||
dragOffset: { x: number; y: number },
|
||||
) => {
|
||||
const originalElement =
|
||||
@ -148,7 +168,7 @@ const updateElementCoords = (
|
||||
const nextX = originalElement.x + dragOffset.x;
|
||||
const nextY = originalElement.y + dragOffset.y;
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
x: nextX,
|
||||
y: nextY,
|
||||
});
|
||||
@ -175,6 +195,7 @@ export const dragNewElement = ({
|
||||
shouldMaintainAspectRatio,
|
||||
shouldResizeFromCenter,
|
||||
zoom,
|
||||
scene,
|
||||
widthAspectRatio = null,
|
||||
originOffset = null,
|
||||
informMutation = true,
|
||||
@ -190,6 +211,7 @@ export const dragNewElement = ({
|
||||
shouldMaintainAspectRatio: boolean;
|
||||
shouldResizeFromCenter: boolean;
|
||||
zoom: NormalizedZoomValue;
|
||||
scene: Scene;
|
||||
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
||||
true */
|
||||
widthAspectRatio?: number | null;
|
||||
@ -270,7 +292,7 @@ export const dragNewElement = ({
|
||||
};
|
||||
}
|
||||
|
||||
mutateElement(
|
||||
scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
x: newX + (originOffset?.x ?? 0),
|
||||
@ -280,7 +302,7 @@ export const dragNewElement = ({
|
||||
...textAutoResize,
|
||||
...imageInitialDimension,
|
||||
},
|
||||
informMutation,
|
||||
{ informMutation, isDragging: false },
|
||||
);
|
||||
}
|
||||
};
|
487
packages/element/src/duplicate.ts
Normal file
487
packages/element/src/duplicate.ts
Normal file
@ -0,0 +1,487 @@
|
||||
import {
|
||||
ORIG_ID,
|
||||
randomId,
|
||||
randomInteger,
|
||||
arrayToMap,
|
||||
castArray,
|
||||
findLastIndex,
|
||||
getUpdatedTimestamp,
|
||||
isTestEnv,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import {
|
||||
getElementsInGroup,
|
||||
getNewGroupIdsForDuplication,
|
||||
getSelectedGroupForElement,
|
||||
} from "./groups";
|
||||
|
||||
import {
|
||||
bindElementsToFramesAfterDuplication,
|
||||
getFrameChildren,
|
||||
} from "./frame";
|
||||
|
||||
import { normalizeElementOrder } from "./sortElements";
|
||||
|
||||
import { bumpVersion } from "./mutateElement";
|
||||
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isBoundToContainer,
|
||||
isFrameLikeElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
|
||||
import { fixDuplicatedBindingsAfterDuplication } from "./binding";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
GroupId,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Duplicate an element, often used in the alt-drag operation.
|
||||
* Note that this method has gotten a bit complicated since the
|
||||
* introduction of gruoping/ungrouping elements.
|
||||
* @param editingGroupId The current group being edited. The new
|
||||
* element will inherit this group and its
|
||||
* parents.
|
||||
* @param groupIdMapForOperation A Map that maps old group IDs to
|
||||
* duplicated ones. If you are duplicating
|
||||
* multiple elements at once, share this map
|
||||
* amongst all of them
|
||||
* @param element Element to duplicate
|
||||
*/
|
||||
export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||
editingGroupId: AppState["editingGroupId"],
|
||||
groupIdMapForOperation: Map<GroupId, GroupId>,
|
||||
element: TElement,
|
||||
randomizeSeed?: boolean,
|
||||
): Readonly<TElement> => {
|
||||
const copy = deepCopyElement(element);
|
||||
|
||||
if (isTestEnv()) {
|
||||
__test__defineOrigId(copy, element.id);
|
||||
}
|
||||
|
||||
copy.id = randomId();
|
||||
copy.updated = getUpdatedTimestamp();
|
||||
if (randomizeSeed) {
|
||||
copy.seed = randomInteger();
|
||||
bumpVersion(copy);
|
||||
}
|
||||
|
||||
copy.groupIds = getNewGroupIdsForDuplication(
|
||||
copy.groupIds,
|
||||
editingGroupId,
|
||||
(groupId) => {
|
||||
if (!groupIdMapForOperation.has(groupId)) {
|
||||
groupIdMapForOperation.set(groupId, randomId());
|
||||
}
|
||||
return groupIdMapForOperation.get(groupId)!;
|
||||
},
|
||||
);
|
||||
return copy;
|
||||
};
|
||||
|
||||
export const duplicateElements = (
|
||||
opts: {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
randomizeSeed?: boolean;
|
||||
overrides?: (data: {
|
||||
duplicateElement: ExcalidrawElement;
|
||||
origElement: ExcalidrawElement;
|
||||
origIdToDuplicateId: Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement["id"]
|
||||
>;
|
||||
}) => Partial<ExcalidrawElement>;
|
||||
} & (
|
||||
| {
|
||||
/**
|
||||
* Duplicates all elements in array.
|
||||
*
|
||||
* Use this when programmaticaly duplicating elements, without direct
|
||||
* user interaction.
|
||||
*/
|
||||
type: "everything";
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Duplicates specified elements and inserts them back into the array
|
||||
* in specified order.
|
||||
*
|
||||
* Use this when duplicating Scene elements, during user interaction
|
||||
* such as alt-drag or on duplicate action.
|
||||
*/
|
||||
type: "in-place";
|
||||
idsOfElementsToDuplicate: Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement
|
||||
>;
|
||||
appState: {
|
||||
editingGroupId: AppState["editingGroupId"];
|
||||
selectedGroupIds: AppState["selectedGroupIds"];
|
||||
};
|
||||
}
|
||||
),
|
||||
) => {
|
||||
let { elements } = opts;
|
||||
|
||||
const appState =
|
||||
"appState" in opts
|
||||
? opts.appState
|
||||
: ({
|
||||
editingGroupId: null,
|
||||
selectedGroupIds: {},
|
||||
} as const);
|
||||
|
||||
// Ids of elements that have already been processed so we don't push them
|
||||
// into the array twice if we end up backtracking when retrieving
|
||||
// discontiguous group of elements (can happen due to a bug, or in edge
|
||||
// cases such as a group containing deleted elements which were not selected).
|
||||
//
|
||||
// This is not enough to prevent duplicates, so we do a second loop afterwards
|
||||
// to remove them.
|
||||
//
|
||||
// For convenience we mark even the newly created ones even though we don't
|
||||
// loop over them.
|
||||
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
||||
const groupIdMap = new Map();
|
||||
const duplicatedElements: ExcalidrawElement[] = [];
|
||||
const origElements: ExcalidrawElement[] = [];
|
||||
const origIdToDuplicateId = new Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement["id"]
|
||||
>();
|
||||
const duplicateIdToOrigElement = new Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement
|
||||
>();
|
||||
const duplicateElementsMap = new Map<string, ExcalidrawElement>();
|
||||
const elementsMap = arrayToMap(elements) as ElementsMap;
|
||||
const _idsOfElementsToDuplicate =
|
||||
opts.type === "in-place"
|
||||
? opts.idsOfElementsToDuplicate
|
||||
: new Map(elements.map((el) => [el.id, el]));
|
||||
|
||||
// For sanity
|
||||
if (opts.type === "in-place") {
|
||||
for (const groupId of Object.keys(opts.appState.selectedGroupIds)) {
|
||||
elements
|
||||
.filter((el) => el.groupIds?.includes(groupId))
|
||||
.forEach((el) => _idsOfElementsToDuplicate.set(el.id, el));
|
||||
}
|
||||
}
|
||||
|
||||
elements = normalizeElementOrder(elements);
|
||||
|
||||
const elementsWithDuplicates: ExcalidrawElement[] = elements.slice();
|
||||
|
||||
// helper functions
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Used for the heavy lifing of copying a single element, a group of elements
|
||||
// an element with bound text etc.
|
||||
const copyElements = <T extends ExcalidrawElement | ExcalidrawElement[]>(
|
||||
element: T,
|
||||
): T extends ExcalidrawElement[]
|
||||
? ExcalidrawElement[]
|
||||
: ExcalidrawElement | null => {
|
||||
const elements = castArray(element);
|
||||
|
||||
const _newElements = elements.reduce(
|
||||
(acc: ExcalidrawElement[], element) => {
|
||||
if (processedIds.has(element.id)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
processedIds.set(element.id, true);
|
||||
|
||||
const newElement = duplicateElement(
|
||||
appState.editingGroupId,
|
||||
groupIdMap,
|
||||
element,
|
||||
opts.randomizeSeed,
|
||||
);
|
||||
|
||||
processedIds.set(newElement.id, true);
|
||||
|
||||
duplicateElementsMap.set(newElement.id, newElement);
|
||||
origIdToDuplicateId.set(element.id, newElement.id);
|
||||
duplicateIdToOrigElement.set(newElement.id, element);
|
||||
|
||||
origElements.push(element);
|
||||
duplicatedElements.push(newElement);
|
||||
|
||||
acc.push(newElement);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
Array.isArray(element) ? _newElements : _newElements[0] || null
|
||||
) as T extends ExcalidrawElement[]
|
||||
? ExcalidrawElement[]
|
||||
: ExcalidrawElement | null;
|
||||
};
|
||||
|
||||
// Helper to position cloned elements in the Z-order the product needs it
|
||||
const insertBeforeOrAfterIndex = (
|
||||
index: number,
|
||||
elements: ExcalidrawElement | null | ExcalidrawElement[],
|
||||
) => {
|
||||
if (!elements) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index > elementsWithDuplicates.length - 1) {
|
||||
elementsWithDuplicates.push(...castArray(elements));
|
||||
return;
|
||||
}
|
||||
|
||||
elementsWithDuplicates.splice(index + 1, 0, ...castArray(elements));
|
||||
};
|
||||
|
||||
const frameIdsToDuplicate = new Set(
|
||||
elements
|
||||
.filter(
|
||||
(el) => _idsOfElementsToDuplicate.has(el.id) && isFrameLikeElement(el),
|
||||
)
|
||||
.map((el) => el.id),
|
||||
);
|
||||
|
||||
for (const element of elements) {
|
||||
if (processedIds.has(element.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_idsOfElementsToDuplicate.has(element.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// groups
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const groupId = getSelectedGroupForElement(appState, element);
|
||||
if (groupId) {
|
||||
const groupElements = getElementsInGroup(elements, groupId).flatMap(
|
||||
(element) =>
|
||||
isFrameLikeElement(element)
|
||||
? [...getFrameChildren(elements, element.id), element]
|
||||
: [element],
|
||||
);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return el.groupIds?.includes(groupId);
|
||||
});
|
||||
|
||||
insertBeforeOrAfterIndex(targetIndex, copyElements(groupElements));
|
||||
continue;
|
||||
}
|
||||
|
||||
// frame duplication
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
if (element.frameId && frameIdsToDuplicate.has(element.frameId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isFrameLikeElement(element)) {
|
||||
const frameId = element.id;
|
||||
|
||||
const frameChildren = getFrameChildren(elements, frameId);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return el.frameId === frameId || el.id === frameId;
|
||||
});
|
||||
|
||||
insertBeforeOrAfterIndex(
|
||||
targetIndex,
|
||||
copyElements([...frameChildren, element]),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// text container
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
if (hasBoundTextElement(element)) {
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return (
|
||||
el.id === element.id ||
|
||||
("containerId" in el && el.containerId === element.id)
|
||||
);
|
||||
});
|
||||
|
||||
if (boundTextElement) {
|
||||
insertBeforeOrAfterIndex(
|
||||
targetIndex,
|
||||
copyElements([element, boundTextElement]),
|
||||
);
|
||||
} else {
|
||||
insertBeforeOrAfterIndex(targetIndex, copyElements(element));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isBoundToContainer(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return el.id === element.id || el.id === container?.id;
|
||||
});
|
||||
|
||||
if (container) {
|
||||
insertBeforeOrAfterIndex(
|
||||
targetIndex,
|
||||
copyElements([container, element]),
|
||||
);
|
||||
} else {
|
||||
insertBeforeOrAfterIndex(targetIndex, copyElements(element));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// default duplication (regular elements)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
insertBeforeOrAfterIndex(
|
||||
findLastIndex(elementsWithDuplicates, (el) => el.id === element.id),
|
||||
copyElements(element),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fixDuplicatedBindingsAfterDuplication(
|
||||
duplicatedElements,
|
||||
origIdToDuplicateId,
|
||||
duplicateElementsMap as NonDeletedSceneElementsMap,
|
||||
);
|
||||
|
||||
bindElementsToFramesAfterDuplication(
|
||||
elementsWithDuplicates,
|
||||
origElements,
|
||||
origIdToDuplicateId,
|
||||
);
|
||||
|
||||
if (opts.overrides) {
|
||||
for (const duplicateElement of duplicatedElements) {
|
||||
const origElement = duplicateIdToOrigElement.get(duplicateElement.id);
|
||||
if (origElement) {
|
||||
Object.assign(
|
||||
duplicateElement,
|
||||
opts.overrides({
|
||||
duplicateElement,
|
||||
origElement,
|
||||
origIdToDuplicateId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
duplicatedElements,
|
||||
duplicateElementsMap,
|
||||
elementsWithDuplicates,
|
||||
origIdToDuplicateId,
|
||||
};
|
||||
};
|
||||
|
||||
// Simplified deep clone for the purpose of cloning ExcalidrawElement.
|
||||
//
|
||||
// Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
|
||||
// Typed arrays and other non-null objects.
|
||||
//
|
||||
// Adapted from https://github.com/lukeed/klona
|
||||
//
|
||||
// The reason for `deepCopyElement()` wrapper is type safety (only allow
|
||||
// passing ExcalidrawElement as the top-level argument).
|
||||
const _deepCopyElement = (val: any, depth: number = 0) => {
|
||||
// only clone non-primitives
|
||||
if (val == null || typeof val !== "object") {
|
||||
return val;
|
||||
}
|
||||
|
||||
const objectType = Object.prototype.toString.call(val);
|
||||
|
||||
if (objectType === "[object Object]") {
|
||||
const tmp =
|
||||
typeof val.constructor === "function"
|
||||
? Object.create(Object.getPrototypeOf(val))
|
||||
: {};
|
||||
for (const key in val) {
|
||||
if (val.hasOwnProperty(key)) {
|
||||
// don't copy non-serializable objects like these caches. They'll be
|
||||
// populated when the element is rendered.
|
||||
if (depth === 0 && (key === "shape" || key === "canvas")) {
|
||||
continue;
|
||||
}
|
||||
tmp[key] = _deepCopyElement(val[key], depth + 1);
|
||||
}
|
||||
}
|
||||
return tmp;
|
||||
}
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
let k = val.length;
|
||||
const arr = new Array(k);
|
||||
while (k--) {
|
||||
arr[k] = _deepCopyElement(val[k], depth + 1);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
// we're not cloning non-array & non-plain-object objects because we
|
||||
// don't support them on excalidraw elements yet. If we do, we need to make
|
||||
// sure we start cloning them, so let's warn about it.
|
||||
if (import.meta.env.DEV) {
|
||||
if (
|
||||
objectType !== "[object Object]" &&
|
||||
objectType !== "[object Array]" &&
|
||||
objectType.startsWith("[object ")
|
||||
) {
|
||||
console.warn(
|
||||
`_deepCloneElement: unexpected object type ${objectType}. This value will not be cloned!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clones ExcalidrawElement data structure. Does not regenerate id, nonce, or
|
||||
* any value. The purpose is to to break object references for immutability
|
||||
* reasons, whenever we want to keep the original element, but ensure it's not
|
||||
* mutated.
|
||||
*
|
||||
* Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
|
||||
* Typed arrays and other non-null objects.
|
||||
*/
|
||||
export const deepCopyElement = <T extends ExcalidrawElement>(
|
||||
val: T,
|
||||
): Mutable<T> => {
|
||||
return _deepCopyElement(val);
|
||||
};
|
||||
|
||||
const __test__defineOrigId = (clonedObj: object, origId: string) => {
|
||||
Object.defineProperty(clonedObj, ORIG_ID, {
|
||||
value: origId,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
});
|
||||
};
|
@ -12,11 +12,18 @@ import {
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
} from "@excalidraw/math";
|
||||
import BinaryHeap from "../binaryheap";
|
||||
import { getSizeFromPoints } from "../points";
|
||||
import { aabbForElement, pointInsideBounds } from "../shapes";
|
||||
import { invariant, isAnyTrue, tupleToCoors } from "../utils";
|
||||
import type { AppState } from "../types";
|
||||
|
||||
import {
|
||||
BinaryHeap,
|
||||
invariant,
|
||||
isAnyTrue,
|
||||
tupleToCoors,
|
||||
getSizeFromPoints,
|
||||
isDevEnv,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import {
|
||||
bindPointToSnapToElementOutline,
|
||||
FIXED_BINDING_DISTANCE,
|
||||
@ -25,8 +32,7 @@ import {
|
||||
snapToMid,
|
||||
getHoveredElementForBinding,
|
||||
} from "./binding";
|
||||
import type { Bounds } from "./bounds";
|
||||
import type { Heading } from "./heading";
|
||||
import { distanceToBindableElement } from "./distance";
|
||||
import {
|
||||
compareHeading,
|
||||
flipHeading,
|
||||
@ -44,8 +50,12 @@ import { isBindableElement } from "./typeChecks";
|
||||
import {
|
||||
type ExcalidrawElbowArrowElement,
|
||||
type NonDeletedSceneElementsMap,
|
||||
type SceneElementsMap,
|
||||
} from "./types";
|
||||
|
||||
import { aabbForElement, pointInsideBounds } from "./shapes";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
import type { Heading } from "./heading";
|
||||
import type {
|
||||
Arrowhead,
|
||||
ElementsMap,
|
||||
@ -54,7 +64,6 @@ import type {
|
||||
FixedSegment,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./types";
|
||||
import { distanceToBindableElement } from "./distance";
|
||||
|
||||
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
||||
|
||||
@ -235,16 +244,6 @@ const handleSegmentRenormalization = (
|
||||
nextPoints.map((p) =>
|
||||
pointFrom<LocalPoint>(p[0] - arrow.x, p[1] - arrow.y),
|
||||
),
|
||||
arrow.startBinding &&
|
||||
getBindableElementForId(
|
||||
arrow.startBinding.elementId,
|
||||
elementsMap,
|
||||
),
|
||||
arrow.endBinding &&
|
||||
getBindableElementForId(
|
||||
arrow.endBinding.elementId,
|
||||
elementsMap,
|
||||
),
|
||||
),
|
||||
) ?? [],
|
||||
),
|
||||
@ -255,7 +254,7 @@ const handleSegmentRenormalization = (
|
||||
);
|
||||
}
|
||||
|
||||
import.meta.env.DEV &&
|
||||
isDevEnv() &&
|
||||
invariant(
|
||||
validateElbowPoints(nextPoints),
|
||||
"Invalid elbow points with fixed segments",
|
||||
@ -338,9 +337,6 @@ const handleSegmentRelease = (
|
||||
y,
|
||||
),
|
||||
],
|
||||
startBinding &&
|
||||
getBindableElementForId(startBinding.elementId, elementsMap),
|
||||
endBinding && getBindableElementForId(endBinding.elementId, elementsMap),
|
||||
{ isDragging: false },
|
||||
);
|
||||
|
||||
@ -890,7 +886,7 @@ export const updateElbowArrowPoints = (
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
updates: {
|
||||
points?: readonly LocalPoint[];
|
||||
fixedSegments?: FixedSegment[] | null;
|
||||
fixedSegments?: readonly FixedSegment[] | null;
|
||||
startBinding?: FixedPointBinding | null;
|
||||
endBinding?: FixedPointBinding | null;
|
||||
},
|
||||
@ -980,6 +976,8 @@ export const updateElbowArrowPoints = (
|
||||
);
|
||||
}
|
||||
|
||||
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
||||
|
||||
const updatedPoints: readonly LocalPoint[] = updates.points
|
||||
? updates.points && updates.points.length === 2
|
||||
? arrow.points.map((p, idx) =>
|
||||
@ -992,26 +990,36 @@ export const updateElbowArrowPoints = (
|
||||
: updates.points.slice()
|
||||
: arrow.points.slice();
|
||||
|
||||
// 0. During all element replacement in the scene, we just need to renormalize
|
||||
// During all element replacement in the scene, we just need to renormalize
|
||||
// the arrow
|
||||
// TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed
|
||||
const {
|
||||
startBinding: updatedStartBinding,
|
||||
endBinding: updatedEndBinding,
|
||||
...restOfTheUpdates
|
||||
} = updates;
|
||||
const startBinding =
|
||||
typeof updates.startBinding !== "undefined"
|
||||
? updates.startBinding
|
||||
typeof updatedStartBinding !== "undefined"
|
||||
? updatedStartBinding
|
||||
: arrow.startBinding;
|
||||
const endBinding =
|
||||
typeof updates.endBinding !== "undefined"
|
||||
? updates.endBinding
|
||||
typeof updatedEndBinding !== "undefined"
|
||||
? updatedEndBinding
|
||||
: arrow.endBinding;
|
||||
const startElement =
|
||||
startBinding &&
|
||||
getBindableElementForId(startBinding.elementId, elementsMap);
|
||||
const endElement =
|
||||
endBinding && getBindableElementForId(endBinding.elementId, elementsMap);
|
||||
const areUpdatedPointsValid = validateElbowPoints(updatedPoints);
|
||||
|
||||
if (
|
||||
(elementsMap.size === 0 && validateElbowPoints(updatedPoints)) ||
|
||||
startElement?.id !== startBinding?.elementId ||
|
||||
endElement?.id !== endBinding?.elementId
|
||||
(startBinding && !startElement && areUpdatedPointsValid) ||
|
||||
(endBinding && !endElement && areUpdatedPointsValid) ||
|
||||
(elementsMap.size === 0 && areUpdatedPointsValid) ||
|
||||
(Object.keys(restOfTheUpdates).length === 0 &&
|
||||
(startElement?.id !== startBinding?.elementId ||
|
||||
endElement?.id !== endBinding?.elementId))
|
||||
) {
|
||||
return normalizeArrowElementUpdate(
|
||||
updatedPoints.map((p) =>
|
||||
@ -1043,12 +1051,22 @@ export const updateElbowArrowPoints = (
|
||||
},
|
||||
elementsMap,
|
||||
updatedPoints,
|
||||
startElement,
|
||||
endElement,
|
||||
options,
|
||||
);
|
||||
|
||||
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
||||
// 0. During all element replacement in the scene, we just need to renormalize
|
||||
// the arrow
|
||||
// TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed
|
||||
if (elementsMap.size === 0 && areUpdatedPointsValid) {
|
||||
return normalizeArrowElementUpdate(
|
||||
updatedPoints.map((p) =>
|
||||
pointFrom<GlobalPoint>(arrow.x + p[0], arrow.y + p[1]),
|
||||
),
|
||||
arrow.fixedSegments,
|
||||
arrow.startIsSpecial,
|
||||
arrow.endIsSpecial,
|
||||
);
|
||||
}
|
||||
|
||||
////
|
||||
// 1. Renormalize the arrow
|
||||
@ -1071,7 +1089,8 @@ export const updateElbowArrowPoints = (
|
||||
p,
|
||||
arrow.points[i] ?? pointFrom<LocalPoint>(Infinity, Infinity),
|
||||
),
|
||||
)
|
||||
) &&
|
||||
areUpdatedPointsValid
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
@ -1182,8 +1201,6 @@ const getElbowArrowData = (
|
||||
},
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
nextPoints: readonly LocalPoint[],
|
||||
startElement: ExcalidrawBindableElement | null,
|
||||
endElement: ExcalidrawBindableElement | null,
|
||||
options?: {
|
||||
isDragging?: boolean;
|
||||
zoom?: AppState["zoom"];
|
||||
@ -1198,8 +1215,8 @@ const getElbowArrowData = (
|
||||
GlobalPoint
|
||||
>(nextPoints[nextPoints.length - 1], vector(arrow.x, arrow.y));
|
||||
|
||||
let hoveredStartElement = startElement;
|
||||
let hoveredEndElement = endElement;
|
||||
let hoveredStartElement = null;
|
||||
let hoveredEndElement = null;
|
||||
if (options?.isDragging) {
|
||||
const elements = Array.from(elementsMap.values());
|
||||
hoveredStartElement =
|
||||
@ -1208,53 +1225,59 @@ const getElbowArrowData = (
|
||||
elementsMap,
|
||||
elements,
|
||||
options?.zoom,
|
||||
) || startElement;
|
||||
) || null;
|
||||
hoveredEndElement =
|
||||
getHoveredElement(
|
||||
origEndGlobalPoint,
|
||||
elementsMap,
|
||||
elements,
|
||||
options?.zoom,
|
||||
) || endElement;
|
||||
) || null;
|
||||
} else {
|
||||
hoveredStartElement = arrow.startBinding
|
||||
? getBindableElementForId(arrow.startBinding.elementId, elementsMap) ||
|
||||
null
|
||||
: null;
|
||||
hoveredEndElement = arrow.endBinding
|
||||
? getBindableElementForId(arrow.endBinding.elementId, elementsMap) || null
|
||||
: null;
|
||||
}
|
||||
|
||||
const startGlobalPoint = getGlobalPoint(
|
||||
{
|
||||
...arrow,
|
||||
type: "arrow",
|
||||
elbowed: true,
|
||||
points: nextPoints,
|
||||
} as ExcalidrawElbowArrowElement,
|
||||
"start",
|
||||
arrow.startBinding?.fixedPoint,
|
||||
origStartGlobalPoint,
|
||||
startElement,
|
||||
hoveredStartElement,
|
||||
options?.isDragging,
|
||||
);
|
||||
const endGlobalPoint = getGlobalPoint(
|
||||
{
|
||||
...arrow,
|
||||
type: "arrow",
|
||||
elbowed: true,
|
||||
points: nextPoints,
|
||||
} as ExcalidrawElbowArrowElement,
|
||||
"end",
|
||||
arrow.endBinding?.fixedPoint,
|
||||
origEndGlobalPoint,
|
||||
endElement,
|
||||
hoveredEndElement,
|
||||
options?.isDragging,
|
||||
);
|
||||
const startHeading = getBindPointHeading(
|
||||
startGlobalPoint,
|
||||
endGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredStartElement,
|
||||
origStartGlobalPoint,
|
||||
);
|
||||
const endHeading = getBindPointHeading(
|
||||
endGlobalPoint,
|
||||
startGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredEndElement,
|
||||
origEndGlobalPoint,
|
||||
);
|
||||
@ -2186,36 +2209,35 @@ const getGlobalPoint = (
|
||||
startOrEnd: "start" | "end",
|
||||
fixedPointRatio: [number, number] | undefined | null,
|
||||
initialPoint: GlobalPoint,
|
||||
boundElement?: ExcalidrawBindableElement | null,
|
||||
hoveredElement?: ExcalidrawBindableElement | null,
|
||||
element?: ExcalidrawBindableElement | null,
|
||||
isDragging?: boolean,
|
||||
): GlobalPoint => {
|
||||
if (isDragging) {
|
||||
if (hoveredElement) {
|
||||
if (element) {
|
||||
const snapPoint = bindPointToSnapToElementOutline(
|
||||
arrow,
|
||||
hoveredElement,
|
||||
element,
|
||||
startOrEnd,
|
||||
);
|
||||
|
||||
return snapToMid(hoveredElement, snapPoint);
|
||||
return snapToMid(element, snapPoint);
|
||||
}
|
||||
|
||||
return initialPoint;
|
||||
}
|
||||
|
||||
if (boundElement) {
|
||||
if (element) {
|
||||
const fixedGlobalPoint = getGlobalFixedPointForBindableElement(
|
||||
fixedPointRatio || [0, 0],
|
||||
boundElement,
|
||||
element,
|
||||
);
|
||||
|
||||
// NOTE: Resize scales the binding position point too, so we need to update it
|
||||
return Math.abs(
|
||||
distanceToBindableElement(boundElement, fixedGlobalPoint) -
|
||||
distanceToBindableElement(element, fixedGlobalPoint) -
|
||||
FIXED_BINDING_DISTANCE,
|
||||
) > 0.01
|
||||
? bindPointToSnapToElementOutline(arrow, boundElement, startOrEnd)
|
||||
? bindPointToSnapToElementOutline(arrow, element, startOrEnd)
|
||||
: fixedGlobalPoint;
|
||||
}
|
||||
|
||||
@ -2225,7 +2247,6 @@ const getGlobalPoint = (
|
||||
const getBindPointHeading = (
|
||||
p: GlobalPoint,
|
||||
otherPoint: GlobalPoint,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||
origPoint: GlobalPoint,
|
||||
): Heading =>
|
||||
@ -2243,7 +2264,6 @@ const getBindPointHeading = (
|
||||
number,
|
||||
],
|
||||
),
|
||||
elementsMap,
|
||||
origPoint,
|
||||
);
|
||||
|
@ -2,10 +2,12 @@
|
||||
* Create and link between shapes.
|
||||
*/
|
||||
|
||||
import { ELEMENT_LINK_KEY } from "../constants";
|
||||
import { normalizeLink } from "../data/url";
|
||||
import { elementsAreInSameGroup } from "../groups";
|
||||
import type { AppProps, AppState } from "../types";
|
||||
import { ELEMENT_LINK_KEY, normalizeLink } from "@excalidraw/common";
|
||||
|
||||
import type { AppProps, AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { elementsAreInSameGroup } from "./groups";
|
||||
|
||||
import type { ExcalidrawElement } from "./types";
|
||||
|
||||
export const defaultGetElementLinkFromSelection: Exclude<
|
@ -1,18 +1,22 @@
|
||||
import { register } from "../actions/register";
|
||||
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
||||
import type { ExcalidrawProps } from "../types";
|
||||
import { escapeDoubleQuotes, getFontString, updateActiveTool } from "../utils";
|
||||
import { setCursorForShape } from "../cursor";
|
||||
import {
|
||||
FONT_FAMILY,
|
||||
VERTICAL_ALIGN,
|
||||
escapeDoubleQuotes,
|
||||
getFontString,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { ExcalidrawProps } from "@excalidraw/excalidraw/types";
|
||||
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { newTextElement } from "./newElement";
|
||||
import { wrapText } from "./textWrapping";
|
||||
import { isIframeElement } from "./typeChecks";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawIframeLikeElement,
|
||||
IframeData,
|
||||
} from "./types";
|
||||
import type { MarkRequired } from "../utility-types";
|
||||
import { CaptureUpdateAction } from "../store";
|
||||
|
||||
type IframeDataWithSandbox = MarkRequired<IframeData, "sandbox">;
|
||||
|
||||
@ -29,6 +33,8 @@ const RE_GH_GIST = /^https:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)/;
|
||||
const RE_GH_GIST_EMBED =
|
||||
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist\.github\.com\/.*?)\.js["']/i;
|
||||
|
||||
const RE_MSFORMS = /^(?:https?:\/\/)?forms\.microsoft\.com\//;
|
||||
|
||||
// not anchored to start to allow <blockquote> twitter embeds
|
||||
const RE_TWITTER =
|
||||
/(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/;
|
||||
@ -65,6 +71,7 @@ const ALLOWED_DOMAINS = new Set([
|
||||
"val.town",
|
||||
"giphy.com",
|
||||
"reddit.com",
|
||||
"forms.microsoft.com",
|
||||
]);
|
||||
|
||||
const ALLOW_SAME_ORIGIN = new Set([
|
||||
@ -78,6 +85,7 @@ const ALLOW_SAME_ORIGIN = new Set([
|
||||
"*.simplepdf.eu",
|
||||
"stackblitz.com",
|
||||
"reddit.com",
|
||||
"forms.microsoft.com",
|
||||
]);
|
||||
|
||||
export const createSrcDoc = (body: string) => {
|
||||
@ -202,6 +210,10 @@ export const getEmbedLink = (
|
||||
};
|
||||
}
|
||||
|
||||
if (RE_MSFORMS.test(link) && !link.includes("embed=true")) {
|
||||
link += link.includes("?") ? "&embed=true" : "?embed=true";
|
||||
}
|
||||
|
||||
if (RE_TWITTER.test(link)) {
|
||||
const postId = link.match(RE_TWITTER)![1];
|
||||
// the embed srcdoc still supports twitter.com domain only.
|
||||
@ -317,34 +329,6 @@ export const createPlaceholderEmbeddableLabel = (
|
||||
});
|
||||
};
|
||||
|
||||
export const actionSetEmbeddableAsActiveTool = register({
|
||||
name: "setEmbeddableAsActiveTool",
|
||||
trackEvent: { category: "toolbar" },
|
||||
target: "Tool",
|
||||
label: "toolBar.embeddable",
|
||||
perform: (elements, appState, _, app) => {
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "embeddable",
|
||||
});
|
||||
|
||||
setCursorForShape(app.canvas, {
|
||||
...appState,
|
||||
activeTool: nextActiveTool,
|
||||
});
|
||||
|
||||
return {
|
||||
elements,
|
||||
appState: {
|
||||
...appState,
|
||||
activeTool: updateActiveTool(appState, {
|
||||
type: "embeddable",
|
||||
}),
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const matchHostname = (
|
||||
url: string,
|
||||
/** using a Set assumes it already contains normalized bare domains */
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user